Controller_TypeChar.cs 19.6 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
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;
M
Matt Warren 已提交
13
using System.Threading;
14
using Microsoft.CodeAnalysis.Shared.Extensions;
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29

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 已提交
30 31
            Trace.WriteLine("Entered completion command handler for typechar.");

32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
            AssertIsForeground();

            var initialCaretPosition = GetCaretPointInViewBuffer();

            // 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.  After we send the character into the buffer we then decide what to do
            // with the completion set.  If we decide to commit it then we will replace the
            // appropriate span (which will include the character just sent to the buffer) with the
            // appropriate insertion text *and* the character typed.  This way, after we commit, the
            // editor has the insertion text of the selected item, and the character typed.  It
            // also means that if we then undo that we'll see the text that would have been typed
            // had no completion been active.

            // 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 已提交
76 77
                Trace.WriteLine("typechar was handled by someone else, cannot have a completion session.");

78 79 80 81 82 83 84 85
                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 已提交
86 87
                        Trace.WriteLine("typechar was on seam and a commit char, cannot have a completion session.");

88 89 90 91 92 93 94
                        this.CommitOnTypeChar(args.TypedChar);
                        return;
                    }
                    else if (_autoBraceCompletionChars.Contains(args.TypedChar) &&
                             this.SubjectBuffer.GetOption(InternalFeatureOnOffOptions.AutomaticPairCompletion) &&
                             this.IsCommitCharacter(args.TypedChar))
                    {
B
Balaji Krishnan 已提交
95 96
                        Trace.WriteLine("typechar was brace completion char and a commit char, cannot have a completion session.");

97 98 99 100 101 102 103
                        // 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
                        this.CommitOnTypeChar(args.TypedChar);
                        return;
                    }
                    else
                    {
B
Balaji Krishnan 已提交
104 105
                        Trace.WriteLine("we stop model computation, cannot have a completion session.");

106 107 108 109 110 111 112 113 114
                        // 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 已提交
115
            var completionService = this.GetCompletionService();
116 117
            if (completionService == null)
            {
B
Balaji Krishnan 已提交
118 119
                Trace.WriteLine("handling typechar, completion service is null, cannot have a completion session.");

120 121 122 123 124 125 126
                return;
            }

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

            var isTextuallyTriggered = IsTextualTriggerCharacter(completionService, args.TypedChar, options);
C
Charles Stoner 已提交
127
            var isPotentialFilterCharacter = IsPotentialFilterCharacter(args);
M
Matt Warren 已提交
128
            var trigger = CompletionTrigger.CreateInsertionTrigger(args.TypedChar);
129 130 131 132 133 134 135 136

            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 已提交
137 138
                    Trace.WriteLine("no completion session yet and this is a trigger char, starting model computation.");

139 140
                    // First create the session that represents that we now have a potential
                    // completion list.  Then tell it to start computing.
M
Matt Warren 已提交
141
                    StartNewModelComputation(completionService, trigger, filterItems: true);
142 143 144 145
                    return;
                }
                else
                {
B
Balaji Krishnan 已提交
146 147
                    Trace.WriteLine("no completion session yet and this is NOT a trigger char, we won't have completion.");

148 149 150 151 152 153
                    // No need to do anything.  Just stay in the state where we have no session.
                    return;
                }
            }
            else
            {
B
Balaji Krishnan 已提交
154 155
                Trace.WriteLine("we have a completion session.");

156 157 158 159 160 161 162 163
                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 已提交
164
                if (isPotentialFilterCharacter)
165 166 167
                {
                    if (isTextuallyTriggered)
                    {
B
Balaji Krishnan 已提交
168 169
                        Trace.WriteLine("computing completion again and filtering...");

170 171 172 173
                        // 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 已提交
174
                        sessionOpt.ComputeModel(completionService, trigger, _roles, options);
175 176 177
                    }

                    // Now filter whatever result we have.
178 179 180 181
                    sessionOpt.FilterModel(
                        CompletionFilterReason.TypeChar,
                        recheckCaretPosition: false,
                        filterState: null);
182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202
                }
                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 已提交
203 204
                        Trace.WriteLine("filtering the session...");

205 206
                        // Known to be a filter character for the currently selected item.  So just 
                        // filter the session.
207 208 209
                        sessionOpt.FilterModel(CompletionFilterReason.TypeChar,
                            recheckCaretPosition: false,
                            filterState: null);
210 211 212 213 214 215 216 217 218 219
                        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 已提交
220 221
                        Trace.WriteLine("committing the session...");

222 223 224 225 226 227
                        // Known to be a commit character for the currently selected item.  So just
                        // commit the session.
                        this.CommitOnTypeChar(args.TypedChar);
                    }
                    else
                    {
B
Balaji Krishnan 已提交
228 229
                        Trace.WriteLine("dismissing the session...");

230 231 232 233 234 235 236 237 238
                        // 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 已提交
239 240
                        Trace.WriteLine("the char commit/dismiss -ed a session and is trigerring completion again. starting model computation.");

241 242
                        // First create the session that represents that we now have a potential
                        // completion list.
M
Matt Warren 已提交
243
                        StartNewModelComputation(completionService, trigger, filterItems: true);
244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266
                        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 已提交
267
        private static bool IsPotentialFilterCharacter(TypeCharCommandArgs args)
268 269 270 271 272 273 274
        {
            // TODO(cyrusn): Actually use the right unicode categories here.
            return char.IsLetter(args.TypedChar)
                || char.IsNumber(args.TypedChar)
                || args.TypedChar == '_';
        }

M
Matt Warren 已提交
275
        private CompletionHelper GetCompletionHelper()
276 277 278 279
        {
            var document = this.SubjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges();
            if (document != null)
            {
C
CyrusNajmabadi 已提交
280
                return CompletionHelper.GetHelper(document);
281 282
            }

283
            return null;
284 285
        }

M
Matt Warren 已提交
286
        private bool IsTextualTriggerCharacter(CompletionService completionService, char ch, OptionSet options)
287 288 289 290 291 292 293
        {
            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 已提交
294 295
            var caretPosition = this.TextView.GetCaretPoint(this.SubjectBuffer).Value;
            var previousPosition = caretPosition - 1;
296 297
            Contract.ThrowIfFalse(this.SubjectBuffer.CurrentSnapshot[previousPosition] == ch);

M
Matt Warren 已提交
298 299
            var trigger = CompletionTrigger.CreateInsertionTrigger(ch);
            return completionService.ShouldTriggerCompletion(previousPosition.Snapshot.AsText(), caretPosition, trigger, _roles, options);
300 301 302 303 304 305 306 307 308 309 310 311 312
        }

        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 已提交
313
            if (model.SelectedItem.IsSuggestionModeItem)
314 315 316 317
            {
                return char.IsLetterOrDigit(ch);
            }

318 319 320 321 322 323
            var completionService = GetCompletionService();
            var filterText = GetCurrentFilterText(model, model.SelectedItem.Item);
            return IsCommitCharacter(
                completionService.GetRules(), model.SelectedItem.Item, ch, filterText);
        }

