diff options
author | Kirill Osenkov <github@osenkov.com> | 2018-02-14 23:54:42 +0300 |
---|---|---|
committer | Kirill Osenkov <github@osenkov.com> | 2018-02-14 23:54:42 +0300 |
commit | 8f4dbae806287ce5ecf847c3178faad2f6b0bcd9 (patch) | |
tree | d8f5918f34a330e7f3d94bcc3e806ab099382c95 /src | |
parent | 4ec55c3dc69faf56a9840c621512523390a2575b (diff) |
Update to VS-Platform source as of 3c4ef6e5fcb2717a52d5ba74d1cd1dfe64d16342.
Diffstat (limited to 'src')
24 files changed, 1354 insertions, 578 deletions
diff --git a/src/Language/Impl/Language/Completion/AsyncCompletionBroker.cs b/src/Language/Impl/Language/Completion/AsyncCompletionBroker.cs index c7c1268..630d1cb 100644 --- a/src/Language/Impl/Language/Completion/AsyncCompletionBroker.cs +++ b/src/Language/Impl/Language/Completion/AsyncCompletionBroker.cs @@ -1,20 +1,16 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; +using System.ComponentModel.Composition; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using Microsoft.VisualStudio.Language.Intellisense; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; -using System.Collections.Immutable; +using Microsoft.VisualStudio.Text.Operations; +using Microsoft.VisualStudio.Text.Utilities; using Microsoft.VisualStudio.Threading; using Microsoft.VisualStudio.Utilities; -using Microsoft.VisualStudio.Text.Utilities; - -#if NET46 -using System.ComponentModel.Composition; -#else -using System.Composition; -#endif namespace Microsoft.VisualStudio.Language.Intellisense.Implementation { @@ -27,27 +23,32 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation [Import(AllowDefault = true)] internal ILoggingServiceInternal Logger { get; set; } + // This may be used to GetExtentOfWord, but it doesn't fully work yet, so we're not using it. [Import(AllowDefault = true)] internal ITextDocumentFactoryService TextDocumentFactoryService { get; set; } [Import] - private IGuardedOperations GuardedOperation { get; set; } + internal IGuardedOperations GuardedOperations { get; set; } [Import] private IContentTypeRegistryService ContentTypeRegistryService { get; set; } + [Import] + internal ITextStructureNavigatorSelectorService TextStructureNavigatorSelectorService { get; set; } + [ImportMany] - private IEnumerable<Lazy<ICompletionUIProvider, IOrderableContentTypeMetadata>> UnorderedUiFactories { get; set; } + private IEnumerable<Lazy<ICompletionPresenterProvider, IOrderableContentTypeMetadata>> UnorderedPresenterProviders { get; set; } + // TODO: use the provider pattern [ImportMany] private IEnumerable<Lazy<IAsyncCompletionItemSource, IOrderableContentTypeMetadata>> UnorderedCompletionItemSources { get; set; } [ImportMany] private IEnumerable<Lazy<IAsyncCompletionService, IOrderableContentTypeMetadata>> UnorderedCompletionServices { get; set; } - private IList<Lazy<ICompletionUIProvider, IOrderableContentTypeMetadata>> _orderedUiFactories; - internal IList<Lazy<ICompletionUIProvider, IOrderableContentTypeMetadata>> OrderedUiFactories - => _orderedUiFactories ?? (_orderedUiFactories = Orderer.Order(UnorderedUiFactories)); + private IList<Lazy<ICompletionPresenterProvider, IOrderableContentTypeMetadata>> _orderedPresenterProviders; + internal IList<Lazy<ICompletionPresenterProvider, IOrderableContentTypeMetadata>> OrderedPresenterProviders + => _orderedPresenterProviders ?? (_orderedPresenterProviders = Orderer.Order(UnorderedPresenterProviders)); private IList<Lazy<IAsyncCompletionItemSource, IOrderableContentTypeMetadata>> _orderedCompletionItemSources; internal IList<Lazy<IAsyncCompletionItemSource, IOrderableContentTypeMetadata>> OrderedCompletionItemSources @@ -57,56 +58,24 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation internal IList<Lazy<IAsyncCompletionService, IOrderableContentTypeMetadata>> OrderedCompletionServices => _orderedCompletionServices ?? (_orderedCompletionServices = Orderer.Order(UnorderedCompletionServices)); - private ImmutableDictionary<IContentType, ImmutableArray<string>> _commitCharacters = ImmutableDictionary<IContentType, ImmutableArray<string>>.Empty; + private ImmutableDictionary<IContentType, ImmutableSortedSet<char>> _commitCharacters = ImmutableDictionary<IContentType, ImmutableSortedSet<char>>.Empty; private ImmutableDictionary<IContentType, ImmutableArray<IAsyncCompletionItemSource>> _cachedCompletionItemSources = ImmutableDictionary<IContentType, ImmutableArray<IAsyncCompletionItemSource>>.Empty; private ImmutableDictionary<IContentType, IAsyncCompletionService> _cachedCompletionServices = ImmutableDictionary<IContentType, IAsyncCompletionService>.Empty; - private ImmutableDictionary<IContentType, ICompletionUIProvider> _cachedUiFactories = ImmutableDictionary<IContentType, ICompletionUIProvider>.Empty; - bool firstRun = true; // used only for diagnostics + private ImmutableDictionary<IContentType, ICompletionPresenterProvider> _cachedUiFactories = ImmutableDictionary<IContentType, ICompletionPresenterProvider>.Empty; + private bool firstRun = true; // used only for diagnostics private bool _firstInvocationReported; // used for "time to code" - void IAsyncCompletionBroker.Dismiss(ITextView view) - { - var session = GetSession(view); - if (session == null) - { - return; - } - view.Properties.RemoveProperty(typeof(IAsyncCompletionSession)); - session.DismissAndHide(); - } - - void IAsyncCompletionBroker.Commit(ITextView view, string edit) - { - var session = GetSession(view); - if (session == null) - { - return; - } - - session.Commit(edit); - ((IAsyncCompletionBroker)this).Dismiss(view); - } - - void IAsyncCompletionBroker.OpenOrUpdate(ITextView view, ITrackingSpan trackedEdit, CompletionTrigger trigger, SnapshotPoint triggerLocation) + internal void DismissSession(IAsyncCompletionSession session) { - var session = GetSession(view); - if (session == null) - { - // Either the session was dismissed, or Begin was not called yet. - // what would be a good way to tell the two apart? - // throw new InvalidOperationException("No completion session available for this view. Call Begin first."); - return; - } - session.OpenOrUpdate(view, trackedEdit, trigger, triggerLocation); + session.TextView.Properties.RemoveProperty(typeof(IAsyncCompletionSession)); } - void IAsyncCompletionBroker.TriggerCompletion(ITextView view, SnapshotPoint triggerLocation) + IAsyncCompletionSession IAsyncCompletionBroker.TriggerCompletion(ITextView view, SnapshotPoint triggerLocation) { var session = GetSession(view); if (session != null) { - // Session already exists. - return; + return session; } // TODO: Handle the race condition: two consecutive OpenAsync. Both create completion @@ -116,7 +85,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation if (!sourcesWithLocations.Any()) { // There is no completion source available for this buffer - return; + return null; } var buffers = CompletionUtilities.GetBuffersForTriggerPoint(view, triggerLocation).ToImmutableArray(); @@ -126,17 +95,17 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation if (service == null) { - // There is no modern completion service available for this buffer - return; + // This should never happen because we provide a default and IsCompletionFeatureAvailable would have returned false + throw new InvalidOperationException("Default completion service was not found. Completion will be unavailable."); } var uiFactory = buffers - .Select(b => GetUiFactory(b.ContentType)) - .FirstOrDefault(s => s != null); + .Select(n => GetUiFactory(n.ContentType)) + .FirstOrDefault(n => n != null); if (firstRun) { - System.Diagnostics.Debug.Assert(uiFactory != null, $"No instance of {nameof(ICompletionUIProvider)} is loaded. The prototype completion will work without the UI."); + System.Diagnostics.Debug.Assert(uiFactory != null, $"No instance of {nameof(ICompletionPresenterProvider)} is loaded. Completion will work without the UI."); firstRun = false; } session = new AsyncCompletionSession(JtContext.Factory, uiFactory, sourcesWithLocations, service, this, view); @@ -144,7 +113,9 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation view.Closed += TextView_Closed; // Additionally, emulate the legacy completion telemetry - emulateLegacyCompletionTelemetry(buffers.FirstOrDefault()?.ContentType, view); + EmulateLegacyCompletionTelemetry(buffers.FirstOrDefault()?.ContentType, view); + + return session; } private ImmutableArray<IAsyncCompletionItemSource> GetCompletionItemSources(IContentType contentType) @@ -154,14 +125,15 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation return cachedSources; } - ImmutableArray<IAsyncCompletionItemSource> result = ImmutableArray<IAsyncCompletionItemSource>.Empty; + var builder = ImmutableArray.CreateBuilder<IAsyncCompletionItemSource>(); foreach (var item in OrderedCompletionItemSources) { if (item.Metadata.ContentTypes.Any(n => contentType.IsOfType(n))) { - result = result.Add(item.Value); + builder.Add(item.Value); } } + var result = builder.ToImmutable(); _cachedCompletionItemSources = _cachedCompletionItemSources.Add(contentType, result); return result; } @@ -173,7 +145,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation return service; } - IAsyncCompletionService bestService = GuardedOperation.InvokeBestMatchingFactory( + IAsyncCompletionService bestService = GuardedOperations.InvokeBestMatchingFactory( providerHandles: OrderedCompletionServices, getter: n => n, dataContentType: contentType, @@ -184,15 +156,15 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation return bestService; } - private ICompletionUIProvider GetUiFactory(IContentType contentType) + private ICompletionPresenterProvider GetUiFactory(IContentType contentType) { if (_cachedUiFactories.TryGetValue(contentType, out var factory)) { return factory; } - ICompletionUIProvider bestFactory = GuardedOperation.InvokeBestMatchingFactory( - providerHandles: OrderedUiFactories, + ICompletionPresenterProvider bestFactory = GuardedOperations.InvokeBestMatchingFactory( + providerHandles: OrderedPresenterProviders, getter: n => n, dataContentType: contentType, contentTypeRegistryService: ContentTypeRegistryService, @@ -202,93 +174,67 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation return bestFactory; } - private void TextView_Closed(object sender, EventArgs e) + internal bool TryGetKnownCommitCharacters(IContentType contentType, out ImmutableSortedSet<char> commitChars) { - ((IAsyncCompletionBroker)this).Dismiss((ITextView)sender); - try + if (_commitCharacters.TryGetValue(contentType, out commitChars)) { - Task.Run(async () => - { - await Task.WhenAll(OrderedCompletionItemSources - .Where(n => n.IsValueCreated) - .Select(n => n.Value) - .Select(n => n.HandleViewClosedAsync((ITextView)sender))); - }); + return commitChars.Any(); } - catch + var allCommitChars = new List<char>(); + foreach (var source in GetCompletionItemSources(contentType)) { - + GuardedOperations.CallExtensionPoint( + errorSource: source, + call: () => allCommitChars.AddRange(source.GetPotentialCommitCharacters()) + ); } + commitChars = ImmutableSortedSet.CreateRange(allCommitChars); + _commitCharacters = _commitCharacters.Add(contentType, commitChars); + return commitChars.Any(); } - bool IAsyncCompletionBroker.IsCompletionActive(ITextView view) - { - return view.Properties.ContainsProperty(typeof(IAsyncCompletionSession)); - } - - bool IAsyncCompletionBroker.ShouldCommitCompletion(ITextView view, string edit, SnapshotPoint triggerLocation) + private void TextView_Closed(object sender, EventArgs e) { - if (!((IAsyncCompletionBroker)this).IsCompletionActive(view)) + GetSession((ITextView)sender).Dismiss(); + // TODO: unlink this + Task.Run(async () => { - return false; - } - - foreach (var contentType in CompletionUtilities.GetBuffersForTriggerPoint(view, triggerLocation).Select(b => b.ContentType)) - { - if (TryGetKnownCommitCharacters(contentType, out var commitChars)) - { - if (commitChars.Contains(edit[0].ToString())) - { - if (GetSession(view).ShouldCommit(view, edit, triggerLocation)) - return true; - } - } - } - return false; + await Task.WhenAll(OrderedCompletionItemSources + .Where(n => n.IsValueCreated) + .Select(n => n.Value) + .Select(n => GuardedOperations.CallExtensionPointAsync(n, () => n.HandleViewClosedAsync((ITextView)sender)))); + }); } - private bool TryGetKnownCommitCharacters(IContentType contentType, out ImmutableArray<string> commitChars) + bool IAsyncCompletionBroker.IsCompletionActive(ITextView view) { - if (_commitCharacters.TryGetValue(contentType, out commitChars)) - { - return commitChars.Any(); - } - var commitCharsBuilder = ImmutableArray.CreateBuilder<string>(); - foreach (var source in GetCompletionItemSources(contentType)) - { - commitCharsBuilder.AddRange(source.GetPotentialCommitCharacters()); - } - commitChars = commitCharsBuilder.Distinct().ToImmutableArray(); - _commitCharacters = _commitCharacters.Add(contentType, commitChars); - return commitChars.Any(); + return view.Properties.ContainsProperty(typeof(IAsyncCompletionSession)); } - bool IAsyncCompletionBroker.ShouldTriggerCompletion(ITextView view, string edit, SnapshotPoint triggerLocation) + bool IAsyncCompletionBroker.ShouldTriggerCompletion(ITextView textView, char typeChar, SnapshotPoint triggerLocation) { - var sourcesWithLocations = CompletionUtilities.GetCompletionSourcesWithMappedLocations(view, triggerLocation, GetCompletionItemSources); - return sourcesWithLocations.Any(p => p.Key.ShouldTriggerCompletion(edit, p.Value)); + var sourcesWithLocations = CompletionUtilities.GetCompletionSourcesWithMappedLocations(textView, triggerLocation, GetCompletionItemSources); + return sourcesWithLocations.Any(p => GuardedOperations.CallExtensionPoint( + errorSource: p.Key, + call: () => p.Key.ShouldTriggerCompletion(typeChar, p.Value), + valueOnThrow: false)); } - private IAsyncCompletionSession GetSession(ITextView view) + public IAsyncCompletionSession GetSession(ITextView textView) { - if (view.Properties.TryGetProperty(typeof(IAsyncCompletionSession), out IAsyncCompletionSession property)) + if (textView.Properties.TryGetProperty(typeof(IAsyncCompletionSession), out IAsyncCompletionSession session)) { - return property; + return session; } return null; } - void IAsyncCompletionBroker.SelectUp(ITextView view) => GetSession(view)?.SelectUp(); - void IAsyncCompletionBroker.SelectDown(ITextView view) => GetSession(view)?.SelectDown(); - void IAsyncCompletionBroker.SelectPageUp(ITextView view) => GetSession(view)?.SelectPageUp(); - void IAsyncCompletionBroker.SelectPageDown(ITextView view) => GetSession(view)?.SelectPageDown(); - // Helper methods for telemetry internal string GetItemSourceName(IAsyncCompletionItemSource source) => OrderedCompletionItemSources.FirstOrDefault(n => n.Value == source)?.Metadata.Name ?? string.Empty; internal string GetCompletionServiceName(IAsyncCompletionService service) => OrderedCompletionServices.FirstOrDefault(n => n.Value == service)?.Metadata.Name ?? string.Empty; - // Partiy with legacy telemetry - private void emulateLegacyCompletionTelemetry(IContentType contentType, ITextView view) + // Parity with legacy telemetry + private void EmulateLegacyCompletionTelemetry(IContentType contentType, ITextView textView) { if (Logger == null || _firstInvocationReported) return; @@ -307,7 +253,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation } return null; } - var fileExtension = GetFileExtension(view.TextBuffer) ?? "Unknown"; + var fileExtension = GetFileExtension(textView.TextBuffer) ?? "Unknown"; var reportedContentType = contentType?.ToString() ?? "Unknown"; _firstInvocationReported = true; @@ -315,5 +261,22 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation ("VS.Editor.IntellisenseFirstRun.Opened.ContentType", reportedContentType), ("VS.Editor.IntellisenseFirstRun.Opened.FileExtension", fileExtension)); } + + const string IsCompletionAvailableProperty = "IsCompletionAvailable"; + bool IAsyncCompletionBroker.IsCompletionSupported(ITextView view) + { + bool featureIsAvailable; + if (view.Properties.TryGetProperty(IsCompletionAvailableProperty, out featureIsAvailable)) + { + return featureIsAvailable; + } + + featureIsAvailable = UnorderedCompletionItemSources + .Any(n => n.Metadata.ContentTypes.Any(ct => view.TextBuffer.ContentType.IsOfType(ct))); + featureIsAvailable &= UnorderedCompletionServices + .Any(n => n.Metadata.ContentTypes.Any(ct => view.TextBuffer.ContentType.IsOfType(ct))); + view.Properties.AddProperty(IsCompletionAvailableProperty, featureIsAvailable); + return featureIsAvailable; + } } } diff --git a/src/Language/Impl/Language/Completion/AsyncCompletionSession.cs b/src/Language/Impl/Language/Completion/AsyncCompletionSession.cs index 0bfbdb6..27cf094 100644 --- a/src/Language/Impl/Language/Completion/AsyncCompletionSession.cs +++ b/src/Language/Impl/Language/Completion/AsyncCompletionSession.cs @@ -8,8 +8,10 @@ using System.Threading.Tasks; using Microsoft.VisualStudio.Language.Intellisense; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Operations; using Microsoft.VisualStudio.Text.Utilities; using Microsoft.VisualStudio.Threading; +using Microsoft.VisualStudio.Utilities; namespace Microsoft.VisualStudio.Language.Intellisense.Implementation { @@ -21,16 +23,18 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation { // Available data and services // TODO: consider storing MappingPoint instead of SnapshotPoint - private readonly IDictionary<IAsyncCompletionItemSource, SnapshotPoint> _completionProviders; + private readonly IDictionary<IAsyncCompletionItemSource, SnapshotPoint> _completionSources; private readonly IAsyncCompletionService _completionService; private readonly JoinableTaskFactory _jtf; - private readonly ICompletionUIProvider _uiFactory; - private readonly IAsyncCompletionBroker _broker; + private readonly ICompletionPresenterProvider _uiFactory; + private readonly AsyncCompletionBroker _broker; private readonly ITextView _view; private readonly TelemetryData _telemetryData; + private readonly ITextStructureNavigator _textStructureNavigator; + private readonly IGuardedOperations _guardedOperations; // Presentation: - ICompletionUI _gui; // Must be accessed from GUI thread + ICompletionPresenter _gui; // Must be accessed from GUI thread const int FirstIndex = 0; readonly int PageStepSize; @@ -44,122 +48,236 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation // When set, we won't send dismissed telemetry private bool _committed; - public AsyncCompletionSession(JoinableTaskFactory jtf, ICompletionUIProvider uiFactory, IDictionary<IAsyncCompletionItemSource, SnapshotPoint> providers, IAsyncCompletionService service, AsyncCompletionBroker broker, ITextView view) + public event EventHandler<CompletionItemEventArgs> ItemCommitted; + public event EventHandler Dismissed; + public event EventHandler<CompletionItemsWithHighlightEventArgs> ItemsUpdated; + + public ITextView TextView => _view; + + public AsyncCompletionSession(JoinableTaskFactory jtf, ICompletionPresenterProvider uiFactory, + IDictionary<IAsyncCompletionItemSource, SnapshotPoint> completionSources, + IAsyncCompletionService completionService, AsyncCompletionBroker broker, ITextView view) { _jtf = jtf; _uiFactory = uiFactory; _broker = broker; - _completionProviders = providers; - _completionService = service; + _completionSources = completionSources; + _completionService = completionService; _view = view; + _textStructureNavigator = broker.TextStructureNavigatorSelectorService?.GetTextStructureNavigator(view.TextBuffer); + _guardedOperations = broker.GuardedOperations; _telemetryData = new TelemetryData(broker); PageStepSize = uiFactory?.ResultsPerPage ?? 1; _view.Caret.PositionChanged += OnCaretPositionChanged; } - public void Commit(string edit) + bool IAsyncCompletionSession.CommitIfUnique(CancellationToken token) + { + // Note that this will deadlock if OpenOrUpdate wasn't called ahead of time. + var lastModel = _computation.WaitAndGetResult(); + if (lastModel.UniqueItem != null) + { + Commit(default(char), lastModel.UniqueItem, token); + return true; + } + else if (lastModel.PresentedItems.Length == 1) + { + Commit(default(char), lastModel.PresentedItems[0].CompletionItem, token); + return true; + } + else + { + // Show the UI, because waitAndGetResult canceled showing the UI. + UpdateUiInner(lastModel); // We are on the UI thread, so we may call UpdateUiInner + return false; + } + } + + void IAsyncCompletionSession.Commit(CancellationToken token, char typedChar) { var lastModel = _computation.WaitAndGetResult(); - if (lastModel.UseSuggestionMode && !String.IsNullOrEmpty(edit)) + if (lastModel.SelectSuggestionMode && !typedChar.Equals(default(char))) return; // In suggestion mode, allow only explicit commits (click, tab, e.g. not tied to a text change) - else if (lastModel.SelectSuggestionMode && lastModel.SuggestionIsEmpty) + else if (lastModel.SelectSuggestionMode && string.IsNullOrWhiteSpace(lastModel.SuggestionModeItem?.InsertText)) return; // In suggestion mode, don't commit empty suggestion (suggestion item temporarily shows description of suggestion mode) - else if (lastModel.SelectSuggestionMode && !lastModel.SuggestionIsEmpty) - Commit(edit, lastModel.SuggestionModeItem.CompletionItem); + else if (lastModel.SelectSuggestionMode) + Commit(typedChar, lastModel.SuggestionModeItem, token); else if (!lastModel.PresentedItems.Any()) return; // There is nothing to commit else - Commit(edit, lastModel.PresentedItems.ElementAt(lastModel.SelectedIndex).CompletionItem); + Commit(typedChar, lastModel.PresentedItems[lastModel.SelectedIndex].CompletionItem, token); } - public void Commit(string edit, CompletionItem itemToCommit) + private void Commit(char typedChar, CompletionItem itemToCommit, CancellationToken token) { var lastModel = _computation.WaitAndGetResult(); - // TODO: ensure we are on the UI thread + if (!_jtf.Context.IsOnMainThread) + throw new InvalidOperationException($"{nameof(IAsyncCompletionSession)}.{nameof(IAsyncCompletionSession.Commit)} must be callled from UI thread."); _telemetryData.UiStopwatch.Restart(); - if (itemToCommit.CustomCommit) + if (itemToCommit.UseCustomCommit) { // Pass appropriate buffer to the item's provider - var buffer = _completionProviders[itemToCommit.Source].Snapshot.TextBuffer; - itemToCommit.Source.CustomCommit(_view, buffer, itemToCommit, lastModel.ApplicableSpan, edit); + var buffer = _completionSources[itemToCommit.Source].Snapshot.TextBuffer; + _guardedOperations.CallExtensionPoint( + errorSource: itemToCommit.Source, + call: () => itemToCommit.Source.CustomCommit(_view, buffer, itemToCommit, lastModel.ApplicableSpan, typedChar, token)); } else { - var buffer = _view.TextBuffer; - var bufferEdit = buffer.CreateEdit(); - bufferEdit.Replace(lastModel.ApplicableSpan.GetSpan(buffer.CurrentSnapshot), itemToCommit.InsertText + edit); - bufferEdit.Apply(); + InsertIntoBuffer(_view, lastModel, itemToCommit.InsertText, typedChar); } _telemetryData.UiStopwatch.Stop(); - _telemetryData.RecordCommitAndSend(_telemetryData.UiStopwatch.ElapsedMilliseconds, itemToCommit, edit); + _telemetryData.RecordCommitAndSend(_telemetryData.UiStopwatch.ElapsedMilliseconds, itemToCommit, typedChar); _committed = true; + ItemCommitted?.Invoke(this, new CompletionItemEventArgs(itemToCommit)); + ((IAsyncCompletionSession)this).Dismiss(); } - void IAsyncCompletionSession.DismissAndHide() + private void InsertIntoBuffer(ITextView view, CompletionModel model, string insertText, char typeChar) { - // TODO: protect from race conditions when we get two Dismiss requests + var buffer = view.TextBuffer; + var bufferEdit = buffer.CreateEdit(); + var textToInsert = typeChar.Equals(default(char)) ? insertText : insertText + typeChar; + bufferEdit.Replace(model.ApplicableSpan.GetSpan(buffer.CurrentSnapshot), textToInsert); + bufferEdit.Apply(); + } + + void IAsyncCompletionSession.Dismiss() + { + if (_isDismissed) + return; + _isDismissed = true; + _broker.DismissSession(this); + Dismissed?.Invoke(this, EventArgs.Empty); _view.Caret.PositionChanged -= OnCaretPositionChanged; _computationCancellation.Cancel(); + _computation = null; if (!_committed) _telemetryData.RecordDismissedAndSend(); - if (_gui == null) - return; // nothing to hide - - // TODO: ensure we are on the UI thread - _gui.FiltersChanged -= OnFiltersChanged; - _gui.CompletionItemCommitted -= OnItemCommitted; - _gui.Hide(); - _gui.Dispose(); + if (_gui != null) + { + _guardedOperations.CallExtensionPoint( + errorSource: _gui, + call: async () => + { + await _jtf.SwitchToMainThreadAsync(); + _gui.FiltersChanged -= OnFiltersChanged; + _gui.CommitRequested -= OnCommitRequested; + _gui.CompletionItemSelected -= OnItemSelected; + _gui.CompletionClosed -= OnGuiClosed; + _gui.Close(); + }); + _gui = null; + } } - public void OpenOrUpdate(ITextView view, ITrackingSpan trackedEdit, CompletionTrigger trigger, SnapshotPoint triggerLocation) + void IAsyncCompletionSession.OpenOrUpdate(ITextView textView, CompletionTrigger trigger, SnapshotPoint triggerLocation) { if (_computation == null) { _computation = new ModelComputation<CompletionModel>(PrioritizedTaskScheduler.AboveNormalInstance, _computationCancellation.Token); - _computation.Enqueue((model, token) => GetInitialModel(view, trackedEdit, trigger, triggerLocation, token)); + _computation.Enqueue((model, token) => GetInitialModel(textView, trigger, triggerLocation, token)); } var taskId = Interlocked.Increment(ref _lastFilteringTaskId); - _computation.Enqueue((model, token) => UpdateSnapshot(model, trigger, triggerLocation, token, taskId), UpdateUI); + _computation.Enqueue((model, token) => UpdateSnapshot(model, trigger, FromCompletionTriggerReason(trigger.Reason), triggerLocation, token, taskId), UpdateUi); + } + + internal void InvokeAndCommitIfUnique(ITextView view, CompletionTrigger trigger, SnapshotPoint triggerLocation, CancellationToken token) + { + ((IAsyncCompletionSession)this).OpenOrUpdate(view, trigger, triggerLocation); + if (((IAsyncCompletionSession)this).CommitIfUnique(token)) + { + ((IAsyncCompletionSession)this).Dismiss(); + } } - public void SelectDown() + private static CompletionFilterReason FromCompletionTriggerReason(CompletionTriggerReason reason) { - _computation.Enqueue((model, token) => UpdateSelectedItem(model, +1, token), UpdateUI); + switch (reason) + { + case CompletionTriggerReason.Invoke: + case CompletionTriggerReason.InvokeAndCommitIfUnique: + return CompletionFilterReason.Initial; + case CompletionTriggerReason.Insertion: + return CompletionFilterReason.Insertion; + case CompletionTriggerReason.Deletion: + return CompletionFilterReason.Deletion; + default: + throw new ArgumentOutOfRangeException(nameof(reason)); + } } - public void SelectPageDown() + #region Internal methods accessed by the command handlers + + internal void ToggleSuggestionMode() { - _computation.Enqueue((model, token) => UpdateSelectedItem(model, +PageStepSize, token), UpdateUI); + _computation.Enqueue((model, token) => ToggleCompletionModeInner(model, token), UpdateUi); } - public void SelectUp() + internal void SelectDown() { - _computation.Enqueue((model, token) => UpdateSelectedItem(model, -1, token), UpdateUI); + _computation.Enqueue((model, token) => UpdateSelectedItem(model, +1, token), UpdateUi); } - public void SelectPageUp() + internal void SelectPageDown() { - _computation.Enqueue((model, token) => UpdateSelectedItem(model, -PageStepSize, token), UpdateUI); + _computation.Enqueue((model, token) => UpdateSelectedItem(model, +PageStepSize, token), UpdateUi); } - private void OnFiltersChanged(object sender, CompletionFilterChangedEventArgs e) + internal void SelectUp() + { + _computation.Enqueue((model, token) => UpdateSelectedItem(model, -1, token), UpdateUi); + } + + internal void SelectPageUp() + { + _computation.Enqueue((model, token) => UpdateSelectedItem(model, -PageStepSize, token), UpdateUi); + } + + #endregion + + private void OnFiltersChanged(object sender, CompletionFilterChangedEventArgs args) { var taskId = Interlocked.Increment(ref _lastFilteringTaskId); - _computation.Enqueue((model, token) => UpdateFilters(model, e.Filters, token, taskId), UpdateUI); + _computation.Enqueue((model, token) => UpdateFilters(model, args.Filters, token, taskId), UpdateUi); } - private void OnItemCommitted(object sender, CompletionItemCommittedEventArgs e) + private void OnCommitRequested(object sender, CompletionItemEventArgs args) { - Commit(String.Empty, e.Item); - _broker.Dismiss(_view); + Commit(default(char), args.Item, default(CancellationToken)); + } + + private void OnItemSelected(object sender, CompletionItemSelectedEventArgs args) + { + _computation.Enqueue((model, token) => UpdateSelectedItem(model, args.SelectedItem, args.SuggestionModeSelected, token)); // Note: we do not dispatch the call to update UI afterwards. + } + + private void OnGuiClosed(object sender, CompletionClosedEventArgs args) + { + ((IAsyncCompletionSession)this).Dismiss(); + } + + bool IAsyncCompletionSession.ShouldCommit(ITextView view, char typeChar, SnapshotPoint triggerLocation) + { + foreach (var contentType in CompletionUtilities.GetBuffersForTriggerPoint(view, triggerLocation).Select(b => b.ContentType)) + { + if (_broker.TryGetKnownCommitCharacters(contentType, out var commitChars)) + { + if (commitChars.Contains(typeChar)) + { + if (ShouldCommit(view, typeChar, triggerLocation)) + return true; + } + } + } + return false; } /// <summary> @@ -171,60 +289,83 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation { // http://source.roslyn.io/#Microsoft.CodeAnalysis.EditorFeatures/Implementation/IntelliSense/Completion/Controller_CaretPositionChanged.cs,40 // enqueue a task to see if the caret is still within the broundary - _computation.Enqueue((model, token) => HandleCaretPositionChanged(model, e.NewPosition), UpdateUI); + _computation.Enqueue((model, token) => HandleCaretPositionChanged(model, e.NewPosition), UpdateUi); } - private async Task UpdateUI(CompletionModel model) + private async Task UpdateUi(CompletionModel model) { if (_uiFactory == null) return; await _jtf.SwitchToMainThreadAsync(); + UpdateUiInner(model); + await TaskScheduler.Default; + } + private void UpdateUiInner(CompletionModel model) + { if (_isDismissed) return; _telemetryData.UiStopwatch.Restart(); if (_gui == null) { - _gui = _uiFactory.GetUI(_view); - _gui.Open(new CompletionPresentation(model.PresentedItems, model.Filters, model.ApplicableSpan, model.UseSoftSelection, model.UseSuggestionMode, model.SelectSuggestionMode, model.SelectedIndex, model.SuggestionModeItem)); - _gui.FiltersChanged += OnFiltersChanged; - _gui.CompletionItemCommitted += OnItemCommitted; + _gui = _guardedOperations.CallExtensionPoint(errorSource: _uiFactory, call: () => _uiFactory.GetOrCreate(_view), valueOnThrow: null); + if (_gui != null) + { + _guardedOperations.CallExtensionPoint( + errorSource: _gui, + call: () => + { + _gui = _uiFactory.GetOrCreate(_view); + _gui.Open(new CompletionPresentationViewModel(model.PresentedItems, model.Filters, model.ApplicableSpan, model.UseSoftSelection, + model.DisplaySuggestionMode, model.SelectSuggestionMode, model.SelectedIndex, model.SuggestionModeItem, model.SuggestionModeDescription)); + _gui.FiltersChanged += OnFiltersChanged; + _gui.CommitRequested += OnCommitRequested; + _gui.CompletionItemSelected += OnItemSelected; + _gui.CompletionClosed += OnGuiClosed; + }); + } } else { - _gui.Update(new CompletionPresentation(model.PresentedItems, model.Filters, model.ApplicableSpan, model.UseSoftSelection, model.UseSuggestionMode, model.SelectSuggestionMode, model.SelectedIndex, model.SuggestionModeItem)); + _guardedOperations.CallExtensionPoint( + errorSource: _gui, + call: () => _gui.Update(new CompletionPresentationViewModel(model.PresentedItems, model.Filters, model.ApplicableSpan, model.UseSoftSelection, + model.DisplaySuggestionMode, model.SelectSuggestionMode, model.SelectedIndex, model.SuggestionModeItem, model.SuggestionModeDescription))); } _telemetryData.UiStopwatch.Stop(); _telemetryData.RecordRendering(_telemetryData.UiStopwatch.ElapsedMilliseconds); - - await TaskScheduler.Default; } /// <summary> /// Creates a new model and populates it with initial data /// </summary> - private async Task<CompletionModel> GetInitialModel(ITextView view, ITrackingSpan trackedEdit, CompletionTrigger trigger, SnapshotPoint triggerLocation, CancellationToken token) + private async Task<CompletionModel> GetInitialModel(ITextView textView, CompletionTrigger trigger, SnapshotPoint triggerLocation, CancellationToken token) { _telemetryData.ComputationStopwatch.Restart(); // Map the trigger location to respective view for each completion provider - var nestedResults = await Task.WhenAll(_completionProviders.Select(p => p.Key.GetCompletionContextAsync(trigger, p.Value))); - var originalCompletionItems = nestedResults.SelectMany(n => n.Items).ToImmutableArray(); - - var availableFilters = nestedResults - .SelectMany(n => n.Items) + var nestedResults = await Task.WhenAll( + _completionSources.Select( + p => _guardedOperations.CallExtensionPointAsync<CompletionContext>( + errorSource: p.Key, + asyncCall: () => p.Key.GetCompletionContextAsync(trigger, p.Value, token), + valueOnThrow: null + ))); + var initialCompletionItems = nestedResults.Where(n => n != null && !n.Items.IsDefaultOrEmpty).SelectMany(n => n.Items).ToImmutableArray(); + + var availableFilters = initialCompletionItems .SelectMany(n => n.Filters) .Distinct() .Select(n => new CompletionFilterWithState(n, true)) .ToImmutableArray(); - // Note: do not use the tracked edit from the editor. Exclusively rely on data from the completion providers var spans = nestedResults .Select(n => n.ApplicableToSpan) - .Select(s => view.BufferGraph.CreateMappingSpan(s, SpanTrackingMode.EdgeNegative)) + .Select(s => textView.BufferGraph.CreateMappingSpan(s, SpanTrackingMode.EdgeNegative)) .Select(s => new SnapshotSpan( s.Start.GetPoint(triggerLocation.Snapshot, PositionAffinity.Predecessor).Value, s.End.GetPoint(triggerLocation.Snapshot, PositionAffinity.Predecessor).Value)); + //var extentFromStructureNavigator = _textStructureNavigator?.GetExtentOfWord(triggerLocation - 1).Span; var extent = new SnapshotSpan( spans.Min(n => n.Start), spans.Max(n => n.End)); @@ -232,32 +373,49 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation var useSoftSelection = nestedResults.Any(n => n.UseSoftSelection); var useSuggestionMode = nestedResults.Any(n => n.UseSuggestionMode); - var suggestionModeDescription = nestedResults.FirstOrDefault(n => !String.IsNullOrEmpty(n.SuggestionModeDescription)).SuggestionModeDescription ?? String.Empty; + var suggestionModeDescription = nestedResults.FirstOrDefault(n => !string.IsNullOrEmpty(n.SuggestionModeDescription))?.SuggestionModeDescription ?? string.Empty; + + _telemetryData.ComputationStopwatch.Stop(); + _telemetryData.RecordInitialModel(_telemetryData.ComputationStopwatch.ElapsedMilliseconds, _completionSources.Keys, initialCompletionItems.Length, _completionService); + _telemetryData.ComputationStopwatch.Restart(); + var sortedList = await _guardedOperations.CallExtensionPointAsync( + errorSource: _completionService, + asyncCall: () => _completionService.SortCompletionListAsync(initialCompletionItems, trigger.Reason, triggerLocation.Snapshot, applicableSpan, _view, token), + valueOnThrow: initialCompletionItems); _telemetryData.ComputationStopwatch.Stop(); - _telemetryData.RecordInitialModel(_telemetryData.ComputationStopwatch.ElapsedMilliseconds, _completionProviders.Keys, originalCompletionItems.Length, _completionService); + _telemetryData.RecordProcessing(_telemetryData.ComputationStopwatch.ElapsedMilliseconds, initialCompletionItems.Length); - return new CompletionModel(originalCompletionItems, applicableSpan, triggerLocation.Snapshot, availableFilters, useSoftSelection, useSuggestionMode, suggestionModeDescription); + return new CompletionModel(initialCompletionItems, sortedList, applicableSpan, trigger.Reason, triggerLocation.Snapshot, + availableFilters, useSoftSelection, useSuggestionMode, suggestionModeDescription, suggestionModeItem: null); } /// <summary> /// User has moved the caret. Ensure that the caret is still within the applicable span. If not, dismiss the session. /// </summary> /// <returns></returns> - private async Task<CompletionModel> HandleCaretPositionChanged(CompletionModel model, CaretPosition caretPosition) + private Task<CompletionModel> HandleCaretPositionChanged(CompletionModel model, CaretPosition caretPosition) { if (!(model.ApplicableSpan.GetSpan(caretPosition.VirtualBufferPosition.Position.Snapshot).IntersectsWith(new SnapshotSpan(caretPosition.VirtualBufferPosition.Position, 0)))) { - await _jtf.SwitchToMainThreadAsync(); - _broker.Dismiss(_view); + ((IAsyncCompletionSession)this).Dismiss(); } - return model; + return Task.FromResult(model); + } + + /// <summary> + /// User has moved the caret. Ensure that the caret is still within the applicable span. If not, dismiss the session. + /// </summary> + /// <returns></returns> + private Task<CompletionModel> ToggleCompletionModeInner(CompletionModel model, CancellationToken token) + { + return Task.FromResult(model.WithSuggestionModeActive(!model.DisplaySuggestionMode)); } /// <summary> /// User has typed /// </summary> - private async Task<CompletionModel> UpdateSnapshot(CompletionModel model, CompletionTrigger trigger, SnapshotPoint triggerLocation, CancellationToken token, int thisId) + private async Task<CompletionModel> UpdateSnapshot(CompletionModel model, CompletionTrigger trigger, CompletionFilterReason filterReason, SnapshotPoint triggerLocation, CancellationToken token, int thisId) { // Filtering got preempted, so store the most recent snapshot for the next time we filter if (token.IsCancellationRequested || thisId != _lastFilteringTaskId) @@ -267,68 +425,76 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation // TODO: dismiss if applicable span was reduced to zero AND moved again (e.g. first backspace keeps completion open, second backspace closes it) if (triggerLocation < model.ApplicableSpan.GetStartPoint(triggerLocation.Snapshot) || triggerLocation > model.ApplicableSpan.GetEndPoint(triggerLocation.Snapshot)) { - await _jtf.SwitchToMainThreadAsync(); - _broker.Dismiss(_view); // We need to dismiss through the broker, so that it updates its state. + ((IAsyncCompletionSession)this).Dismiss(); return model; } _telemetryData.ComputationStopwatch.Restart(); var filteredCompletion = await _completionService.UpdateCompletionListAsync( - model.AllItems, - trigger, + model.InitialItems, + model.InitialTriggerReason, + filterReason, triggerLocation.Snapshot, model.ApplicableSpan, - model.Filters); + model.Filters, + _view, + token); - if (model.UseSuggestionMode) - { - var filterText = model.ApplicableSpan.GetText(triggerLocation.Snapshot); - CompletionItemWithHighlight suggestionModeItem; - if (String.IsNullOrWhiteSpace(filterText)) - { - suggestionModeItem = new CompletionItemWithHighlight(new CompletionItem(model.SuggestionModeDescription, null), ImmutableArray<Span>.Empty); - model = model.WithEmptySuggestionItem(suggestionModeItem); - } - else - { - suggestionModeItem = new CompletionItemWithHighlight(new CompletionItem(filterText, null), ImmutableArray.Create<Span>(new Span(0, filterText.Length))); - model = model.WithSuggestionItem(suggestionModeItem); - } - } + // Prevent null references when service returns default(ImmutableArray) + ImmutableArray<CompletionItemWithHighlight> returnedItems = filteredCompletion.Items.IsDefault + ? ImmutableArray<CompletionItemWithHighlight>.Empty + : filteredCompletion.Items; _telemetryData.ComputationStopwatch.Stop(); - _telemetryData.RecordProcessing(_telemetryData.ComputationStopwatch.ElapsedMilliseconds, filteredCompletion.Items.Count()); + _telemetryData.RecordProcessing(_telemetryData.ComputationStopwatch.ElapsedMilliseconds, returnedItems.Length); - // TODO: if filtered completion has no items to display, - // reuse previous filtered completion, but with soft selection - return model.WithSnapshot(triggerLocation.Snapshot).WithPresentedItems(filteredCompletion.Items, filteredCompletion.SelectedItemIndex); + if (filteredCompletion.SelectionMode == CompletionItemSelection.SoftSelected) + model = model.WithSoftSelection(true); + else if (filteredCompletion.SelectionMode == CompletionItemSelection.Selected) + model = model.WithSoftSelection(false); + + // Prepare the suggestionModeItem if we ever change the mode + var enteredText = model.ApplicableSpan.GetText(triggerLocation.Snapshot); + if (string.IsNullOrWhiteSpace(enteredText)) + enteredText = "_"; // TODO: change CompletionItem to allow for empty display text and remove this code. + var suggestionModeItem = new CompletionItem(enteredText, SuggestionModeCompletionItemSource.Instance); + + // TODO: guarded operations + ItemsUpdated?.Invoke(this, new CompletionItemsWithHighlightEventArgs(returnedItems)); + + // TODO: combine this chain into a single method: + return model.WithSnapshot(triggerLocation.Snapshot).WithUniqueItem(filteredCompletion.UniqueItem) + .WithSuggestionModeItem(suggestionModeItem).WithPresentedItems(returnedItems, filteredCompletion.SelectedItemIndex); } /// <summary> - /// User has changed a filter + /// Reacts to user toggling a filter /// </summary> - private async Task<CompletionModel> UpdateFilters(CompletionModel model, ImmutableArray<CompletionFilterWithState> filters, CancellationToken token, int thisId) + private async Task<CompletionModel> UpdateFilters(CompletionModel model, ImmutableArray<CompletionFilterWithState> newFilters, CancellationToken token, int thisId) { _telemetryData.RecordChangingFilters(); // Filtering got preempted, so store the most updated filters for the next time we filter if (token.IsCancellationRequested || thisId != _lastFilteringTaskId) - return model.WithFilters(filters); + return model.WithFilters(newFilters); var filteredCompletion = await _completionService.UpdateCompletionListAsync( - model.AllItems, - new CompletionTrigger(CompletionTriggerReason.FilterChange), + model.InitialItems, + model.InitialTriggerReason, + CompletionFilterReason.FilterChange, model.Snapshot, model.ApplicableSpan, - filters); + newFilters, + _view, + token); - return model.WithFilters(filters).WithPresentedItems(filteredCompletion.Items, filteredCompletion.SelectedItemIndex); + return model.WithFilters(newFilters).WithPresentedItems(filteredCompletion.Items, filteredCompletion.SelectedItemIndex); } #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously /// <summary> - /// User is scrolling the list + /// Reacts to user scrolling the list /// </summary> private async Task<CompletionModel> UpdateSelectedItem(CompletionModel model, int offset, CancellationToken token) #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously @@ -338,7 +504,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation if (!model.PresentedItems.Any()) { // No-op if there are no items - if (model.UseSuggestionMode) + if (model.DisplaySuggestionMode) { // Unless there is a suggestion mode item. Select it. return model.WithSuggestionItemSelected(); @@ -353,7 +519,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation { if (currentIndex == lastIndex) { - if (model.UseSuggestionMode) + if (model.DisplaySuggestionMode) return model.WithSuggestionItemSelected(); else return model.WithSelectedIndex(FirstIndex); @@ -371,7 +537,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation else if (currentIndex == FirstIndex) { // The first item is selected. If there is a suggestion, select it. - if (model.UseSuggestionMode) + if (model.DisplaySuggestionMode) return model.WithSuggestionItemSelected(); else return model.WithSelectedIndex(lastIndex); @@ -381,6 +547,31 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation } } +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + /// <summary> + /// Reacts to user selecting a specific item in the list + /// </summary> + private async Task<CompletionModel> UpdateSelectedItem(CompletionModel model, CompletionItem selectedItem, bool suggestionModeSelected, CancellationToken token) +#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously + { + if (suggestionModeSelected) + { + return model.WithSuggestionItemSelected(); + } + else + { + for (int i = 0; i < model.PresentedItems.Length; i++) + { + if (model.PresentedItems[i].CompletionItem == selectedItem) + { + return model.WithSelectedIndex(i); + } + } + // This item is not in the model + return model; + } + } + /// <summary> /// This method creates a mapping point from the top-buffer trigger location /// then iterates through completion item sources pertinent to this session. @@ -388,19 +579,26 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation /// Ensures that we work only with sources applicable to current location /// and returns whether any source would like to commit completion /// </summary> - public bool ShouldCommit(ITextView view, string edit, SnapshotPoint triggerLocation) + public bool ShouldCommit(ITextView view, char typeChar, SnapshotPoint triggerLocation) { var mappingPoint = view.BufferGraph.CreateMappingPoint(triggerLocation, PointTrackingMode.Negative); - return _completionProviders + return _completionSources .Select(p => (p, mappingPoint.GetPoint(p.Value.Snapshot.TextBuffer, PositionAffinity.Predecessor))) .Where(n => n.Item2.HasValue) - .Any(n => n.Item1.Key.ShouldCommitCompletion(edit, n.Item2.Value)); - // Remove previous line and uncomment the following lines to get the specific IAsyncCompletionItemSource that wanted to commit - /* - .Where(n => n.Item1.Key.ShouldCommitCompletion(edit, n.Item2.Value)) - .Select(n => n.Item1.Key) - .FirstOrDefault(); - */ + .Any(n => _guardedOperations.CallExtensionPoint( + errorSource: n.Item1.Key, + call: () => n.Item1.Key.ShouldCommitCompletion(typeChar, n.Item2.Value), + valueOnThrow: false)); + } + + public ImmutableArray<CompletionItem> GetVisibleItems(CancellationToken token) + { + throw new NotImplementedException(); + } + + public CompletionItem GetSelectedItem(CancellationToken token) + { + throw new NotImplementedException(); } private class TelemetryData @@ -473,7 +671,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation internal void RecordInitialModel(long processingTime, ICollection<IAsyncCompletionItemSource> sources, int initialItemCount, IAsyncCompletionService service) { InitialItemCount = initialItemCount; - InitialItemSources = String.Join(" ", sources.Select(n => _broker.GetItemSourceName(n))); + InitialItemSources = string.Join(" ", sources.Select(n => _broker.GetItemSourceName(n))); InitialModelDuration = processingTime; CompletionService = _broker.GetCompletionServiceName(service); // Service does not participate in getting the initial model, but this is a good place to get this data } @@ -505,7 +703,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation UserEverSetFilters = true; } - internal void RecordCommitAndSend(long commitDuration, CompletionItem committedItem, string edit) + internal void RecordCommitAndSend(long commitDuration, CompletionItem committedItem, char typeChar) { _logger?.PostEvent(TelemetryEventType.Operation, TelemetryData.CommitEvent, @@ -513,7 +711,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation (InitialItemSourcesProperty, InitialItemSources ?? string.Empty), (InitialItemCountProperty, InitialItemCount), (InitialModelDurationProperty, InitialModelDuration), - (CompletionServiceProperty, CompletionService), + (CompletionServiceProperty, CompletionService ?? string.Empty), (TotalProcessingDurationProperty, TotalProcessingDuration), (TotalProcessingCountProperty, TotalProcessingCount), (InitialRenderingDurationProperty, InitialRenderingDuration), @@ -523,10 +721,10 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation (UserEverSetFiltersProperty, UserEverSetFilters), (UserLastScrolledProperty, UserLastScrolled), (LastItemCountProperty, LastItemCount), - (CustomCommitProperty, committedItem.CustomCommit), - (CommittedItemSourceProperty, GetItemSourceName(committedItem.Source)), + (CustomCommitProperty, committedItem.UseCustomCommit), + (CommittedItemSourceProperty, GetItemSourceName(committedItem.Source) ?? string.Empty), (CommitDurationProperty, commitDuration), - (CommitTriggerProperty, edit ?? string.Empty) + (CommitTriggerProperty, typeChar) ); } @@ -538,7 +736,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation (InitialItemSourcesProperty, InitialItemSources ?? string.Empty), (InitialItemCountProperty, InitialItemCount), (InitialModelDurationProperty, InitialModelDuration), - (CompletionServiceProperty, CompletionService), + (CompletionServiceProperty, CompletionService ?? string.Empty), (LastItemCountProperty, LastItemCount), (TotalProcessingDurationProperty, TotalProcessingDuration), (TotalProcessingCountProperty, TotalProcessingCount), diff --git a/src/Language/Impl/Language/Completion/CompletionCommandHandlers.cs b/src/Language/Impl/Language/Completion/CompletionCommandHandlers.cs new file mode 100644 index 0000000..a50b6fb --- /dev/null +++ b/src/Language/Impl/Language/Completion/CompletionCommandHandlers.cs @@ -0,0 +1,329 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.VisualStudio.Commanding; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; +using Microsoft.VisualStudio.Text.Utilities; +using Microsoft.VisualStudio.Utilities; + +namespace Microsoft.VisualStudio.Language.Intellisense.Implementation +{ + /// <summary> + /// Reacts to the down arrow command and attempts to scroll the completion list. + /// </summary> + [Name(nameof(CompletionCommandHandlers))] + [ContentType("any")] + [Export(typeof(ICommandHandler))] + internal sealed class CompletionCommandHandlers : + ICommandHandler<DownKeyCommandArgs>, + ICommandHandler<PageDownKeyCommandArgs>, + ICommandHandler<PageUpKeyCommandArgs>, + ICommandHandler<UpKeyCommandArgs>, + IChainedCommandHandler<BackspaceKeyCommandArgs>, + ICommandHandler<EscapeKeyCommandArgs>, + ICommandHandler<InvokeCompletionListCommandArgs>, + ICommandHandler<CommitUniqueCompletionListItemCommandArgs>, + ICommandHandler<InsertSnippetCommandArgs>, + ICommandHandler<ToggleCompletionModeCommandArgs>, + IChainedCommandHandler<DeleteKeyCommandArgs>, + ICommandHandler<WordDeleteToEndCommandArgs>, + ICommandHandler<WordDeleteToStartCommandArgs>, + ICommandHandler<ReturnKeyCommandArgs>, + ICommandHandler<TabKeyCommandArgs>, + IChainedCommandHandler<TypeCharCommandArgs> + { + [Import] + IAsyncCompletionBroker Broker { get; set; } + + [Import] + IExperimentationServiceInternal ExperimentationService { get; set; } + + string INamed.DisplayName => Strings.CompletionCommandHandlerName; + + /// <summary> + /// Helper method that returns command state for commands + /// that are always available - unless the completion feature is available. + /// </summary> + /// <param name="view"></param> + /// <returns></returns> + private CommandState Available(ITextView view) + { + return ModernCompletionFeature.GetFeatureState(ExperimentationService) + && Broker.IsCompletionSupported(view) + ? CommandState.Available + : CommandState.Unspecified; + } + + /// <summary> + /// Helper method that returns command state for commands + /// that are available when completion is active. + /// </summary> + /// <remarks> + /// Completion might be active only if the feature is available, so we're skipping other checks. + /// </remarks> + private CommandState AvailableIfCompletionIsUp(ITextView view) + { + return Broker.IsCompletionActive(view) + ? CommandState.Available + : CommandState.Unspecified; + } + + CommandState IChainedCommandHandler<BackspaceKeyCommandArgs>.GetCommandState(BackspaceKeyCommandArgs args, Func<CommandState> nextCommandHandler) + => AvailableIfCompletionIsUp(args.TextView); + + void IChainedCommandHandler<BackspaceKeyCommandArgs>.ExecuteCommand(BackspaceKeyCommandArgs args, Action nextCommandHandler, CommandExecutionContext executionContext) + { + // Execute other commands in the chain to see the change in the buffer. + nextCommandHandler(); + + // We are only inteterested in the top buffer. Currently, commanding implementation calls us multiple times, once per each buffer. + if (args.TextView.BufferGraph.TopBuffer != args.SubjectBuffer) + return; + + var session = Broker.GetSession(args.TextView); + if (session != null) + { + var trigger = new CompletionTrigger(CompletionTriggerReason.Deletion); + var location = args.TextView.Caret.Position.BufferPosition; + session.OpenOrUpdate(args.TextView, trigger, location); + } + } + + CommandState ICommandHandler<EscapeKeyCommandArgs>.GetCommandState(EscapeKeyCommandArgs args) + => AvailableIfCompletionIsUp(args.TextView); + + bool ICommandHandler<EscapeKeyCommandArgs>.ExecuteCommand(EscapeKeyCommandArgs args, CommandExecutionContext executionContext) + { + var session = Broker.GetSession(args.TextView); + if (session != null) + { + session.Dismiss(); + return true; + } + return false; + } + + CommandState ICommandHandler<InvokeCompletionListCommandArgs>.GetCommandState(InvokeCompletionListCommandArgs args) + => Available(args.TextView); + + bool ICommandHandler<InvokeCompletionListCommandArgs>.ExecuteCommand(InvokeCompletionListCommandArgs args, CommandExecutionContext executionContext) + { + var trigger = new CompletionTrigger(CompletionTriggerReason.Invoke); + var location = args.TextView.Caret.Position.BufferPosition; + var session = Broker.TriggerCompletion(args.TextView, location); + session.OpenOrUpdate(args.TextView, trigger, location); + return true; + } + + CommandState ICommandHandler<CommitUniqueCompletionListItemCommandArgs>.GetCommandState(CommitUniqueCompletionListItemCommandArgs args) + => Available(args.TextView); + + bool ICommandHandler<CommitUniqueCompletionListItemCommandArgs>.ExecuteCommand(CommitUniqueCompletionListItemCommandArgs args, CommandExecutionContext executionContext) + { + var trigger = new CompletionTrigger(CompletionTriggerReason.InvokeAndCommitIfUnique); + var location = args.TextView.Caret.Position.BufferPosition; + var session = Broker.TriggerCompletion(args.TextView, location); + session.OpenOrUpdate(args.TextView, trigger, location); + // TODO: figure out dismissing. who should dismiss? here, OpenOrUpdate dismisses. Else, commit dismisses. + return true; + } + + CommandState ICommandHandler<InsertSnippetCommandArgs>.GetCommandState(InsertSnippetCommandArgs args) + => Available(args.TextView); + + bool ICommandHandler<InsertSnippetCommandArgs>.ExecuteCommand(InsertSnippetCommandArgs args, CommandExecutionContext executionContext) + { + System.Diagnostics.Debug.WriteLine("!!!! InsertSnippetCommandArgs"); + return false; + } + + CommandState ICommandHandler<ToggleCompletionModeCommandArgs>.GetCommandState(ToggleCompletionModeCommandArgs args) + => Available(args.TextView); + + bool ICommandHandler<ToggleCompletionModeCommandArgs>.ExecuteCommand(ToggleCompletionModeCommandArgs args, CommandExecutionContext executionContext) + { + var session = Broker.GetSession(args.TextView) as AsyncCompletionSession; // we are accessing an internal method + if (session != null) + { + session.ToggleSuggestionMode(); + return true; // TODO: See if the toobar button gets updated. + } + return false; + } + + CommandState IChainedCommandHandler<DeleteKeyCommandArgs>.GetCommandState(DeleteKeyCommandArgs args, Func<CommandState> nextCommandHandler) + => AvailableIfCompletionIsUp(args.TextView); + + void IChainedCommandHandler<DeleteKeyCommandArgs>.ExecuteCommand(DeleteKeyCommandArgs args, Action nextCommandHandler, CommandExecutionContext executionContext) + { + // Execute other commands in the chain to see the change in the buffer. + nextCommandHandler(); + + // We are only inteterested in the top buffer. Currently, commanding implementation calls us multiple times, once per each buffer. + if (args.TextView.BufferGraph.TopBuffer != args.SubjectBuffer) + return; + + var session = Broker.GetSession(args.TextView); + if (session != null) + { + var trigger = new CompletionTrigger(CompletionTriggerReason.Deletion); + var location = args.TextView.Caret.Position.BufferPosition; + session.OpenOrUpdate(args.TextView, trigger, location); + } + } + + CommandState ICommandHandler<WordDeleteToEndCommandArgs>.GetCommandState(WordDeleteToEndCommandArgs args) + => AvailableIfCompletionIsUp(args.TextView); + + bool ICommandHandler<WordDeleteToEndCommandArgs>.ExecuteCommand(WordDeleteToEndCommandArgs args, CommandExecutionContext executionContext) + { + var session = Broker.GetSession(args.TextView); + if (session != null) + { + session.Dismiss(); + return false; // return false so that the editor can handle this event + } + return false; + } + + CommandState ICommandHandler<WordDeleteToStartCommandArgs>.GetCommandState(WordDeleteToStartCommandArgs args) + => AvailableIfCompletionIsUp(args.TextView); + + bool ICommandHandler<WordDeleteToStartCommandArgs>.ExecuteCommand(WordDeleteToStartCommandArgs args, CommandExecutionContext executionContext) + { + var session = Broker.GetSession(args.TextView); + if (session != null) + { + session.Dismiss(); + return false; // return false so that the editor can handle this event + } + return false; + } + + CommandState ICommandHandler<ReturnKeyCommandArgs>.GetCommandState(ReturnKeyCommandArgs args) + => AvailableIfCompletionIsUp(args.TextView); + + bool ICommandHandler<ReturnKeyCommandArgs>.ExecuteCommand(ReturnKeyCommandArgs args, CommandExecutionContext executionContext) + { + var session = Broker.GetSession(args.TextView); + if (session != null) + { + session.Commit(executionContext.OperationContext.UserCancellationToken); + session.Dismiss(); // TODO: Currently the implementation needs UI thread + return true; + } + return false; + } + + CommandState ICommandHandler<TabKeyCommandArgs>.GetCommandState(TabKeyCommandArgs args) + => AvailableIfCompletionIsUp(args.TextView); + + bool ICommandHandler<TabKeyCommandArgs>.ExecuteCommand(TabKeyCommandArgs args, CommandExecutionContext executionContext) + { + var session = Broker.GetSession(args.TextView); + if (session != null) + { + session.Commit(executionContext.OperationContext.UserCancellationToken); + session.Dismiss(); // TODO: Currently the implementation needs UI thread + return true; + } + return false; + } + + CommandState IChainedCommandHandler<TypeCharCommandArgs>.GetCommandState(TypeCharCommandArgs args, Func<CommandState> nextCommandHandler) + => Available(args.TextView); + + void IChainedCommandHandler<TypeCharCommandArgs>.ExecuteCommand(TypeCharCommandArgs args, Action nextCommandHandler, CommandExecutionContext executionContext) + { + // Execute other commands in the chain to see the change in the buffer. + nextCommandHandler(); + + // We are only inteterested in the top buffer. Currently, commanding implementation calls us multiple times, once per each buffer. + if (args.TextView.BufferGraph.TopBuffer != args.SubjectBuffer) + return; + + var view = args.TextView; + var location = view.Caret.Position.BufferPosition; + var sessionToCommit = Broker.GetSession(args.TextView); + if (sessionToCommit?.ShouldCommit(view, args.TypedChar, location) == true) + { + sessionToCommit.Commit(executionContext.OperationContext.UserCancellationToken, args.TypedChar); + sessionToCommit.Dismiss(); // TODO: Currently the implementation needs UI thread + // Snapshot has changed when committing. Update it for when we try to trigger new session. + location = view.Caret.Position.BufferPosition; + } + + var trigger = new CompletionTrigger(CompletionTriggerReason.Insertion, args.TypedChar); + var session = Broker.GetSession(args.TextView); + if (session != null) + { + session.OpenOrUpdate(view, trigger, location); + } + else if (Broker.ShouldTriggerCompletion(view, args.TypedChar, location)) + { + var newSession = Broker.TriggerCompletion(view, location); + newSession.OpenOrUpdate(view, trigger, location); + } + } + + CommandState ICommandHandler<DownKeyCommandArgs>.GetCommandState(DownKeyCommandArgs args) + => AvailableIfCompletionIsUp(args.TextView); + + bool ICommandHandler<DownKeyCommandArgs>.ExecuteCommand(DownKeyCommandArgs args, CommandExecutionContext executionContext) + { + var session = Broker.GetSession(args.TextView) as AsyncCompletionSession; // we are accessing an internal method + if (session != null) + { + session.SelectDown(); + return true; + } + return false; + } + + CommandState ICommandHandler<PageDownKeyCommandArgs>.GetCommandState(PageDownKeyCommandArgs args) + => AvailableIfCompletionIsUp(args.TextView); + + bool ICommandHandler<PageDownKeyCommandArgs>.ExecuteCommand(PageDownKeyCommandArgs args, CommandExecutionContext executionContext) + { + var session = Broker.GetSession(args.TextView) as AsyncCompletionSession; // we are accessing an internal method + if (session != null) + { + session.SelectPageDown(); + return true; + } + return false; + } + + CommandState ICommandHandler<PageUpKeyCommandArgs>.GetCommandState(PageUpKeyCommandArgs args) + => AvailableIfCompletionIsUp(args.TextView); + + bool ICommandHandler<PageUpKeyCommandArgs>.ExecuteCommand(PageUpKeyCommandArgs args, CommandExecutionContext executionContext) + { + var session = Broker.GetSession(args.TextView) as AsyncCompletionSession; // we are accessing an internal method + if (session != null) + { + session.SelectPageUp(); + return true; + } + return false; + } + + CommandState ICommandHandler<UpKeyCommandArgs>.GetCommandState(UpKeyCommandArgs args) + => AvailableIfCompletionIsUp(args.TextView); + + bool ICommandHandler<UpKeyCommandArgs>.ExecuteCommand(UpKeyCommandArgs args, CommandExecutionContext executionContext) + { + var session = Broker.GetSession(args.TextView) as AsyncCompletionSession; // we are accessing an internal method + if (session != null) + { + session.SelectUp(); + return true; + } + return false; + } + } +} diff --git a/src/Language/Impl/Language/Completion/CompletionModel.cs b/src/Language/Impl/Language/Completion/CompletionModel.cs index 7fd95ac..cda2d2e 100644 --- a/src/Language/Impl/Language/Completion/CompletionModel.cs +++ b/src/Language/Impl/Language/Completion/CompletionModel.cs @@ -9,12 +9,17 @@ using Microsoft.VisualStudio.Text; namespace Microsoft.VisualStudio.Language.Intellisense.Implementation { - class CompletionModel + sealed class CompletionModel { /// <summary> /// All items, as provided by completion item sources. /// </summary> - public readonly ImmutableArray<CompletionItem> AllItems; + public readonly ImmutableArray<CompletionItem> InitialItems; + + /// <summary> + /// Sorted array of all items, as provided by the completion service. + /// </summary> + public readonly ImmutableArray<CompletionItem> SortedItems; /// <summary> /// Span pertinent to this completion model. @@ -27,6 +32,11 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation public readonly ITextSnapshot Snapshot; /// <summary> + /// Stores the initial reason this session was triggererd. + /// </summary> + public readonly CompletionTriggerReason InitialTriggerReason; + + /// <summary> /// Filters involved in this completion model, including their availability and selection state. /// </summary> public readonly ImmutableArray<CompletionFilterWithState> Filters; @@ -34,7 +44,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation /// <summary> /// Items to be displayed in the UI. /// </summary> - public readonly IEnumerable<CompletionItemWithHighlight> PresentedItems; + public readonly ImmutableArray<CompletionItemWithHighlight> PresentedItems; /// <summary> /// Index of item to select. Use -1 to select nothing, when suggestion mode item should be selected. @@ -49,7 +59,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation /// <summary> /// Whether suggestion mode item should be visible. /// </summary> - public readonly bool UseSuggestionMode; + public readonly bool DisplaySuggestionMode; /// <summary> /// Whether suggestion mode item should be selected. @@ -57,190 +67,262 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation public readonly bool SelectSuggestionMode; /// <summary> - /// Text to display in place of suggestion mode when filtered text is empty. + /// <see cref="CompletionItem"/> which contains user-entered text. + /// Used to display and commit the suggestion mode item /// </summary> - public readonly string SuggestionModeDescription; + public readonly CompletionItem SuggestionModeItem; /// <summary> - /// Item to display as suggestion mode item + /// Text to display in place of suggestion mode when filtered text is empty. /// </summary> - public readonly CompletionItemWithHighlight SuggestionModeItem; + public readonly string SuggestionModeDescription; /// <summary> - /// When true, committing the suggestion mode item is a no-op. + /// <see cref="CompletionItem"/> which overrides regular unique item selection. + /// When this is null, the single item from <see cref="PresentedItems"/> is used as unique item. /// </summary> - public readonly bool SuggestionIsEmpty; + public readonly CompletionItem UniqueItem; /// <summary> /// Constructor for the initial model /// </summary> - public CompletionModel(ImmutableArray<CompletionItem> originalItems, ITrackingSpan applicableSpan, ITextSnapshot snapshot, ImmutableArray<CompletionFilterWithState> filters, bool useSoftSelection, bool useSuggestionMode, string suggestionModeDescription) + public CompletionModel(ImmutableArray<CompletionItem> initialItems, ImmutableArray<CompletionItem> sortedItems, + ITrackingSpan applicableSpan, CompletionTriggerReason initialTriggerReason, ITextSnapshot snapshot, + ImmutableArray<CompletionFilterWithState> filters, bool useSoftSelection, bool useSuggestionMode, string suggestionModeDescription, CompletionItem suggestionModeItem) { - AllItems = originalItems; + InitialItems = initialItems; + SortedItems = sortedItems; ApplicableSpan = applicableSpan; + InitialTriggerReason = initialTriggerReason; Snapshot = snapshot; Filters = filters; SelectedIndex = 0; UseSoftSelection = useSoftSelection; - UseSuggestionMode = useSuggestionMode; + DisplaySuggestionMode = useSuggestionMode; SelectSuggestionMode = useSuggestionMode; SuggestionModeDescription = suggestionModeDescription; - SuggestionModeItem = CompletionItemWithHighlight.Empty; + SuggestionModeItem = suggestionModeItem; + UniqueItem = null; } /// <summary> /// Private constructor for the With* methods /// </summary> - private CompletionModel(ImmutableArray<CompletionItem> originalItems, ITrackingSpan applicableSpan, ITextSnapshot snapshot, ImmutableArray<CompletionFilterWithState> filters, IEnumerable<CompletionItemWithHighlight> presentedItems, bool useSoftSelection, bool useSuggestionMode, string suggestionModeDescription, int selectedIndex, bool selectSuggestionMode, CompletionItemWithHighlight suggestionModeItem, bool suggestionIsEmpty) + private CompletionModel(ImmutableArray<CompletionItem> initialItems, ImmutableArray<CompletionItem> sortedItems, ITrackingSpan applicableSpan, CompletionTriggerReason initialTriggerReason, + ITextSnapshot snapshot, ImmutableArray<CompletionFilterWithState> filters, ImmutableArray<CompletionItemWithHighlight> presentedItems, bool useSoftSelection, bool useSuggestionMode, + string suggestionModeDescription, int selectedIndex, bool selectSuggestionMode, CompletionItem suggestionModeItem, CompletionItem uniqueItem) { - AllItems = originalItems; + InitialItems = initialItems; + SortedItems = sortedItems; ApplicableSpan = applicableSpan; + InitialTriggerReason = initialTriggerReason; Snapshot = snapshot; Filters = filters; PresentedItems = presentedItems; SelectedIndex = selectedIndex; UseSoftSelection = useSoftSelection; - UseSuggestionMode = useSuggestionMode; + DisplaySuggestionMode = useSuggestionMode; SelectSuggestionMode = selectSuggestionMode; SuggestionModeDescription = suggestionModeDescription; SuggestionModeItem = suggestionModeItem; - SuggestionIsEmpty = suggestionIsEmpty; } - public CompletionModel WithPresentedItems(IEnumerable<CompletionItemWithHighlight> newPresentedItems, int newSelectedIndex) + public CompletionModel WithPresentedItems(ImmutableArray<CompletionItemWithHighlight> newPresentedItems, int newSelectedIndex) { return new CompletionModel( - originalItems: AllItems, + initialItems: InitialItems, + sortedItems: SortedItems, applicableSpan: ApplicableSpan, + initialTriggerReason: InitialTriggerReason, snapshot: Snapshot, filters: Filters, presentedItems: newPresentedItems, // Updated useSoftSelection: UseSoftSelection, - useSuggestionMode: UseSuggestionMode, + useSuggestionMode: DisplaySuggestionMode, suggestionModeDescription: SuggestionModeDescription, selectedIndex: newSelectedIndex, // Updated selectSuggestionMode: SelectSuggestionMode, suggestionModeItem: SuggestionModeItem, - suggestionIsEmpty: SuggestionIsEmpty + uniqueItem: UniqueItem ); } public CompletionModel WithSnapshot(ITextSnapshot newSnapshot) { return new CompletionModel( - originalItems: AllItems, + initialItems: InitialItems, + sortedItems: SortedItems, applicableSpan: ApplicableSpan, + initialTriggerReason: InitialTriggerReason, snapshot: newSnapshot, // Updated filters: Filters, presentedItems: PresentedItems, useSoftSelection: UseSoftSelection, - useSuggestionMode: UseSuggestionMode, + useSuggestionMode: DisplaySuggestionMode, suggestionModeDescription: SuggestionModeDescription, selectedIndex: SelectedIndex, selectSuggestionMode: SelectSuggestionMode, suggestionModeItem: SuggestionModeItem, - suggestionIsEmpty: SuggestionIsEmpty + uniqueItem: UniqueItem ); } public CompletionModel WithFilters(ImmutableArray<CompletionFilterWithState> newFilters) { return new CompletionModel( - originalItems: AllItems, + initialItems: InitialItems, + sortedItems: SortedItems, applicableSpan: ApplicableSpan, + initialTriggerReason: InitialTriggerReason, snapshot: Snapshot, filters: newFilters, // Updated presentedItems: PresentedItems, useSoftSelection: UseSoftSelection, - useSuggestionMode: UseSuggestionMode, + useSuggestionMode: DisplaySuggestionMode, suggestionModeDescription: SuggestionModeDescription, selectedIndex: SelectedIndex, selectSuggestionMode: SelectSuggestionMode, suggestionModeItem: SuggestionModeItem, - suggestionIsEmpty: SuggestionIsEmpty + uniqueItem: UniqueItem ); } public CompletionModel WithSelectedIndex(int newIndex) { return new CompletionModel( - originalItems: AllItems, + initialItems: InitialItems, + sortedItems: SortedItems, applicableSpan: ApplicableSpan, + initialTriggerReason: InitialTriggerReason, snapshot: Snapshot, filters: Filters, presentedItems: PresentedItems, useSoftSelection: false, // Explicit selection and soft selection are mutually exclusive - useSuggestionMode: UseSuggestionMode, + useSuggestionMode: DisplaySuggestionMode, suggestionModeDescription: SuggestionModeDescription, selectedIndex: newIndex, // Updated selectSuggestionMode: false, // Explicit selection of regular item suggestionModeItem: SuggestionModeItem, - suggestionIsEmpty: SuggestionIsEmpty + uniqueItem: UniqueItem ); } public CompletionModel WithSuggestionItemSelected() { return new CompletionModel( - originalItems: AllItems, + initialItems: InitialItems, + sortedItems: SortedItems, applicableSpan: ApplicableSpan, + initialTriggerReason: InitialTriggerReason, snapshot: Snapshot, filters: Filters, presentedItems: PresentedItems, useSoftSelection: false, // Explicit selection and soft selection are mutually exclusive - useSuggestionMode: UseSuggestionMode, + useSuggestionMode: DisplaySuggestionMode, suggestionModeDescription: SuggestionModeDescription, selectedIndex: -1, // Deselect regular item selectSuggestionMode: true, // Explicit selection of suggestion item suggestionModeItem: SuggestionModeItem, - suggestionIsEmpty: SuggestionIsEmpty + uniqueItem: UniqueItem + ); + } + + public CompletionModel WithSuggestionModeActive(bool newUseSuggestionMode) + { + return new CompletionModel( + initialItems: InitialItems, + sortedItems: SortedItems, + applicableSpan: ApplicableSpan, + initialTriggerReason: InitialTriggerReason, + snapshot: Snapshot, + filters: Filters, + presentedItems: PresentedItems, + useSoftSelection: UseSoftSelection | newUseSuggestionMode, // Enabling suggestion mode also enables soft selection + useSuggestionMode: newUseSuggestionMode, // Updated + suggestionModeDescription: SuggestionModeDescription, + selectedIndex: SelectedIndex, + selectSuggestionMode: SelectSuggestionMode, + suggestionModeItem: SuggestionModeItem, + uniqueItem: UniqueItem ); } - internal CompletionModel WithSuggestionItem(CompletionItemWithHighlight newSuggestionModeItem) + /// <summary> + /// </summary> + /// <param name="newSuggestionModeItem">It is ok to pass in null when there is no suggestion. UI will display SuggestsionModeDescription instead.</param> + internal CompletionModel WithSuggestionModeItem(CompletionItem newSuggestionModeItem) { return new CompletionModel( - originalItems: AllItems, + initialItems: InitialItems, + sortedItems: SortedItems, applicableSpan: ApplicableSpan, + initialTriggerReason: InitialTriggerReason, snapshot: Snapshot, filters: Filters, presentedItems: PresentedItems, useSoftSelection: UseSoftSelection, - useSuggestionMode: UseSuggestionMode, + useSuggestionMode: DisplaySuggestionMode, suggestionModeDescription: SuggestionModeDescription, selectedIndex: SelectedIndex, selectSuggestionMode: SelectSuggestionMode, - suggestionModeItem: newSuggestionModeItem, // Updated - suggestionIsEmpty: false // This method guarantees that the suggestion is not empty + suggestionModeItem: newSuggestionModeItem, + uniqueItem: UniqueItem ); } - internal CompletionModel WithEmptySuggestionItem(CompletionItemWithHighlight newSuggestionModeItem) + /// <summary> + /// </summary> + /// <param name="newUniqueItem">Overrides typical unique item selection. + /// Pass in null to use regular behavior: treating single <see cref="PresentedItems"/> item as the unique item.</param> + internal CompletionModel WithUniqueItem(CompletionItem newUniqueItem) { return new CompletionModel( - originalItems: AllItems, + initialItems: InitialItems, + sortedItems: SortedItems, applicableSpan: ApplicableSpan, + initialTriggerReason: InitialTriggerReason, snapshot: Snapshot, filters: Filters, presentedItems: PresentedItems, useSoftSelection: UseSoftSelection, - useSuggestionMode: UseSuggestionMode, + useSuggestionMode: DisplaySuggestionMode, suggestionModeDescription: SuggestionModeDescription, selectedIndex: SelectedIndex, selectSuggestionMode: SelectSuggestionMode, - suggestionModeItem: newSuggestionModeItem, // Updated - suggestionIsEmpty: true // This method guarantees that the suggestion is empty + suggestionModeItem: SuggestionModeItem, + uniqueItem: newUniqueItem + ); + } + + internal CompletionModel WithSoftSelection(bool newSoftSelection) + { + return new CompletionModel( + initialItems: InitialItems, + sortedItems: SortedItems, + applicableSpan: ApplicableSpan, + initialTriggerReason: InitialTriggerReason, + snapshot: Snapshot, + filters: Filters, + presentedItems: PresentedItems, + useSoftSelection: newSoftSelection, // Updated + useSuggestionMode: DisplaySuggestionMode, + suggestionModeDescription: SuggestionModeDescription, + selectedIndex: SelectedIndex, + selectSuggestionMode: SelectSuggestionMode, + suggestionModeItem: SuggestionModeItem, + uniqueItem: UniqueItem ); } } - class ModelComputation<TModel> + sealed class ModelComputation<TModel> { Task<TModel> _lastTask = Task.FromResult(default(TModel)); private Task _notifyUITask = Task.CompletedTask; private readonly TaskScheduler _computationTaskScheduler; private readonly CancellationToken _token; - private readonly CancellationTokenSource _uiCancellation; + private CancellationTokenSource _uiCancellation; + internal TModel RecentModel { get; private set; } = default(TModel); public ModelComputation(TaskScheduler computationTaskScheduler, CancellationToken token) { @@ -265,17 +347,26 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation /// </summary> public void Enqueue(Func<TModel, CancellationToken, Task<TModel>> transformation, Func<TModel, Task> updateUI) { + System.Diagnostics.Debug.WriteLine($"Enqueue runs on thread {Thread.CurrentThread.ManagedThreadId}"); // This method is based on Roslyn's ModelComputation.ChainTaskAndNotifyControllerWhenFinished var nextTask = _lastTask.ContinueWith(t => transformation(t.Result, _token), _computationTaskScheduler).Unwrap(); _lastTask = nextTask; + // If the _notifyUITask is canceled, refresh it + if (_notifyUITask.IsCanceled || _uiCancellation.IsCancellationRequested) + { + _notifyUITask = Task.CompletedTask; + _uiCancellation = new CancellationTokenSource(); + } + _notifyUITask = Task.Factory.ContinueWhenAll( new[] { _notifyUITask, nextTask }, async existingTasks => { if (existingTasks.All(t => t.Status == TaskStatus.RanToCompletion)) { - if (nextTask == _lastTask && updateUI != null) + OnModelUpdated(nextTask.Result); + if (updateUI != null && nextTask == _lastTask) { await updateUI(nextTask.Result); } @@ -285,6 +376,12 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation ); } + private void OnModelUpdated(TModel result) + { + System.Diagnostics.Debug.WriteLine($"OnModelUpdated runs on thread {Thread.CurrentThread.ManagedThreadId}"); + RecentModel = result; + } + /// <summary> /// Blocks, waiting for all background work to finish. /// Ignores the last piece of work a.k.a. "updateUI" diff --git a/src/Language/Impl/Language/Completion/CompletionUtilities.cs b/src/Language/Impl/Language/Completion/CompletionUtilities.cs index 613ff70..d6c9e0d 100644 --- a/src/Language/Impl/Language/Completion/CompletionUtilities.cs +++ b/src/Language/Impl/Language/Completion/CompletionUtilities.cs @@ -11,15 +11,14 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation { internal static class CompletionUtilities { - - internal static IEnumerable<ITextBuffer> GetBuffersForTriggerPoint(ITextView view, SnapshotPoint point) + internal static IEnumerable<ITextBuffer> GetBuffersForTriggerPoint(ITextView textView, SnapshotPoint point) { // We are looking at the buffer to the left of the caret. - return view.BufferGraph.GetTextBuffers(n => - view.BufferGraph.MapDownToBuffer(point, PointTrackingMode.Negative, n, PositionAffinity.Predecessor) != null); + return textView.BufferGraph.GetTextBuffers(n => + textView.BufferGraph.MapDownToBuffer(point, PointTrackingMode.Negative, n, PositionAffinity.Predecessor) != null); } - internal static IDictionary<IAsyncCompletionItemSource, SnapshotPoint> GetCompletionSourcesWithMappedLocations(ITextView view, SnapshotPoint originalPoint, Func<IContentType, ImmutableArray<IAsyncCompletionItemSource>> getCompletionItemSources) + internal static IDictionary<IAsyncCompletionItemSource, SnapshotPoint> GetCompletionSourcesWithMappedLocations(ITextView textView, SnapshotPoint originalPoint, Func<IContentType, ImmutableArray<IAsyncCompletionItemSource>> getCompletionItemSources) { // This method is created based on EditorCommandHandlerService.GetOrderedBuffersAndCommandHandlers @@ -50,7 +49,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation var sortedContentTypes = new SortedSet<IContentType>(ContentTypeComparer.Instance); var result = new Dictionary<IAsyncCompletionItemSource, SnapshotPoint>(); - var mappedPoints = GetPointsOnAvailableBuffers(view, originalPoint); + var mappedPoints = GetPointsOnAvailableBuffers(textView, originalPoint); foreach (var mappedPoint in mappedPoints) { AddContentTypeHierarchy(sortedContentTypes, mappedPoint.Snapshot.ContentType); @@ -73,10 +72,10 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation return result; } - private static IEnumerable<SnapshotPoint> GetPointsOnAvailableBuffers(ITextView view, SnapshotPoint point) + private static IEnumerable<SnapshotPoint> GetPointsOnAvailableBuffers(ITextView textView, SnapshotPoint point) { - var mappingPoint = view.BufferGraph.CreateMappingPoint(point, PointTrackingMode.Negative); - var buffers = view.BufferGraph.GetTextBuffers(b => mappingPoint.GetPoint(b, PositionAffinity.Predecessor) != null); + var mappingPoint = textView.BufferGraph.CreateMappingPoint(point, PointTrackingMode.Negative); + var buffers = textView.BufferGraph.GetTextBuffers(b => mappingPoint.GetPoint(b, PositionAffinity.Predecessor) != null); var pointsInBuffers = buffers.Select(b => mappingPoint.GetPoint(b, PositionAffinity.Predecessor).Value); return pointsInBuffers; } diff --git a/src/Language/Impl/Language/Completion/DefaultCompletionService.cs b/src/Language/Impl/Language/Completion/DefaultCompletionService.cs index 90a66ec..3e02dbd 100644 --- a/src/Language/Impl/Language/Completion/DefaultCompletionService.cs +++ b/src/Language/Impl/Language/Completion/DefaultCompletionService.cs @@ -1,48 +1,43 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.ComponentModel.Composition; using System.Linq; +using System.Threading; using System.Threading.Tasks; -using Microsoft.VisualStudio.Language.Intellisense; -using System.Collections.Immutable; +using Microsoft.VisualStudio.Core.Imaging; using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Text.PatternMatching; using Microsoft.VisualStudio.Utilities; -using Microsoft.VisualStudio.Core.Imaging; -using System; - -#if NET46 -using System.ComponentModel.Composition; -#else -using System.Composition; -using Microsoft.VisualStudio.Text.Editor; -#endif namespace Microsoft.VisualStudio.Language.Intellisense.Implementation { [Export(typeof(IAsyncCompletionService))] - [Name("Default completion service")] - [ContentType("text")] - public class DefaultCompletionService : IAsyncCompletionService + [Name(KnownCompletionNames.DefaultCompletionService)] + [ContentType("any")] + internal class DefaultCompletionService : IAsyncCompletionService { [Import] public IPatternMatcherFactory PatternMatcherFactory { get; set; } -#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously - async Task<CompletionList> IAsyncCompletionService.UpdateCompletionListAsync(IEnumerable<CompletionItem> originalList, CompletionTrigger trigger, ITextSnapshot snapshot, ITrackingSpan applicableSpan, ImmutableArray<CompletionFilterWithState> filters) -#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously + Task<FilteredCompletionModel> IAsyncCompletionService.UpdateCompletionListAsync( + ImmutableArray<CompletionItem> sortedList, CompletionTriggerReason triggerReason, CompletionFilterReason filterReason, + ITextSnapshot snapshot, ITrackingSpan applicableSpan, ImmutableArray<CompletionFilterWithState> filters, ITextView view, CancellationToken token) { // Filter by text var filterText = applicableSpan.GetText(snapshot); if (string.IsNullOrWhiteSpace(filterText)) { // There is no text filtering. Just apply user filters, sort alphabetically and return. - var listFiltered = originalList; + IEnumerable<CompletionItem> listFiltered = sortedList; if (filters.Any(n => n.IsSelected)) { - listFiltered = originalList.Where(n => ShouldBeInCompletionList(n, filters)); + listFiltered = sortedList.Where(n => ShouldBeInCompletionList(n, filters)); } var listSorted = listFiltered.OrderBy(n => n.SortText); - var listHighlighted = listSorted.Select(n => new CompletionItemWithHighlight(n)); - return new CompletionList(listHighlighted, 0, filters); + var listHighlighted = listSorted.Select(n => new CompletionItemWithHighlight(n)).ToImmutableArray(); + return Task.FromResult(new FilteredCompletionModel(listHighlighted, 0, filters)); } // Pattern matcher not only filters, but also provides a way to order the results by their match quality. @@ -51,7 +46,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation filterText, new PatternMatcherCreationOptions(System.Globalization.CultureInfo.CurrentCulture, PatternMatcherCreationFlags.IncludeMatchedSpans)); - var matches = originalList + var matches = sortedList // Perform pattern matching .Select(completionItem => (completionItem, patternMatcher.TryMatch(completionItem.FilterText))) // Pick only items that were matched, unless length of filter text is 1 @@ -70,10 +65,8 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation filterFilteredList = matches.Where(n => ShouldBeInCompletionList(n.Item1, filters)); } - // Order the list alphabetically and select the best match - var sortedList = filterFilteredList.OrderBy(n => n.Item1.SortText); var bestMatch = filterFilteredList.OrderByDescending(n => n.Item2.HasValue).ThenBy(n => n.Item2).FirstOrDefault(); - var listWithHighlights = sortedList.Select(n => n.Item2.HasValue ? new CompletionItemWithHighlight(n.Item1, n.Item2.Value.MatchedSpans) : new CompletionItemWithHighlight(n.Item1)).ToImmutableArray(); + var listWithHighlights = filterFilteredList.Select(n => n.Item2.HasValue ? new CompletionItemWithHighlight(n.Item1, n.Item2.Value.MatchedSpans) : new CompletionItemWithHighlight(n.Item1)).ToImmutableArray(); int selectedItemIndex = 0; for (int i = 0; i < listWithHighlights.Length; i++) @@ -85,12 +78,19 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation } } - return new CompletionList(listWithHighlights, selectedItemIndex, updatedFilters); + return Task.FromResult(new FilteredCompletionModel(listWithHighlights, selectedItemIndex, updatedFilters)); + } + + Task<ImmutableArray<CompletionItem>> IAsyncCompletionService.SortCompletionListAsync( + ImmutableArray<CompletionItem> initialList, CompletionTriggerReason triggerReason, ITextSnapshot snapshot, + ITrackingSpan applicableToSpan, ITextView view, CancellationToken token) + { + return Task.FromResult(initialList.OrderBy(n => n.SortText).ToImmutableArray()); } #region Filtering - public static bool ShouldBeInCompletionList( + private static bool ShouldBeInCompletionList( CompletionItem item, ImmutableArray<CompletionFilterWithState> filtersWithState) { @@ -107,7 +107,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation #endregion } -#if DEBUG +#if DEBUG && false [Export(typeof(IAsyncCompletionItemSource))] [Name("Debug completion item source")] [Order(After = "default")] @@ -123,18 +123,18 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation private static readonly ImmutableArray<CompletionFilter> FilterCollection1 = ImmutableArray.Create(Filter1); private static readonly ImmutableArray<CompletionFilter> FilterCollection2 = ImmutableArray.Create(Filter2); private static readonly ImmutableArray<CompletionFilter> FilterCollection3 = ImmutableArray.Create(Filter3); - private static readonly ImmutableArray<string> commitCharacters = ImmutableArray.Create(" ", ";", "\t", ".", "<", "(", "["); + private static readonly ImmutableArray<char> commitCharacters = ImmutableArray.Create(' ', ';', '\t', '.', '<', '(', '['); - void IAsyncCompletionItemSource.CustomCommit(Text.Editor.ITextView view, ITextBuffer buffer, CompletionItem item, ITrackingSpan applicableSpan, string commitCharacter) + void IAsyncCompletionItemSource.CustomCommit(Text.Editor.ITextView view, ITextBuffer buffer, CompletionItem item, ITrackingSpan applicableSpan, char typeChar) { throw new System.NotImplementedException(); } - async Task<CompletionContext> IAsyncCompletionItemSource.GetCompletionContextAsync(CompletionTrigger trigger, SnapshotPoint triggerLocation) + async Task<CompletionContext> IAsyncCompletionItemSource.GetCompletionContextAsync(CompletionTrigger trigger, SnapshotPoint triggerLocation, CancellationToken token) { var charBeforeCaret = triggerLocation.Subtract(1).GetChar(); SnapshotSpan applicableSpan; - if (commitCharacters.Contains(charBeforeCaret.ToString())) + if (commitCharacters.Contains(charBeforeCaret)) { // skip this character. the applicable span starts later applicableSpan = new SnapshotSpan(triggerLocation, 0); @@ -146,32 +146,25 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation } return await Task.FromResult(new CompletionContext( ImmutableArray.Create( - new CompletionItem("SampleItem<>", "SampleItem", "SampleItem<>", "SampleItem", this, FilterCollection1, false, Icon1), - new CompletionItem("AnotherItem🐱👤", "AnotherItem", "AnotherItem", "AnotherItem", this, FilterCollection1, false, Icon1), - new CompletionItem("Aaaaa", "Aaaaa", "Aaaaa", "Aaaaa", this, FilterCollection2, false, Icon2), - new CompletionItem("Bbbbb", "Bbbbb", "Bbbbb", "Bbbbb", this, FilterCollection2, false, Icon2), - new CompletionItem("Ccccc", "Ccccc", "Ccccc", "Ccccc", this, FilterCollection2, false, Icon2), - new CompletionItem("Ddddd", "Ddddd", "Ddddd", "Ddddd", this, FilterCollection2, false, Icon2), - new CompletionItem("Eeee", "Eeee", "Eeee", "Eeee", this, FilterCollection2, false, Icon2), - new CompletionItem("Ffffff", "Ffffff", "Ffffff", "Ffffff", this, FilterCollection2, false, Icon2), - new CompletionItem("Ggggggg", "Ggggggg", "Ggggggg", "Ggggggg", this, FilterCollection2, false, Icon2), - new CompletionItem("Hhhhh", "Hhhhh", "Hhhhh", "Hhhhh", this, FilterCollection2, false, Icon2), - new CompletionItem("Iiiii", "Iiiii", "Iiiii", "Iiiii", this, FilterCollection2, false, Icon2), - new CompletionItem("Jjjjj", "Jjjjj", "Jjjjj", "Jjjjj", this, FilterCollection3, false, Icon3), - new CompletionItem("kkkkk", "kkkkk", "kkkkk", "kkkkk", this, FilterCollection3, false, Icon3), - new CompletionItem("llllol", "llllol", "llllol", "llllol", this, FilterCollection3, false, Icon3), - new CompletionItem("mmmmm", "mmmmm", "mmmmm", "mmmmm", this, FilterCollection3, false, Icon3), - new CompletionItem("nnNnnn", "nnNnnn", "nnNnnn", "nnNnnn", this, FilterCollection3, false, Icon3), - new CompletionItem("oOoOOO", "oOoOOO", "oOoOOO", "oOoOOO", this, FilterCollection3, false, Icon3) + new CompletionItem("SampleItem<>", this, Icon3, FilterCollection3, string.Empty, false, "SampleItem", "SampleItem<>", "SampleItem", ImmutableArray<AccessibleImage>.Empty), + new CompletionItem("AnotherItem🐱👤", this, Icon3, FilterCollection3, string.Empty, false, "AnotherItem", "AnotherItem", "AnotherItem", ImmutableArray.Create(new AccessibleImage("cat", "ninja cat", Icon3))), + new CompletionItem("Sampling", this, Icon1, FilterCollection1), + new CompletionItem("Sampler", this, Icon1, FilterCollection1), + new CompletionItem("Sapling", this, Icon2, FilterCollection2, "Sapling is a young tree"), + new CompletionItem("OverSampling", this, Icon1, FilterCollection1, "overload"), + new CompletionItem("AnotherSample", this, Icon2, FilterCollection2), + new CompletionItem("AnotherSampling", this, Icon2, FilterCollection2), + new CompletionItem("Simple", this, Icon3, FilterCollection3, "KISS"), + new CompletionItem("Simpler", this, Icon3, FilterCollection3, "KISS") ), applicableSpan));//, true, true, "Suggestion mode description!")); } - async Task<object> IAsyncCompletionItemSource.GetDescriptionAsync(CompletionItem item) + async Task<object> IAsyncCompletionItemSource.GetDescriptionAsync(CompletionItem item, CancellationToken token) { return await Task.FromResult("This is a tooltip for " + item.DisplayText); } - ImmutableArray<string> IAsyncCompletionItemSource.GetPotentialCommitCharacters() => commitCharacters; + ImmutableArray<char> IAsyncCompletionItemSource.GetPotentialCommitCharacters() => commitCharacters; #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously async Task IAsyncCompletionItemSource.HandleViewClosedAsync(Text.Editor.ITextView view) @@ -180,12 +173,12 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation return; } - bool IAsyncCompletionItemSource.ShouldCommitCompletion(string typedChar, SnapshotPoint location) + bool IAsyncCompletionItemSource.ShouldCommitCompletion(char typeChar, SnapshotPoint location) { return true; } - bool IAsyncCompletionItemSource.ShouldTriggerCompletion(string typedChar, SnapshotPoint location) + bool IAsyncCompletionItemSource.ShouldTriggerCompletion(char typeChar, SnapshotPoint location) { return true; } @@ -197,24 +190,24 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation [ContentType("RazorCSharp")] public class DebugHtmlCompletionItemSource : IAsyncCompletionItemSource { - void IAsyncCompletionItemSource.CustomCommit(Text.Editor.ITextView view, ITextBuffer buffer, CompletionItem item, ITrackingSpan applicableSpan, string commitCharacter) + void IAsyncCompletionItemSource.CustomCommit(Text.Editor.ITextView view, ITextBuffer buffer, CompletionItem item, ITrackingSpan applicableSpan, char typeChar) { throw new System.NotImplementedException(); } - async Task<CompletionContext> IAsyncCompletionItemSource.GetCompletionContextAsync(CompletionTrigger trigger, SnapshotPoint triggerLocation) + async Task<CompletionContext> IAsyncCompletionItemSource.GetCompletionContextAsync(CompletionTrigger trigger, SnapshotPoint triggerLocation, CancellationToken token) { return await Task.FromResult(new CompletionContext(ImmutableArray.Create(new CompletionItem("html", this), new CompletionItem("head", this), new CompletionItem("body", this), new CompletionItem("header", this)), new SnapshotSpan(triggerLocation, 0))); } - async Task<object> IAsyncCompletionItemSource.GetDescriptionAsync(CompletionItem item) + async Task<object> IAsyncCompletionItemSource.GetDescriptionAsync(CompletionItem item, CancellationToken token) { return await Task.FromResult(item.DisplayText); } - ImmutableArray<string> IAsyncCompletionItemSource.GetPotentialCommitCharacters() + ImmutableArray<char> IAsyncCompletionItemSource.GetPotentialCommitCharacters() { - return ImmutableArray.Create(" ", ">", "=", "\t"); + return ImmutableArray.Create(' ', '>', '=', '\t'); } #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously @@ -224,12 +217,12 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation return; } - bool IAsyncCompletionItemSource.ShouldCommitCompletion(string typedChar, SnapshotPoint location) + bool IAsyncCompletionItemSource.ShouldCommitCompletion(char typeChar, SnapshotPoint location) { return true; } - bool IAsyncCompletionItemSource.ShouldTriggerCompletion(string typedChar, SnapshotPoint location) + bool IAsyncCompletionItemSource.ShouldTriggerCompletion(char typeChar, SnapshotPoint location) { return true; } diff --git a/src/Language/Impl/Language/Completion/ModernCompletionFeature.cs b/src/Language/Impl/Language/Completion/ModernCompletionFeature.cs new file mode 100644 index 0000000..4a3f874 --- /dev/null +++ b/src/Language/Impl/Language/Completion/ModernCompletionFeature.cs @@ -0,0 +1,32 @@ +using System.Diagnostics; +using Microsoft.VisualStudio.Text.Utilities; + +namespace Microsoft.VisualStudio.Language.Intellisense.Implementation +{ + /// <summary> + /// Provides information whether modern completion should be enabled, given the buffer's content type. + /// </summary> + internal static class ModernCompletionFeature + { + private const string TreatmentFlightName = "CompletionAPI"; + private static bool _treatmentFlightEnabled; + private static bool _initialized; + + /// <summary> + /// Returns whether or not modern completion should be enabled. + /// </summary> + /// <returns>true if experiment is enabled.</returns> + public static bool GetFeatureState(IExperimentationServiceInternal experimentationService) + { + if (_initialized) + { + return _treatmentFlightEnabled; + } + + _treatmentFlightEnabled = experimentationService.IsCachedFlightEnabled(TreatmentFlightName); + Debug.Assert(_treatmentFlightEnabled); + _initialized = true; + return _treatmentFlightEnabled; + } + } +} diff --git a/src/Language/Impl/Language/Completion/SelectDownCommandHandler.cs b/src/Language/Impl/Language/Completion/SelectDownCommandHandler.cs deleted file mode 100644 index 6d38a74..0000000 --- a/src/Language/Impl/Language/Completion/SelectDownCommandHandler.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.ComponentModel.Composition; -using Microsoft.VisualStudio.Commanding; -using Microsoft.VisualStudio.Language.Intellisense; -using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; -using Microsoft.VisualStudio.Utilities; - -namespace Microsoft.VisualStudio.Language.Intellisense.Implementation -{ - /// <summary> - /// Reacts to the down arrow command and attempts to scroll the completion list. - /// </summary> - [Name(nameof(SelectDownCommandHandler))] - [ContentType("any")] - [Export(typeof(ICommandHandler))] - internal sealed class SelectDownCommandHandler : ICommandHandler<DownKeyCommandArgs> - { - [Import] - IAsyncCompletionBroker broker; - - // TODO: Localize - string INamed.DisplayName => "Handler for down arrow in completion"; - - bool ICommandHandler<DownKeyCommandArgs>.ExecuteCommand(DownKeyCommandArgs args, CommandExecutionContext executionContext) - { - if (broker.IsCompletionActive(args.TextView)) - { - broker.SelectDown(args.TextView); - return true; - } - return false; - } - - CommandState ICommandHandler<DownKeyCommandArgs>.GetCommandState(DownKeyCommandArgs args) - { - return broker.IsCompletionActive(args.TextView) - ? CommandState.Available - : CommandState.Unspecified; - } - } -} diff --git a/src/Language/Impl/Language/Completion/SelectPageDownCommandHandler.cs b/src/Language/Impl/Language/Completion/SelectPageDownCommandHandler.cs deleted file mode 100644 index c9b0cda..0000000 --- a/src/Language/Impl/Language/Completion/SelectPageDownCommandHandler.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.ComponentModel.Composition; -using Microsoft.VisualStudio.Commanding; -using Microsoft.VisualStudio.Language.Intellisense; -using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; -using Microsoft.VisualStudio.Utilities; - -namespace Microsoft.VisualStudio.Language.Intellisense.Implementation -{ - /// <summary> - /// Reacts to the page down command and attempts to scroll the completion list. - /// </summary> - [Name(nameof(SelectPageDownCommandHandler))] - [ContentType("any")] - [Export(typeof(ICommandHandler))] - internal sealed class SelectPageDownCommandHandler : ICommandHandler<PageDownKeyCommandArgs> - { - [Import] - IAsyncCompletionBroker broker; - - // TODO: Localize - string INamed.DisplayName => "Handler for page down in completion"; - - bool ICommandHandler<PageDownKeyCommandArgs>.ExecuteCommand(PageDownKeyCommandArgs args, CommandExecutionContext executionContext) - { - if (broker.IsCompletionActive(args.TextView)) - { - broker.SelectPageDown(args.TextView); - return true; - } - return false; - } - - CommandState ICommandHandler<PageDownKeyCommandArgs>.GetCommandState(PageDownKeyCommandArgs args) - { - return broker.IsCompletionActive(args.TextView) - ? CommandState.Available - : CommandState.Unspecified; - } - } -} diff --git a/src/Language/Impl/Language/Completion/SelectPageUpCommandHandler.cs b/src/Language/Impl/Language/Completion/SelectPageUpCommandHandler.cs deleted file mode 100644 index 49af033..0000000 --- a/src/Language/Impl/Language/Completion/SelectPageUpCommandHandler.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.ComponentModel.Composition; -using Microsoft.VisualStudio.Commanding; -using Microsoft.VisualStudio.Language.Intellisense; -using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; -using Microsoft.VisualStudio.Utilities; - -namespace Microsoft.VisualStudio.Language.Intellisense.Implementation -{ - /// <summary> - /// Reacts to the page up command and attempts to scroll the completion list. - /// </summary> - [Name(nameof(SelectPageUpCommandHandler))] - [ContentType("any")] - [Export(typeof(ICommandHandler))] - internal sealed class SelectPageUpCommandHandler : ICommandHandler<PageUpKeyCommandArgs> - { - [Import] - IAsyncCompletionBroker broker; - - // TODO: Localize - string INamed.DisplayName => "Handler for page down in completion"; - - bool ICommandHandler<PageUpKeyCommandArgs>.ExecuteCommand(PageUpKeyCommandArgs args, CommandExecutionContext executionContext) - { - if (broker.IsCompletionActive(args.TextView)) - { - broker.SelectPageUp(args.TextView); - return true; - } - return false; - } - - CommandState ICommandHandler<PageUpKeyCommandArgs>.GetCommandState(PageUpKeyCommandArgs args) - { - return broker.IsCompletionActive(args.TextView) - ? CommandState.Available - : CommandState.Unspecified; - } - } -} diff --git a/src/Language/Impl/Language/Completion/SelectUpCommandHandler.cs b/src/Language/Impl/Language/Completion/SelectUpCommandHandler.cs deleted file mode 100644 index d31e71e..0000000 --- a/src/Language/Impl/Language/Completion/SelectUpCommandHandler.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.ComponentModel.Composition; -using Microsoft.VisualStudio.Commanding; -using Microsoft.VisualStudio.Language.Intellisense; -using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; -using Microsoft.VisualStudio.Utilities; - -namespace Microsoft.VisualStudio.Language.Intellisense.Implementation -{ - /// <summary> - /// Reacts to the up arrow command and attempts to scroll the completion list. - /// </summary> - [Name(nameof(SelectUpCommandHandler))] - [ContentType("any")] - [Export(typeof(ICommandHandler))] - internal sealed class SelectUpCommandHandler : ICommandHandler<UpKeyCommandArgs> - { - [Import] - IAsyncCompletionBroker broker; - - // TODO: Localize - string INamed.DisplayName => "Handler for down arrow in completion"; - - bool ICommandHandler<UpKeyCommandArgs>.ExecuteCommand(UpKeyCommandArgs args, CommandExecutionContext executionContext) - { - if (broker.IsCompletionActive(args.TextView)) - { - broker.SelectUp(args.TextView); - return true; - } - return false; - } - - CommandState ICommandHandler<UpKeyCommandArgs>.GetCommandState(UpKeyCommandArgs args) - { - return broker.IsCompletionActive(args.TextView) - ? CommandState.Available - : CommandState.Unspecified; - } - } -} diff --git a/src/Language/Impl/Language/Completion/SuggestionModeCompletionItemSource.cs b/src/Language/Impl/Language/Completion/SuggestionModeCompletionItemSource.cs new file mode 100644 index 0000000..e3a6abb --- /dev/null +++ b/src/Language/Impl/Language/Completion/SuggestionModeCompletionItemSource.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; + +namespace Microsoft.VisualStudio.Language.Intellisense.Implementation +{ + /// <summary> + /// Internal item source used during lifetime of the suggestion mode item. + /// </summary> + internal class SuggestionModeCompletionItemSource : IAsyncCompletionItemSource + { + static IAsyncCompletionItemSource _instance; + internal static IAsyncCompletionItemSource Instance + { + get + { + if (_instance == null) + _instance = new SuggestionModeCompletionItemSource(); + return _instance; + } + } + + void IAsyncCompletionItemSource.CustomCommit(ITextView view, ITextBuffer buffer, CompletionItem item, ITrackingSpan applicableSpan, char typeChar, CancellationToken token) + { + throw new NotImplementedException("Suggestion mode item does not have custom commit behavior"); + } + + Task<CompletionContext> IAsyncCompletionItemSource.GetCompletionContextAsync(CompletionTrigger trigger, SnapshotPoint triggerLocation, CancellationToken token) + { + throw new NotImplementedException("This item source is not meant to be registered. It is used only to provide tooltip."); + } + + Task<object> IAsyncCompletionItemSource.GetDescriptionAsync(CompletionItem item, CancellationToken token) + { + return Task.FromResult<object>(string.Empty); + } + + ImmutableArray<char> IAsyncCompletionItemSource.GetPotentialCommitCharacters() + { + throw new NotImplementedException("This item source is not meant to be registered. It is used only to provide tooltip."); + } + + Task IAsyncCompletionItemSource.HandleViewClosedAsync(ITextView view) + { + throw new NotImplementedException("This item source is not meant to be registered. It is used only to provide tooltip."); + } + + bool IAsyncCompletionItemSource.ShouldCommitCompletion(char typeChar, SnapshotPoint location) + { + return false; // Typing should not commit the suggestion mode item. + } + + bool IAsyncCompletionItemSource.ShouldTriggerCompletion(char typeChar, SnapshotPoint location) + { + throw new NotImplementedException("This item source is not meant to be registered. It is used only to provide tooltip."); + } + } +} diff --git a/src/Language/Impl/Language/Strings.Designer.cs b/src/Language/Impl/Language/Strings.Designer.cs new file mode 100644 index 0000000..e4e63cc --- /dev/null +++ b/src/Language/Impl/Language/Strings.Designer.cs @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------ +// <auto-generated> +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// </auto-generated> +//------------------------------------------------------------------------------ + +namespace Microsoft.VisualStudio.Language.Intellisense.Implementation { + using System; + + + /// <summary> + /// A strongly-typed resource class, for looking up localized strings, etc. + /// </summary> + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class Strings { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Strings() { + } + + /// <summary> + /// Returns the cached ResourceManager instance used by this class. + /// </summary> + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.VisualStudio.Text.Implementation.Language.Impl.Language.Strings", typeof(Strings).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// <summary> + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// </summary> + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// <summary> + /// Looks up a localized string similar to Completion command handler. + /// </summary> + public static string CompletionCommandHandlerName { + get { + return ResourceManager.GetString("CompletionCommandHandlerName", resourceCulture); + } + } + } +} diff --git a/src/Language/Impl/Language/Strings.resx b/src/Language/Impl/Language/Strings.resx new file mode 100644 index 0000000..3870d03 --- /dev/null +++ b/src/Language/Impl/Language/Strings.resx @@ -0,0 +1,123 @@ +<?xml version="1.0" encoding="utf-8"?> +<root> + <!-- + Microsoft ResX Schema + + Version 2.0 + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes + associated with the data types. + + Example: + + ... ado.net/XML headers & schema ... + <resheader name="resmimetype">text/microsoft-resx</resheader> + <resheader name="version">2.0</resheader> + <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> + <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader> + <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data> + <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data> + <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> + <value>[base64 mime encoded serialized .NET Framework object]</value> + </data> + <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> + <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> + <comment>This is a comment</comment> + </data> + + There are any number of "resheader" rows that contain simple + name/value pairs. + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the + mimetype set. + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not + extensible. For a given mimetype the value must be set accordingly: + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can + read any of the formats listed below. + + mimetype: application/x-microsoft.net.object.binary.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.soap.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Soap.SoapFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.bytearray.base64 + value : The object must be serialized into a byte array + : using a System.ComponentModel.TypeConverter + : and then encoded with base64 encoding. + --> + <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> + <xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> + <xsd:element name="root" msdata:IsDataSet="true"> + <xsd:complexType> + <xsd:choice maxOccurs="unbounded"> + <xsd:element name="metadata"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" /> + </xsd:sequence> + <xsd:attribute name="name" use="required" type="xsd:string" /> + <xsd:attribute name="type" type="xsd:string" /> + <xsd:attribute name="mimetype" type="xsd:string" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="assembly"> + <xsd:complexType> + <xsd:attribute name="alias" type="xsd:string" /> + <xsd:attribute name="name" type="xsd:string" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="data"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> + <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> + <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="resheader"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" /> + </xsd:complexType> + </xsd:element> + </xsd:choice> + </xsd:complexType> + </xsd:element> + </xsd:schema> + <resheader name="resmimetype"> + <value>text/microsoft-resx</value> + </resheader> + <resheader name="version"> + <value>2.0</value> + </resheader> + <resheader name="reader"> + <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <resheader name="writer"> + <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <data name="CompletionCommandHandlerName" xml:space="preserve"> + <value>Completion command handler</value> + </data> +</root>
\ No newline at end of file diff --git a/src/Microsoft.VisualStudio.Text.Implementation.csproj b/src/Microsoft.VisualStudio.Text.Implementation.csproj index adf7d62..d2e464e 100644 --- a/src/Microsoft.VisualStudio.Text.Implementation.csproj +++ b/src/Microsoft.VisualStudio.Text.Implementation.csproj @@ -5,10 +5,10 @@ <SignAssembly>true</SignAssembly> <AssemblyOriginatorKeyFile>key.snk</AssemblyOriginatorKeyFile> <DelaySign>false</DelaySign> - <Version>15.0.13-pre</Version> + <Version>15.0.14-pre</Version> <AssemblyVersion>15.0.0.0</AssemblyVersion> - <NuGetVersionEditor>15.8.173-preview-g3a2638b5fc</NuGetVersionEditor> - <NuGetVersionLanguage>15.6.289-preview-g5a23388e08</NuGetVersionLanguage> + <NuGetVersionEditor>15.6.391-preview-g3c4ef6e5fc</NuGetVersionEditor> + <NuGetVersionLanguage>15.6.391-preview-g3c4ef6e5fc</NuGetVersionLanguage> </PropertyGroup> <PropertyGroup> @@ -33,6 +33,11 @@ <DesignTime>True</DesignTime> <DependentUpon>Strings.resx</DependentUpon> </Compile> + <Compile Update="Language\Impl\Language\Strings.Designer.cs"> + <AutoGen>True</AutoGen> + <DesignTime>True</DesignTime> + <DependentUpon>Strings.resx</DependentUpon> + </Compile> <Compile Update="Text\Impl\TextModel\Strings.Designer.cs"> <AutoGen>True</AutoGen> <DesignTime>True</DesignTime> @@ -76,6 +81,11 @@ <LastGenOutput>Strings.Designer.cs</LastGenOutput> <CustomToolNamespace>Microsoft.VisualStudio.Utilities.Implementation</CustomToolNamespace> </EmbeddedResource> + <EmbeddedResource Update="Language\Impl\Language\Strings.resx"> + <Generator>ResXFileCodeGenerator</Generator> + <LastGenOutput>Strings.Designer.cs</LastGenOutput> + <CustomToolNamespace>Microsoft.VisualStudio.Language.Intellisense.Implementation</CustomToolNamespace> + </EmbeddedResource> <EmbeddedResource Update="Text\Impl\TextModel\Strings.resx"> <Generator>ResXFileCodeGenerator</Generator> <LastGenOutput>Strings.Designer.cs</LastGenOutput> diff --git a/src/Text/Def/TextData/Model/IExtensionPerformanceTracker.cs b/src/Text/Def/TextData/Model/IExtensionPerformanceTracker.cs index fae97b8..3c5abdd 100644 --- a/src/Text/Def/TextData/Model/IExtensionPerformanceTracker.cs +++ b/src/Text/Def/TextData/Model/IExtensionPerformanceTracker.cs @@ -26,5 +26,15 @@ namespace Microsoft.VisualStudio.Text /// Invoked after calling to an extension event handler. /// </summary> void AfterCallingEventHandler(Delegate eventHandler); + + /// <summary> + /// Invoked before calling to an extensibility point. + /// </summary> + void BeforeCallingExtension(object extension); + + /// <summary> + /// Invoked after calling to an extensibility point. + /// </summary> + void AfterCallingExtension(object extension); } } diff --git a/src/Text/Def/TextUI/Utilities/AbstractUIThreadOperationContext.cs b/src/Text/Def/TextUI/Utilities/AbstractUIThreadOperationContext.cs index c7d8279..8047106 100644 --- a/src/Text/Def/TextUI/Utilities/AbstractUIThreadOperationContext.cs +++ b/src/Text/Def/TextUI/Utilities/AbstractUIThreadOperationContext.cs @@ -13,7 +13,7 @@ namespace Microsoft.VisualStudio.Utilities private List<IUIThreadOperationScope> _scopes; private bool _allowCancellation; private PropertyCollection _properties; - private readonly string _contextDescription; + private readonly string _defaultDescription; private int _completedItems; private int _totalItems; @@ -22,11 +22,11 @@ namespace Microsoft.VisualStudio.Utilities /// </summary> /// <param name="allowCancellation">Initial value of the <see cref="IUIThreadOperationContext.AllowCancellation"/> /// property, which can change as new scopes are added to the context.</param> - /// <param name="description">Initial value of the <see cref="IUIThreadOperationContext.Description"/> + /// <param name="defaultDescription">Default value of the <see cref="IUIThreadOperationContext.Description"/> /// property, which can change as new scopes are added to the context.</param> - public AbstractUIThreadOperationContext(bool allowCancellation, string description) + public AbstractUIThreadOperationContext(bool allowCancellation, string defaultDescription) { - _contextDescription = description ?? throw new ArgumentNullException(nameof(description)); + _defaultDescription = defaultDescription ?? throw new ArgumentNullException(nameof(defaultDescription)); _allowCancellation = allowCancellation; } @@ -64,7 +64,7 @@ namespace Microsoft.VisualStudio.Utilities } /// <summary> - /// Gets user readable operation description, composed of initial context description and + /// Gets user readable operation description, composed of initial context description or /// descriptions of all currently added scopes. /// </summary> public virtual string Description @@ -73,11 +73,17 @@ namespace Microsoft.VisualStudio.Utilities { if (_scopes == null || _scopes.Count == 0) { - return _contextDescription; + return _defaultDescription; } - // Combine context description with descriptions of all current scopes - return _contextDescription + Environment.NewLine + string.Join(Environment.NewLine, _scopes.Select((s) => s.Description)); + // Most common case + if (_scopes.Count == 1) + { + return _scopes[0].Description; + } + + // Combine descriptions of all current scopes + return string.Join(Environment.NewLine, _scopes.Select((s) => s.Description)); } } diff --git a/src/Text/Def/TextUI/Utilities/IUIThreadOperationExecutor.cs b/src/Text/Def/TextUI/Utilities/IUIThreadOperationExecutor.cs index 3daf364..b47b0b4 100644 --- a/src/Text/Def/TextUI/Utilities/IUIThreadOperationExecutor.cs +++ b/src/Text/Def/TextUI/Utilities/IUIThreadOperationExecutor.cs @@ -65,12 +65,14 @@ namespace Microsoft.VisualStudio.Utilities /// Executes the action synchronously and waits for it to complete. /// </summary> /// <param name="title">Operation's title.</param> - /// <param name="description">Initial operation's description.</param> - /// <param name="allowCancel">Whether to allow cancellability.</param> + /// <param name="defaultDescription">Default operation's description, which is displayed on the wait dialog unless + /// one or more <see cref="IUIThreadOperationScope"/>s with more specific descriptions were added to + /// the <see cref="IUIThreadOperationContext"/>.</param> + /// <param name="allowCancellation">Whether to allow cancellability.</param> /// <param name="showProgress">Whether to show progress indication.</param> /// <param name="action">An action to execute.</param> /// <returns>A status of action execution.</returns> - UIThreadOperationStatus Execute(string title, string description, bool allowCancel, bool showProgress, + UIThreadOperationStatus Execute(string title, string defaultDescription, bool allowCancellation, bool showProgress, Action<IUIThreadOperationContext> action); /// <summary> @@ -78,12 +80,14 @@ namespace Microsoft.VisualStudio.Utilities /// cancellability and wait indication. /// </summary> /// <param name="title">Operation's title.</param> - /// <param name="description">Initial operation's description.</param> - /// <param name="allowCancel">Whether to allow cancellability.</param> + /// <param name="defaultDescription">Default operation's description, which is displayed on the wait dialog unless + /// one or more <see cref="IUIThreadOperationScope"/>s with more specific descriptions were added to + /// the <see cref="IUIThreadOperationContext"/>.</param> + /// <param name="allowCancellation">Whether to allow cancellability.</param> /// <param name="showProgress">Whether to show progress indication.</param> /// <returns><see cref="IUIThreadOperationContext"/> instance that provides access to shared two way /// cancellability and wait indication for the given operation. The operation is considered executed /// when this <see cref="IUIThreadOperationContext"/> instance is disposed.</returns> - IUIThreadOperationContext BeginExecute(string title, string description, bool allowCancel, bool showProgress); + IUIThreadOperationContext BeginExecute(string title, string defaultDescription, bool allowCancellation, bool showProgress); } } diff --git a/src/Text/Impl/ClassificationAggregator/ClassifierAggregator.cs b/src/Text/Impl/ClassificationAggregator/ClassifierAggregator.cs index 7ccd6cb..b10eee8 100644 --- a/src/Text/Impl/ClassificationAggregator/ClassifierAggregator.cs +++ b/src/Text/Impl/ClassificationAggregator/ClassifierAggregator.cs @@ -97,11 +97,17 @@ namespace Microsoft.VisualStudio.Text.Classification.Implementation /// </returns> public IList<ClassificationSpan> GetClassificationSpans(SnapshotSpan span) { + if (_tagAggregator == null) + return new List<ClassificationSpan>(0); + return this.InternalGetClassificationSpans(span, _tagAggregator.GetTags(span)); } public IList<ClassificationSpan> GetAllClassificationSpans(SnapshotSpan span, CancellationToken cancel) { + if (_tagAggregator == null) + return new List<ClassificationSpan>(0); + return this.InternalGetClassificationSpans(span, _tagAggregator.GetAllTags(span, cancel)); } #endregion // Exposed Methods diff --git a/src/Text/Impl/Commanding/CommandingStrings.Designer.cs b/src/Text/Impl/Commanding/CommandingStrings.Designer.cs index cfaadfd..714059e 100644 --- a/src/Text/Impl/Commanding/CommandingStrings.Designer.cs +++ b/src/Text/Impl/Commanding/CommandingStrings.Designer.cs @@ -61,16 +61,7 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation { } /// <summary> - /// Looks up a localized string similar to Executing a command. - /// </summary> - internal static string ExecutingCommand { - get { - return ResourceManager.GetString("ExecutingCommand", resourceCulture); - } - } - - /// <summary> - /// Looks up a localized string similar to Please wait for command execution to finish.... + /// Looks up a localized string similar to Please wait for an editor command to finish.... /// </summary> internal static string WaitForCommandExecution { get { diff --git a/src/Text/Impl/Commanding/CommandingStrings.resx b/src/Text/Impl/Commanding/CommandingStrings.resx index 72d604d..b05561d 100644 --- a/src/Text/Impl/Commanding/CommandingStrings.resx +++ b/src/Text/Impl/Commanding/CommandingStrings.resx @@ -98,10 +98,7 @@ <resheader name="writer"> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> - <data name="ExecutingCommand" xml:space="preserve"> - <value>Executing a command</value> - </data> <data name="WaitForCommandExecution" xml:space="preserve"> - <value>Please wait for command execution to finish...</value> + <value>Please wait for an editor command to finish...</value> </data> </root>
\ No newline at end of file diff --git a/src/Text/Impl/Commanding/EditorCommandHandlerService.cs b/src/Text/Impl/Commanding/EditorCommandHandlerService.cs index 3bd08c2..315e55f 100644 --- a/src/Text/Impl/Commanding/EditorCommandHandlerService.cs +++ b/src/Text/Impl/Commanding/EditorCommandHandlerService.cs @@ -25,6 +25,7 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation private readonly static IReadOnlyList<ICommandHandlerAndMetadata> EmptyHandlerList = new List<ICommandHandlerAndMetadata>(0); private readonly static Action EmptyAction = delegate { }; private readonly static Func<CommandState> UnavalableCommandFunc = new Func<CommandState>(() => CommandState.Unavailable); + private readonly static string WaitForCommandExecutionString = CommandingStrings.WaitForCommandExecution; /// This dictionary acts as a cache so we can avoid having to look through the full list of /// handlers every time we need handlers of a specific type, for a given content type. @@ -128,7 +129,7 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation commandExecutionContext = CreateCommandExecutionContext(); } - handlerChain = () => handler.ExecuteCommand(args, nextHandler, commandExecutionContext); + handlerChain = () => _guardedOperations.CallExtensionPoint(handler, () => handler.ExecuteCommand(args, nextHandler, commandExecutionContext)); } ExecuteCommandHandlerChain(commandExecutionContext, handlerChain, nextCommandHandler); @@ -153,7 +154,7 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation } finally { - commandExecutionContext?.WaitContext?.Dispose(); + commandExecutionContext?.OperationContext?.Dispose(); } } @@ -181,8 +182,8 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation private CommandExecutionContext CreateCommandExecutionContext() { CommandExecutionContext commandExecutionContext; - var uiThreadOperationContext = _uiThreadOperationExecutor.BeginExecute(CommandingStrings.ExecutingCommand, - CommandingStrings.WaitForCommandExecution, allowCancel: true, showProgress: true); + var uiThreadOperationContext = _uiThreadOperationExecutor.BeginExecute(title: null, // We want same caption as the main window + defaultDescription: WaitForCommandExecutionString, allowCancellation: true, showProgress: true); commandExecutionContext = new CommandExecutionContext(uiThreadOperationContext); return commandExecutionContext; } diff --git a/src/Text/Impl/Outlining/OutliningManager.cs b/src/Text/Impl/Outlining/OutliningManager.cs index ba20323..71418ec 100644 --- a/src/Text/Impl/Outlining/OutliningManager.cs +++ b/src/Text/Impl/Outlining/OutliningManager.cs @@ -413,7 +413,9 @@ namespace Microsoft.VisualStudio.Text.Outlining { // TODO: Notify providers somehow. // Or rewrite so that such things are legal. +#if false Debug.WriteLine("IGNORING TAG " + spans[0] + " due to span conflict"); +#endif } else { @@ -422,7 +424,9 @@ namespace Microsoft.VisualStudio.Text.Outlining } else { +#if false Debug.WriteLine("IGNORING TAG " + tagSpan.Span.GetSpans(editBuffer) + " because it was split or shortened by projection"); +#endif } } @@ -493,9 +497,9 @@ namespace Microsoft.VisualStudio.Text.Outlining return merged; } - #endregion +#endregion - #region Getting collapsibles +#region Getting collapsibles public IEnumerable<ICollapsed> GetCollapsedRegions(SnapshotSpan span) { @@ -635,17 +639,17 @@ namespace Microsoft.VisualStudio.Text.Outlining !collapsedRegionTree.IsPointContainedInANode(regionSpan.End); } - #endregion +#endregion - #region IAccurateOutliningManager methods +#region IAccurateOutliningManager methods public IEnumerable<ICollapsed> CollapseAll(SnapshotSpan span, Predicate<ICollapsible> match, CancellationToken cancel) { return this.InternalCollapseAll(span, match, cancel: cancel); } - #endregion +#endregion - #region IDisposable +#region IDisposable public void Dispose() { @@ -705,10 +709,10 @@ namespace Microsoft.VisualStudio.Text.Outlining } } - #endregion +#endregion } - #region Sorter for sorted lists of collapsibles +#region Sorter for sorted lists of collapsibles class CollapsibleSorter : IComparer<ICollapsible> { private ITextBuffer SourceBuffer { get; set; } @@ -737,5 +741,5 @@ namespace Microsoft.VisualStudio.Text.Outlining return -left.Length.CompareTo(right.Length); } } - #endregion +#endregion } diff --git a/src/Text/Util/TextDataUtil/GuardedOperations.cs b/src/Text/Util/TextDataUtil/GuardedOperations.cs index 8cc4415..4a9ff53 100644 --- a/src/Text/Util/TextDataUtil/GuardedOperations.cs +++ b/src/Text/Util/TextDataUtil/GuardedOperations.cs @@ -372,7 +372,7 @@ namespace Microsoft.VisualStudio.Text.Utilities { try { - BeforeCallingEventHandler(call); + BeforeCallingExtensionPoint(errorSource ?? call); call(); } catch (Exception e) @@ -381,7 +381,7 @@ namespace Microsoft.VisualStudio.Text.Utilities } finally { - AfterCallingEventHandler(call); + AfterCallingExtensionPoint(errorSource ?? call); } } @@ -389,7 +389,7 @@ namespace Microsoft.VisualStudio.Text.Utilities { try { - BeforeCallingEventHandler(call); + BeforeCallingExtensionPoint(errorSource ?? call); return call(); } catch (Exception e) @@ -400,8 +400,8 @@ namespace Microsoft.VisualStudio.Text.Utilities } finally { - AfterCallingEventHandler(call); - } + AfterCallingExtensionPoint(errorSource ?? call); + } } public void CallExtensionPoint(Action call) @@ -522,6 +522,26 @@ namespace Microsoft.VisualStudio.Text.Utilities } } + private void AfterCallingExtensionPoint(object extensionPoint) + { + if (PerfTrackers.Count == 0) + { + return; + } + + foreach (var perfTracker in PerfTrackers) + { + try + { + perfTracker.AfterCallingExtension(extensionPoint); + } + catch (Exception e) + { + HandleException(perfTracker, e); + } + } + } + private void BeforeCallingEventHandler(Delegate handler) { if (PerfTrackers.Count == 0) @@ -542,6 +562,26 @@ namespace Microsoft.VisualStudio.Text.Utilities } } + private void BeforeCallingExtensionPoint(object extensionPoint) + { + if (PerfTrackers.Count == 0) + { + return; + } + + foreach (var perfTracker in PerfTrackers) + { + try + { + perfTracker.BeforeCallingExtension(extensionPoint); + } + catch (Exception e) + { + HandleException(perfTracker, e); + } + } + } + public void HandleException(object errorSource, Exception e) { bool handled = false; |