diff --git a/src/Features/Core/SolutionCrawler/IDocumentTrackingService.cs b/src/Features/Core/SolutionCrawler/IDocumentTrackingService.cs index d415d6954807f..de959652bb63b 100644 --- a/src/Features/Core/SolutionCrawler/IDocumentTrackingService.cs +++ b/src/Features/Core/SolutionCrawler/IDocumentTrackingService.cs @@ -20,5 +20,12 @@ internal interface IDocumentTrackingService : IWorkspaceService ImmutableArray GetVisibleDocuments(); event EventHandler ActiveDocumentChanged; + + /// + /// Events for Non Roslyn text buffer changes. + /// + /// It raises events for buffers opened in a view in host. + /// + event EventHandler NonRoslynBufferTextChanged; } } diff --git a/src/Features/Core/SolutionCrawler/WorkCoordinator.GlobalOperationAwareIdleProcessor.cs b/src/Features/Core/SolutionCrawler/WorkCoordinator.GlobalOperationAwareIdleProcessor.cs index 115d4832d2865..b5eb00d41b479 100644 --- a/src/Features/Core/SolutionCrawler/WorkCoordinator.GlobalOperationAwareIdleProcessor.cs +++ b/src/Features/Core/SolutionCrawler/WorkCoordinator.GlobalOperationAwareIdleProcessor.cs @@ -37,12 +37,36 @@ public GlobalOperationAwareIdleProcessor( base(listener, backOffTimeSpanInMs, shutdownToken) { this.Processor = processor; + _globalOperation = null; _globalOperationTask = SpecializedTasks.EmptyTask; _globalOperationNotificationService = globalOperationNotificationService; _globalOperationNotificationService.Started += OnGlobalOperationStarted; _globalOperationNotificationService.Stopped += OnGlobalOperationStopped; + + if (this.Processor._documentTracker != null) + { + this.Processor._documentTracker.NonRoslynBufferTextChanged += OnNonRoslynBufferTextChanged; + } + } + + private void OnNonRoslynBufferTextChanged(object sender, EventArgs e) + { + // There are 2 things incremental processor takes care of + // + // #1 is making sure we delay processing any work until there is enough idle (ex, typing) in host. + // #2 is managing cancellation and pending works. + // + // we used to do #1 and #2 only for Roslyn files. and that is usually fine since most of time solution contains only roslyn files. + // + // but for mixed solution (ex, Roslyn files + HTML + JS + CSS), #2 still makes sense but #1 doesn't. We want + // to pause any work while something is going on in other project types as well. + // + // we need to make sure we play nice with neighbors as well. + // + // now, we don't care where changes are coming from. if there is any change in host, we puase oursevles for a while. + this.UpdateLastAccessTime(); } protected Task GlobalOperationTask @@ -114,6 +138,11 @@ public virtual void Shutdown() { _globalOperationNotificationService.Started -= OnGlobalOperationStarted; _globalOperationNotificationService.Stopped -= OnGlobalOperationStopped; + + if (this.Processor._documentTracker != null) + { + this.Processor._documentTracker.NonRoslynBufferTextChanged -= OnNonRoslynBufferTextChanged; + } } } } diff --git a/src/VisualStudio/Core/Def/Implementation/ProjectSystem/DocumentProvider.cs b/src/VisualStudio/Core/Def/Implementation/ProjectSystem/DocumentProvider.cs index 46b06d361d57b..4ed87555db7dd 100644 --- a/src/VisualStudio/Core/Def/Implementation/ProjectSystem/DocumentProvider.cs +++ b/src/VisualStudio/Core/Def/Implementation/ProjectSystem/DocumentProvider.cs @@ -266,12 +266,30 @@ private void OnBeforeDocumentWindowShow(IVsWindowFrame frame, uint docCookie, bo { OnBeforeDocumentWindowShow(frame, id, firstShow); } + + if (ids.Count == 0) + { + // deal with non roslyn text file opened in the editor + var buffer = TryGetTextBufferFromDocData(RunningDocumentTable.GetDocumentData(docCookie)); + if (buffer != null) + { + OnBeforeNonRoslynDocumentWindowShow(buffer, firstShow); + } + } } protected virtual void OnBeforeDocumentWindowShow(IVsWindowFrame frame, DocumentId id, bool firstShow) { } + protected virtual void OnBeforeNonRoslynDocumentWindowShow(ITextBuffer buffer, bool firstShow) + { + } + + protected virtual void OnBeforeNonRoslynDocumentClose(ITextBuffer buffer) + { + } + private IList GetDocumentIdsFromDocCookie(uint docCookie) { List documentKeys; @@ -300,35 +318,44 @@ private IList GetDocumentIdsFromDocCookie(uint docCookie) private void CloseDocuments(uint docCookie, string monikerToKeep) { List documentKeys; - if (_docCookiesToOpenDocumentKeys.TryGetValue(docCookie, out documentKeys)) + if (!_docCookiesToOpenDocumentKeys.TryGetValue(docCookie, out documentKeys)) { - // We will remove from documentKeys the things we successfully closed, - // so clone the list so we can mutate while enumerating - var documentsToClose = documentKeys.Where(key => !StringComparer.OrdinalIgnoreCase.Equals(key.Moniker, monikerToKeep)).ToList(); - - // For a given set of open linked or shared files, we may be closing one of the - // documents (e.g. excluding a linked file from one of its owning projects or - // unloading one of the head projects for a shared project) or the entire set of - // documents (e.g. closing the tab of a shared document). If the entire set of - // documents is closing, then we should avoid the process of updating the active - // context document between the closing of individual documents in the set. In the - // case of closing the tab of a shared document, this avoids updating the shared - // item context hierarchy for the entire shared project to head project owning the - // last documentKey in this list. - var updateActiveContext = documentsToClose.Count == 1; - - foreach (var documentKey in documentsToClose) + // let others know about non roslyn document close + var buffer = TryGetTextBufferFromDocData(RunningDocumentTable.GetDocumentData(docCookie)); + if (buffer != null) { - var document = _documentMap[documentKey]; - document.ProcessClose(updateActiveContext); - Contract.ThrowIfFalse(documentKeys.Remove(documentKey)); + OnBeforeNonRoslynDocumentClose(buffer); } - // If we removed all the keys, then remove the list entirely - if (documentKeys.Count == 0) - { - _docCookiesToOpenDocumentKeys.Remove(docCookie); - } + return; + } + + // We will remove from documentKeys the things we successfully closed, + // so clone the list so we can mutate while enumerating + var documentsToClose = documentKeys.Where(key => !StringComparer.OrdinalIgnoreCase.Equals(key.Moniker, monikerToKeep)).ToList(); + + // For a given set of open linked or shared files, we may be closing one of the + // documents (e.g. excluding a linked file from one of its owning projects or + // unloading one of the head projects for a shared project) or the entire set of + // documents (e.g. closing the tab of a shared document). If the entire set of + // documents is closing, then we should avoid the process of updating the active + // context document between the closing of individual documents in the set. In the + // case of closing the tab of a shared document, this avoids updating the shared + // item context hierarchy for the entire shared project to head project owning the + // last documentKey in this list. + var updateActiveContext = documentsToClose.Count == 1; + + foreach (var documentKey in documentsToClose) + { + var document = _documentMap[documentKey]; + document.ProcessClose(updateActiveContext); + Contract.ThrowIfFalse(documentKeys.Remove(documentKey)); + } + + // If we removed all the keys, then remove the list entirely + if (documentKeys.Count == 0) + { + _docCookiesToOpenDocumentKeys.Remove(docCookie); } } diff --git a/src/VisualStudio/Core/Def/Implementation/Workspace/VisualStudioDocumentTrackingService.cs b/src/VisualStudio/Core/Def/Implementation/Workspace/VisualStudioDocumentTrackingService.cs index bc2905c0933ce..b216ef52fd2d0 100644 --- a/src/VisualStudio/Core/Def/Implementation/Workspace/VisualStudioDocumentTrackingService.cs +++ b/src/VisualStudio/Core/Def/Implementation/Workspace/VisualStudioDocumentTrackingService.cs @@ -1,12 +1,16 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Runtime.InteropServices; using System.Text; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Editor.Shared.Utilities; +using Microsoft.VisualStudio.ComponentModelHost; using Microsoft.VisualStudio.Shell.Interop; +using Microsoft.VisualStudio.Text; using Roslyn.Utilities; namespace Microsoft.VisualStudio.LanguageServices.Implementation @@ -14,6 +18,8 @@ namespace Microsoft.VisualStudio.LanguageServices.Implementation [DebuggerDisplay("{GetDebuggerDisplay(),nq}")] internal class VisualStudioDocumentTrackingService : IDocumentTrackingService, IVsSelectionEvents, IDisposable { + private readonly NonRoslynTextBufferTracker _tracker; + private IVsMonitorSelection _monitorSelection; private uint _cookie; private ImmutableList _visibleFrames; @@ -21,6 +27,8 @@ internal class VisualStudioDocumentTrackingService : IDocumentTrackingService, I public VisualStudioDocumentTrackingService(IServiceProvider serviceProvider) { + _tracker = new NonRoslynTextBufferTracker(this); + _visibleFrames = ImmutableList.Empty; _monitorSelection = (IVsMonitorSelection)serviceProvider.GetService(typeof(SVsShellMonitorSelection)); @@ -132,6 +140,18 @@ public int OnCmdUIContextChanged([ComAliasName("Microsoft.VisualStudio.Shell.Int return VSConstants.E_NOTIMPL; } + public event EventHandler NonRoslynBufferTextChanged; + + public void OnNonRoslynBufferOpened(ITextBuffer buffer) + { + _tracker.OnOpened(buffer); + } + + public void OnNonRoslynBufferClosed(ITextBuffer buffer) + { + _tracker.OnClosed(buffer); + } + public void Dispose() { if (_cookie != VSConstants.VSCOOKIE_NIL && _monitorSelection != null) @@ -262,5 +282,51 @@ internal string GetDebuggerDisplay() return caption.ToString(); } } + + /// + /// It tracks non roslyn text buffer text changes. + /// + private class NonRoslynTextBufferTracker : ForegroundThreadAffinitizedObject + { + private readonly VisualStudioDocumentTrackingService _owner; + private readonly HashSet _buffers; + + public NonRoslynTextBufferTracker(VisualStudioDocumentTrackingService owner) + { + _owner = owner; + _buffers = new HashSet(); + } + + public void OnOpened(ITextBuffer buffer) + { + AssertIsForeground(); + + if (_buffers.Contains(buffer)) + { + return; + } + + _buffers.Add(buffer); + buffer.PostChanged += OnTextChanged; + } + + public void OnClosed(ITextBuffer buffer) + { + AssertIsForeground(); + + if (!_buffers.Contains(buffer)) + { + return; + } + + buffer.PostChanged -= OnTextChanged; + _buffers.Remove(buffer); + } + + private void OnTextChanged(object sender, EventArgs e) + { + _owner.NonRoslynBufferTextChanged?.Invoke(sender, e); + } + } } } diff --git a/src/VisualStudio/Core/Def/RoslynDocumentProvider.cs b/src/VisualStudio/Core/Def/RoslynDocumentProvider.cs index 01f12d350da56..80a1fc4d3507b 100644 --- a/src/VisualStudio/Core/Def/RoslynDocumentProvider.cs +++ b/src/VisualStudio/Core/Def/RoslynDocumentProvider.cs @@ -5,6 +5,7 @@ using Microsoft.VisualStudio.LanguageServices.Implementation; using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem; using Microsoft.VisualStudio.Shell.Interop; +using Microsoft.VisualStudio.Text; namespace Microsoft.VisualStudio.LanguageServices { @@ -23,10 +24,28 @@ public RoslynDocumentProvider( protected override void OnBeforeDocumentWindowShow(IVsWindowFrame frame, DocumentId id, bool firstShow) { - if (_documentTrackingService != null) + base.OnBeforeDocumentWindowShow(frame, id, firstShow); + + _documentTrackingService?.DocumentFrameShowing(frame, id, firstShow); + } + + protected override void OnBeforeNonRoslynDocumentWindowShow(ITextBuffer buffer, bool firstShow) + { + base.OnBeforeNonRoslynDocumentWindowShow(buffer, firstShow); + + if (!firstShow) { - _documentTrackingService.DocumentFrameShowing(frame, id, firstShow); + return; } + + _documentTrackingService?.OnNonRoslynBufferOpened(buffer); + } + + protected override void OnBeforeNonRoslynDocumentClose(ITextBuffer buffer) + { + base.OnBeforeNonRoslynDocumentClose(buffer); + + _documentTrackingService?.OnNonRoslynBufferClosed(buffer); } } }