Controller_TypeChar.cs 21.0 KB
Newer Older
1 2 3
// Copyright (c) Microsoft.  All Rights Reserved.  Licensed under the Apache License, Version 2.0.  See License.txt in the project root for license information.

using System;
B
Balaji Krishnan 已提交
4
using System.Diagnostics;
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
using Microsoft.CodeAnalysis.Completion;
using Microsoft.CodeAnalysis.Editor.Commands;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Options;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Text;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.Completion
{
    internal partial class Controller
    {
        CommandState ICommandHandler<TypeCharCommandArgs>.GetCommandState(TypeCharCommandArgs args, Func<CommandState> nextHandler)
        {
            AssertIsForeground();

            // We just defer to the editor here.  We do not interfere with typing normal characters.
            return nextHandler();
        }

        void ICommandHandler<TypeCharCommandArgs>.ExecuteCommand(TypeCharCommandArgs args, Action nextHandler)
        {
B
Balaji Krishnan 已提交
28 29
            Trace.WriteLine("Entered completion command handler for typechar.");

30 31
            AssertIsForeground();

32 33 34 35 36 37 38 39 40 41 42
            // When a character is typed it is *always* sent through to the editor.  This way the
            // editor always represents what would have been typed had completion not been involved
            // at this point.  That means that if we decide to commit, then undo'ing the commit will
            // return you to the code that you would have typed if completion was not up.
            //
            // The steps we follow for commit are as follows:
            //
            //      1) send the commit character through to the buffer.
            //      2) open a transaction.
            //          2a) roll back the text to before the text was sent through
            //          2b) commit the item.
C
CyrusNajmabadi 已提交
43
            //          2c) send the commit character through again.*
44 45 46 47 48 49 50 51 52
            //          2d) commit the transaction.
            //
            // 2c is very important.  it makes sure that post our commit all our normal features
            // run depending on what got typed.  For example if the commit character was (
            // then brace completion may run.  If it was ; then formatting may run.  But, importantly
            // this code doesn't need to know anything about that.  Furthermore, becaue that code
            // runs within this transaction, then the user can always undo and get to what the code
            // would have been if completion was not involved.
            //
C
CyrusNajmabadi 已提交
53 54 55 56 57 58
            // 2c*: note sending the commit character through to the buffer again can be controlled
            // by the completion item.  For example, completion items that want to totally handle
            // what gets output into the buffer can ask for this not to happen.  An example of this
            // is override completion.  If the user types "override Method(" then we'll want to 
            // spit out the entire method and *not* also spit out "(" again.

59 60 61
            // In order to support 2a (rolling back), we capture hte state of the buffer before
            // we send the character through.  We then just apply the edits in reverse order to
            // roll us back.
C
CyrusNajmabadi 已提交
62 63
            var initialTextSnapshot = this.SubjectBuffer.CurrentSnapshot;

64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
            var initialCaretPosition = GetCaretPointInViewBuffer();

            // Note: while we're doing this, we don't want to hear about buffer changes (since we
            // know they're going to happen).  So we disconnect and reconnect to the event
            // afterwards.  That way we can hear about changes to the buffer that don't happen
            // through us.

            // Automatic Brace Completion may also move the caret, so unsubscribe from that too
            this.TextView.TextBuffer.PostChanged -= OnTextViewBufferPostChanged;
            this.TextView.Caret.PositionChanged -= OnCaretPositionChanged;

            // In Venus/Razor, the user might be typing on the buffer's seam. This means that,
            // depending on the character typed, the character may not go into our buffer.
            var isOnSeam = IsOnSeam();

            try
            {
                nextHandler();
            }
            finally
            {
                this.TextView.TextBuffer.PostChanged += OnTextViewBufferPostChanged;
                this.TextView.Caret.PositionChanged += OnCaretPositionChanged;
            }

            // We only want to process typechar if it is a normal typechar and no one else is
            // involved.  i.e. if there was a typechar, but someone processed it and moved the caret
            // somewhere else then we don't want completion.  Also, if a character was typed but
            // something intercepted and placed different text into the editor, then we don't want
            // to proceed. 
            if (this.TextView.TypeCharWasHandledStrangely(this.SubjectBuffer, args.TypedChar))
            {
B
Balaji Krishnan 已提交
96 97
                Trace.WriteLine("typechar was handled by someone else, cannot have a completion session.");

98 99 100 101 102 103 104 105
                if (sessionOpt != null)
                {
                    // If we're on a seam (razor) with a computation, and the user types a character 
                    // that goes into the other side of the seam, the character may be a commit character.
                    // If it's a commit character, just commit without trying to check caret position,
                    // since the caret is no longer in our buffer.
                    if (isOnSeam && this.IsCommitCharacter(args.TypedChar))
                    {
B
Balaji Krishnan 已提交
106 107
                        Trace.WriteLine("typechar was on seam and a commit char, cannot have a completion session.");

108
                        this.CommitOnTypeChar(args.TypedChar, initialTextSnapshot, nextHandler);
109 110 111 112 113 114
                        return;
                    }
                    else if (_autoBraceCompletionChars.Contains(args.TypedChar) &&
                             this.SubjectBuffer.GetOption(InternalFeatureOnOffOptions.AutomaticPairCompletion) &&
                             this.IsCommitCharacter(args.TypedChar))
                    {
B
Balaji Krishnan 已提交
115 116
                        Trace.WriteLine("typechar was brace completion char and a commit char, cannot have a completion session.");

117 118
                        // I don't think there is any better way than this. if typed char is one of auto brace completion char,
                        // we don't do multiple buffer change check
119
                        this.CommitOnTypeChar(args.TypedChar, initialTextSnapshot, nextHandler);
120 121 122 123
                        return;
                    }
                    else
                    {
B
Balaji Krishnan 已提交
124 125
                        Trace.WriteLine("we stop model computation, cannot have a completion session.");

126 127 128 129 130 131 132 133 134
                        // If we were computing anything, we stop.  We only want to process a typechar
                        // if it was a normal character.
                        this.StopModelComputation();
                    }
                }

                return;
            }

D
Dustin Campbell 已提交
135
            var completionService = this.GetCompletionService();
136 137
            if (completionService == null)
            {
B
Balaji Krishnan 已提交
138 139
                Trace.WriteLine("handling typechar, completion service is null, cannot have a completion session.");

140 141 142 143 144 145 146
                return;
            }

            var options = GetOptions();
            Contract.ThrowIfNull(options);

            var isTextuallyTriggered = IsTextualTriggerCharacter(completionService, args.TypedChar, options);
C
Charles Stoner 已提交
147
            var isPotentialFilterCharacter = IsPotentialFilterCharacter(args);
M
Matt Warren 已提交
148
            var trigger = CompletionTrigger.CreateInsertionTrigger(args.TypedChar);
149 150 151 152 153 154 155 156

            if (sessionOpt == null)
            {
                // No computation at all.  If this is not a trigger character, we just ignore it and
                // stay in this state.  Otherwise, if it's a trigger character, start up a new
                // computation and start computing the model in the background.
                if (isTextuallyTriggered)
                {
B
Balaji Krishnan 已提交
157 158
                    Trace.WriteLine("no completion session yet and this is a trigger char, starting model computation.");

159 160
                    // First create the session that represents that we now have a potential
                    // completion list.  Then tell it to start computing.
M
Matt Warren 已提交
161
                    StartNewModelComputation(completionService, trigger, filterItems: true);
162 163 164 165
                    return;
                }
                else
                {
B
Balaji Krishnan 已提交
166 167
                    Trace.WriteLine("no completion session yet and this is NOT a trigger char, we won't have completion.");

168 169 170 171 172 173
                    // No need to do anything.  Just stay in the state where we have no session.
                    return;
                }
            }
            else
            {
B
Balaji Krishnan 已提交
174 175
                Trace.WriteLine("we have a completion session.");

176 177 178 179 180 181 182 183
                sessionOpt.UpdateModelTrackingSpan(initialCaretPosition);

                // If the session is up, it may be in one of many states.  It may know nothing
                // (because it is currently computing the list of completions).  Or it may have a
                // list of completions that it has filtered. 

                // If the user types something which is absolutely known to be a filter character
                // then we can just proceed without blocking.
C
Charles Stoner 已提交
184
                if (isPotentialFilterCharacter)
185 186 187
                {
                    if (isTextuallyTriggered)
                    {
B
Balaji Krishnan 已提交
188 189
                        Trace.WriteLine("computing completion again and filtering...");

190 191 192 193
                        // The character typed was something like "a".  It can both filter a list if
                        // we have computed one, or it can trigger a new list.  Ask the computation
                        // to compute again. If nothing has been computed, then it will try to
                        // compute again, otherwise it will just ignore this request.
M
Matt Warren 已提交
194
                        sessionOpt.ComputeModel(completionService, trigger, _roles, options);
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219
                    }

                    // Now filter whatever result we have.
                    sessionOpt.FilterModel(CompletionFilterReason.TypeChar);
                }
                else
                {
                    // It wasn't a trigger or filter character. At this point, we make our
                    // determination on what to do based on what has actually been computed and
                    // what's being typed. This means waiting on the session and will effectively
                    // block the user.

                    // Again, from this point on we must block on the computation to decide what to
                    // do.

                    // What they type may end up filtering, committing, or else will dismiss.
                    //
                    // For example, we may filter in cases like this: "Color."
                    //
                    // "Color" will have already filtered the list down to some things like
                    // "Color", "Color.Red", "Color.Blue", etc.  When we process the 'dot', we
                    // actually want to filter some more.  But we can't know that ahead of time until
                    // we have computed the list of completions.
                    if (this.IsFilterCharacter(args.TypedChar))
                    {
B
Balaji Krishnan 已提交
220 221
                        Trace.WriteLine("filtering the session...");

222 223 224 225 226 227 228 229 230 231 232 233 234
                        // Known to be a filter character for the currently selected item.  So just 
                        // filter the session.
                        sessionOpt.FilterModel(CompletionFilterReason.TypeChar);
                        return;
                    }

                    // It wasn't a filter character.  We'll either commit what's selected, or we'll
                    // dismiss the completion list.  First, ensure that what was typed is in the
                    // buffer.

                    // Now, commit if it was a commit character.
                    if (this.IsCommitCharacter(args.TypedChar))
                    {
B
Balaji Krishnan 已提交
235 236
                        Trace.WriteLine("committing the session...");

237 238
                        // Known to be a commit character for the currently selected item.  So just
                        // commit the session.
239
                        this.CommitOnTypeChar(args.TypedChar, initialTextSnapshot, nextHandler);
240 241 242
                    }
                    else
                    {
B
Balaji Krishnan 已提交
243 244
                        Trace.WriteLine("dismissing the session...");

245 246 247 248 249 250 251 252 253
                        // Now dismiss the session.
                        this.StopModelComputation();
                    }

                    // The character may commit/dismiss and then trigger completion again. So check
                    // for that here.

                    if (isTextuallyTriggered)
                    {
B
Balaji Krishnan 已提交
254 255
                        Trace.WriteLine("the char commit/dismiss -ed a session and is trigerring completion again. starting model computation.");

256 257
                        // First create the session that represents that we now have a potential
                        // completion list.
M
Matt Warren 已提交
258
                        StartNewModelComputation(completionService, trigger, filterItems: true);
259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281
                        return;
                    }
                }
            }
        }

        private bool IsOnSeam()
        {
            var caretPoint = TextView.Caret.Position.Point;
            var point1 = caretPoint.GetPoint(this.SubjectBuffer, PositionAffinity.Predecessor);
            var point2 = caretPoint.GetPoint(this.SubjectBuffer, PositionAffinity.Successor);
            if (point1.HasValue && point1 != point2)
            {
                return true;
            }

            return false;
        }

        /// <summary>
        /// A potential filter character is something that can filter a completion lists and is
        /// *guaranteed* to not be a commit character.
        /// </summary>
C
Charles Stoner 已提交
282
        private static bool IsPotentialFilterCharacter(TypeCharCommandArgs args)
283 284 285 286 287 288 289
        {
            // TODO(cyrusn): Actually use the right unicode categories here.
            return char.IsLetter(args.TypedChar)
                || char.IsNumber(args.TypedChar)
                || args.TypedChar == '_';
        }

M
Matt Warren 已提交
290
        private CompletionHelper GetCompletionHelper()
291 292 293 294
        {
            var document = this.SubjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges();
            if (document != null)
            {
C
CyrusNajmabadi 已提交
295
                return CompletionHelper.GetHelper(document);
296 297
            }

298
            return null;
299 300
        }

M
Matt Warren 已提交
301
        private bool IsTextualTriggerCharacter(CompletionService completionService, char ch, OptionSet options)
302 303 304 305 306 307 308
        {
            AssertIsForeground();

            // Note: When this function is called we've already guaranteed that
            // TypeCharWasHandledStrangely returned false.  That means we know that the caret is in
            // our buffer, and is after the character just typed.

M
Matt Warren 已提交
309 310
            var caretPosition = this.TextView.GetCaretPoint(this.SubjectBuffer).Value;
            var previousPosition = caretPosition - 1;
311 312
            Contract.ThrowIfFalse(this.SubjectBuffer.CurrentSnapshot[previousPosition] == ch);

M
Matt Warren 已提交
313 314
            var trigger = CompletionTrigger.CreateInsertionTrigger(ch);
            return completionService.ShouldTriggerCompletion(previousPosition.Snapshot.AsText(), caretPosition, trigger, _roles, options);
315 316 317 318 319 320 321 322 323 324 325 326 327
        }

        private bool IsCommitCharacter(char ch)
        {
            AssertIsForeground();

            // TODO(cyrusn): Find a way to allow the user to cancel out of this.
            var model = sessionOpt.WaitForModel();
            if (model == null || model.IsSoftSelection)
            {
                return false;
            }

M
Matt Warren 已提交
328
            if (model.SelectedItem.IsSuggestionModeItem)
329 330 331 332
            {
                return char.IsLetterOrDigit(ch);
            }

333
            var completionService = GetCompletionService();
C
CyrusNajmabadi 已提交
334
            var textTypedSoFar = GetTextTypedSoFar(model, model.SelectedItem.Item);
335
            return IsCommitCharacter(
C
CyrusNajmabadi 已提交
336
                completionService.GetRules(), model.SelectedItem.Item, ch, textTypedSoFar);
337 338
        }

C
CyrusNajmabadi 已提交
339 340 341 342
        /// <summary>
        /// Internal for testing purposes only.
        /// </summary>
        internal static bool IsCommitCharacter(
C
CyrusNajmabadi 已提交
343
            CompletionRules completionRules, CompletionItem item, char ch, string textTypedSoFar)
344
        {
C
CyrusNajmabadi 已提交
345
            // First see if the item has any specifc commit rules it wants followed.
346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368
            foreach (var rule in item.Rules.CommitCharacterRules)
            {
                switch (rule.Kind)
                {
                    case CharacterSetModificationKind.Add:
                        if (rule.Characters.Contains(ch))
                        {
                            return true;
                        }
                        continue;

                    case CharacterSetModificationKind.Remove:
                        if (rule.Characters.Contains(ch))
                        {
                            return false;
                        }
                        continue;

                    case CharacterSetModificationKind.Replace:
                        return rule.Characters.Contains(ch);
                }
            }

C
CyrusNajmabadi 已提交
369 370 371 372 373 374
            // general rule: if the filtering text exactly matches the start of the item then it must be a filter character
            if (TextTypedSoFarMatchesItem(item, ch, textTypedSoFar))
            {
                return false;
            }

375 376
            // Fall back to the default rules for this language's completion service.
            return completionRules.DefaultCommitCharacters.IndexOf(ch) >= 0;
377 378 379 380 381 382 383 384 385 386 387 388 389
        }

        private bool IsFilterCharacter(char ch)
        {
            AssertIsForeground();

            // TODO(cyrusn): Find a way to allow the user to cancel out of this.
            var model = sessionOpt.WaitForModel();
            if (model == null)
            {
                return false;
            }

M
Matt Warren 已提交
390
            if (model.SelectedItem.IsSuggestionModeItem)
391 392 393 394
            {
                return char.IsLetterOrDigit(ch);
            }

C
CyrusNajmabadi 已提交
395 396
            var textTypedSoFar = GetTextTypedSoFar(model, model.SelectedItem.Item);
            return IsFilterCharacter(model.SelectedItem.Item, ch, textTypedSoFar);
397 398 399 400
        }

        private static bool TextTypedSoFarMatchesItem(CompletionItem item, char ch, string textTypedSoFar)
        {
C
CyrusNajmabadi 已提交
401
            if (textTypedSoFar.Length > 0)
M
Matt Warren 已提交
402
            {
C
CyrusNajmabadi 已提交
403 404
                return item.DisplayText.StartsWith(textTypedSoFar, StringComparison.CurrentCultureIgnoreCase) ||
                       item.FilterText.StartsWith(textTypedSoFar, StringComparison.CurrentCultureIgnoreCase);
405 406
            }

C
CyrusNajmabadi 已提交
407 408 409 410 411 412
            return false;
        }

        private static bool IsFilterCharacter(CompletionItem item, char ch, string textTypedSoFar)
        {
            // First see if the item has any specific filter rules it wants followed.
413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433
            foreach (var rule in item.Rules.FilterCharacterRules)
            {
                switch (rule.Kind)
                {
                    case CharacterSetModificationKind.Add:
                        if (rule.Characters.Contains(ch))
                        {
                            return true;
                        }
                        continue;

                    case CharacterSetModificationKind.Remove:
                        if (rule.Characters.Contains(ch))
                        {
                            return false;
                        }
                        continue;

                    case CharacterSetModificationKind.Replace:
                        return rule.Characters.Contains(ch);
                }
M
Matt Warren 已提交
434
            }
435

C
CyrusNajmabadi 已提交
436 437 438 439 440 441
            // general rule: if the filtering text exactly matches the start of the item then it must be a filter character
            if (TextTypedSoFarMatchesItem(item, ch, textTypedSoFar))
            {
                return true;
            }

M
Matt Warren 已提交
442
            return false;
443 444
        }

C
CyrusNajmabadi 已提交
445
        private string GetTextTypedSoFar(Model model, CompletionItem selectedItem)
446 447
        {
            var textSnapshot = this.TextView.TextSnapshot;
M
Matt Warren 已提交
448
            var viewSpan = model.GetViewBufferSpan(selectedItem.Span);
449 450 451 452 453
            var filterText = model.GetCurrentTextInSnapshot(
                viewSpan, textSnapshot, GetCaretPointInViewBuffer());
            return filterText;
        }

C
CyrusNajmabadi 已提交
454
        private void CommitOnTypeChar(
455
            char ch, ITextSnapshot initialTextSnapshot, Action nextHandler)
456 457 458 459 460 461 462 463 464 465 466
        {
            AssertIsForeground();

            // Note: this function is called after the character has already been inserted into the
            // buffer.
            var model = sessionOpt.WaitForModel();

            // We only call CommitOnTypeChar if ch was a commit character.  And we only know if ch
            // was commit character if we had a selected item.
            Contract.ThrowIfNull(model);

467 468 469
            this.Commit(
                model.SelectedItem, model, ch,
                initialTextSnapshot, nextHandler);
470 471
        }
    }
472
}