Controller_TypeChar.cs 21.5 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
            //          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
C
CyrusNajmabadi 已提交
49
            // this code doesn't need to know anything about that.  Furthermore, because that code
50 51 52
            // 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.
161
                    StartNewModelComputation(completionService, trigger, filterItems: true, dismissIfEmptyAllowed: 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
                    }

                    // Now filter whatever result we have.
198 199 200
                    sessionOpt.FilterModel(
                        CompletionFilterReason.TypeChar,
                        recheckCaretPosition: false,
201
                        dismissIfEmptyAllowed: true,
202
                        filterState: null);
203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223
                }
                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 已提交
224 225
                        Trace.WriteLine("filtering the session...");

226 227
                        // Known to be a filter character for the currently selected item.  So just 
                        // filter the session.
228 229
                        sessionOpt.FilterModel(CompletionFilterReason.TypeChar,
                            recheckCaretPosition: false,
230
                            dismissIfEmptyAllowed: true,
231
                            filterState: null);
232 233 234 235 236 237 238 239 240 241
                        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 已提交
242 243
                        Trace.WriteLine("committing the session...");

244 245
                        // Known to be a commit character for the currently selected item.  So just
                        // commit the session.
246
                        this.CommitOnTypeChar(args.TypedChar, initialTextSnapshot, nextHandler);
247 248 249
                    }
                    else
                    {
B
Balaji Krishnan 已提交
250 251
                        Trace.WriteLine("dismissing the session...");

252 253 254 255 256 257 258 259 260
                        // 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 已提交
261 262
                        Trace.WriteLine("the char commit/dismiss -ed a session and is trigerring completion again. starting model computation.");

263 264
                        // First create the session that represents that we now have a potential
                        // completion list.
265 266
                        StartNewModelComputation(
                            completionService, trigger, filterItems: true, dismissIfEmptyAllowed: true);
267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289
                        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 已提交
290
        private static bool IsPotentialFilterCharacter(TypeCharCommandArgs args)
291 292 293 294 295 296 297
        {
            // TODO(cyrusn): Actually use the right unicode categories here.
            return char.IsLetter(args.TypedChar)
                || char.IsNumber(args.TypedChar)
                || args.TypedChar == '_';
        }

298 299 300 301 302
        private Document GetDocument()
        {
            return this.SubjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges();
        }

M
Matt Warren 已提交
303
        private CompletionHelper GetCompletionHelper()
304
        {
305
            var document = GetDocument();
306 307
            if (document != null)
            {
C
CyrusNajmabadi 已提交
308
                return CompletionHelper.GetHelper(document);
309 310
            }

311
            return null;
312 313
        }

M
Matt Warren 已提交
314
        private bool IsTextualTriggerCharacter(CompletionService completionService, char ch, OptionSet options)
315 316 317 318 319 320 321
        {
            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 已提交
322 323
            var caretPosition = this.TextView.GetCaretPoint(this.SubjectBuffer).Value;
            var previousPosition = caretPosition - 1;
324 325
            Contract.ThrowIfFalse(this.SubjectBuffer.CurrentSnapshot[previousPosition] == ch);

M
Matt Warren 已提交
326 327
            var trigger = CompletionTrigger.CreateInsertionTrigger(ch);
            return completionService.ShouldTriggerCompletion(previousPosition.Snapshot.AsText(), caretPosition, trigger, _roles, options);
328 329 330 331 332 333 334 335 336 337 338 339 340
        }

        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 已提交
341
            if (model.SelectedItem.IsSuggestionModeItem)
342 343 344 345
            {
                return char.IsLetterOrDigit(ch);
            }

346
            var completionService = GetCompletionService();
C
CyrusNajmabadi 已提交
347
            var textTypedSoFar = GetTextTypedSoFar(model, model.SelectedItem.Item);
348
            return IsCommitCharacter(
C
CyrusNajmabadi 已提交
349
                completionService.GetRules(), model.SelectedItem.Item, ch, textTypedSoFar);
350 351
        }

C
CyrusNajmabadi 已提交
352 353 354 355
        /// <summary>
        /// Internal for testing purposes only.
        /// </summary>
        internal static bool IsCommitCharacter(
C
CyrusNajmabadi 已提交
356
            CompletionRules completionRules, CompletionItem item, char ch, string textTypedSoFar)
357
        {
C
CyrusNajmabadi 已提交
358
            // First see if the item has any specifc commit rules it wants followed.
359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381
            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 已提交
382 383 384 385 386 387
            // 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;
            }

388 389
            // Fall back to the default rules for this language's completion service.
            return completionRules.DefaultCommitCharacters.IndexOf(ch) >= 0;
390 391 392 393 394 395 396 397 398 399 400 401 402
        }

        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 已提交
403
            if (model.SelectedItem.IsSuggestionModeItem)
404 405 406 407
            {
                return char.IsLetterOrDigit(ch);
            }

C
CyrusNajmabadi 已提交
408 409
            var textTypedSoFar = GetTextTypedSoFar(model, model.SelectedItem.Item);
            return IsFilterCharacter(model.SelectedItem.Item, ch, textTypedSoFar);
410 411 412 413
        }

        private static bool TextTypedSoFarMatchesItem(CompletionItem item, char ch, string textTypedSoFar)
        {
C
CyrusNajmabadi 已提交
414
            if (textTypedSoFar.Length > 0)
M
Matt Warren 已提交
415
            {
C
CyrusNajmabadi 已提交
416 417
                return item.DisplayText.StartsWith(textTypedSoFar, StringComparison.CurrentCultureIgnoreCase) ||
                       item.FilterText.StartsWith(textTypedSoFar, StringComparison.CurrentCultureIgnoreCase);
418 419
            }

C
CyrusNajmabadi 已提交
420 421 422 423 424 425
            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.
426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446
            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 已提交
447
            }
448

C
CyrusNajmabadi 已提交
449 450 451 452 453 454
            // 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 已提交
455
            return false;
456 457
        }

C
CyrusNajmabadi 已提交
458
        private string GetTextTypedSoFar(Model model, CompletionItem selectedItem)
459 460
        {
            var textSnapshot = this.TextView.TextSnapshot;
M
Matt Warren 已提交
461
            var viewSpan = model.GetViewBufferSpan(selectedItem.Span);
462 463 464 465 466
            var filterText = model.GetCurrentTextInSnapshot(
                viewSpan, textSnapshot, GetCaretPointInViewBuffer());
            return filterText;
        }

C
CyrusNajmabadi 已提交
467
        private void CommitOnTypeChar(
468
            char ch, ITextSnapshot initialTextSnapshot, Action nextHandler)
469 470 471 472 473 474 475 476 477 478 479
        {
            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);

480 481 482
            this.Commit(
                model.SelectedItem, model, ch,
                initialTextSnapshot, nextHandler);
483 484
        }
    }
485
}