C
CyrusNajmabadi 已提交
324 325 326 327
        /// <summary>
        /// Internal for testing purposes only.
        /// </summary>
        internal static bool IsCommitCharacter(
328 329 330 331
            CompletionRules completionRules, CompletionItem item, char ch, string filterText)
        {
            // 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: filterText))
M
Matt Warren 已提交
332
            {
333
                return false;
M
Matt Warren 已提交
334
            }
335

336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360
            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);
                }
            }

            // Fall back to the default rules for this language's completion service.
            return completionRules.DefaultCommitCharacters.IndexOf(ch) >= 0;
361 362 363 364 365 366 367 368 369 370 371 372 373
        }

        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 已提交
374
            if (model.SelectedItem.IsSuggestionModeItem)
375 376 377 378
            {
                return char.IsLetterOrDigit(ch);
            }

379 380 381 382 383 384 385 386 387 388 389
            var filterText = GetCurrentFilterText(model, model.SelectedItem.Item);
            return IsFilterCharacter(model.SelectedItem.Item, ch, filterText);
        }

        private static bool TextTypedSoFarMatchesItem(CompletionItem item, char ch, string textTypedSoFar)
        {
            var textTypedWithChar = textTypedSoFar + ch;
            return item.DisplayText.StartsWith(textTypedWithChar, StringComparison.CurrentCultureIgnoreCase) ||
                item.FilterText.StartsWith(textTypedWithChar, StringComparison.CurrentCultureIgnoreCase);
        }

C
CyrusNajmabadi 已提交
390 391 392 393
        /// <summary>
        /// Internal for testing purposes only.
        /// </summary>
        internal static bool IsFilterCharacter(CompletionItem item, char ch, string filterText)
394 395 396
        {
            // 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: filterText))
M
Matt Warren 已提交
397
            {
398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421
                return false;
            }

            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 已提交
422
            }
423

M
Matt Warren 已提交
424
            return false;
425 426 427 428 429
        }

        private string GetCurrentFilterText(Model model, CompletionItem selectedItem)
        {
            var textSnapshot = this.TextView.TextSnapshot;
M
Matt Warren 已提交
430
            var viewSpan = model.GetViewBufferSpan(selectedItem.Span);
431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447
            var filterText = model.GetCurrentTextInSnapshot(
                viewSpan, textSnapshot, GetCaretPointInViewBuffer());
            return filterText;
        }

        private void CommitOnTypeChar(char ch)
        {
            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);

M
Matt Warren 已提交
448
            this.Commit(model.SelectedItem, model, ch);
449 450 451
        }
    }
}