Controller_TypeChar.cs 16.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 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199
                    }

                    // 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 已提交
200 201
                        Trace.WriteLine("filtering the session...");

202 203 204 205 206 207 208 209 210 211 212 213 214
                        // 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 已提交
215 216
                        Trace.WriteLine("committing the session...");

217 218 219 220 221 222
                        // Known to be a commit character for the currently selected item.  So just
                        // commit the session.
                        this.CommitOnTypeChar(args.TypedChar);
                    }
                    else
                    {
B
Balaji Krishnan 已提交
223 224
                        Trace.WriteLine("dismissing the session...");

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

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

M
Matt Warren 已提交
270
        private CompletionHelper GetCompletionHelper()
271 272 273 274
        {
            var document = this.SubjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges();
            if (document != null)
            {
275 276
                return CompletionHelper.GetHelper(
                    document, document.GetLanguageService<CompletionService>());
277 278
            }

279
            return null;
280 281
        }

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

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

        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 已提交
309
            if (model.SelectedItem.IsSuggestionModeItem)
310 311 312 313
            {
                return char.IsLetterOrDigit(ch);
            }

M
Matt Warren 已提交
314 315 316 317 318 319
            var helper = GetCompletionHelper();
            if (helper != null)
            {
                var filterText = GetCurrentFilterText(model, model.SelectedItem.Item);
                return helper.IsCommitCharacter(model.SelectedItem.Item, ch, filterText);
            }
320

M
Matt Warren 已提交
321
            return false;
322 323 324 325 326 327 328 329 330 331 332 333 334
        }

        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 已提交
335
            if (model.SelectedItem.IsSuggestionModeItem)
336 337 338 339
            {
                return char.IsLetterOrDigit(ch);
            }

M
Matt Warren 已提交
340 341 342 343 344 345
            var helper = GetCompletionHelper();
            if (helper != null)
            {
                var filterText = GetCurrentFilterText(model, model.SelectedItem.Item);
                return helper.IsFilterCharacter(model.SelectedItem.Item, ch, filterText);
            }
346

M
Matt Warren 已提交
347
            return false;
348 349 350 351 352
        }

        private string GetCurrentFilterText(Model model, CompletionItem selectedItem)
        {
            var textSnapshot = this.TextView.TextSnapshot;
M
Matt Warren 已提交
353
            var viewSpan = model.GetViewBufferSpan(selectedItem.Span);
354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370
            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 已提交
371
            this.Commit(model.SelectedItem, model, ch);
372 373 374
        }
    }
}