diff --git a/src/EditorFeatures/Core/Implementation/Classification/SyntacticClassificationTaggerProvider.TagComputer.cs b/src/EditorFeatures/Core/Implementation/Classification/SyntacticClassificationTaggerProvider.TagComputer.cs index 5d51d40114129e80a23c8950e904a0a596f26436..847566be4a69a979c66042a692ec23e71b598883 100644 --- a/src/EditorFeatures/Core/Implementation/Classification/SyntacticClassificationTaggerProvider.TagComputer.cs +++ b/src/EditorFeatures/Core/Implementation/Classification/SyntacticClassificationTaggerProvider.TagComputer.cs @@ -53,8 +53,8 @@ internal partial class TagComputer // way, when we call into the actual classification service, it should be very quick for the // it to get the tree if it needs it. private readonly object _gate = new object(); - private ITextSnapshot _lastParsedSnapshot; - private Document _lastParsedDocument; + private ITextSnapshot _lastProcessedSnapshot; + private Document _lastProcessedDocument; private Workspace _workspace; private CancellationTokenSource _reportChangeCancellationSource; @@ -134,7 +134,7 @@ private void ResetLastParsedDocument() { lock (_gate) { - _lastParsedDocument = null; + _lastProcessedDocument = null; } } @@ -155,7 +155,7 @@ private void ConnectToWorkspace(Workspace workspace) var document = workspace.CurrentSolution.GetDocument(documentId); if (document != null) { - EnqueueParseSnapshotTask(document); + EnqueueProcessSnapshotAsync(document); } } } @@ -176,31 +176,15 @@ public void DisconnectFromWorkspace() } } - private void EnqueueParseSnapshotTask(Document newDocument) + private void EnqueueProcessSnapshotAsync(Document newDocument) { - // When renaming a file's extension through VS when it's opened in editor, - // the content type might change and the content type changed event can be - // raised before the renaming propagate through VS workspace. As a result, - // the document we got (based on the buffer) could still be the one in the workspace - // before rename happened. This would cause us problem if the document is supported - // by workspace but not a roslyn language (e.g. xaml, F#, etc.), since none of the roslyn - // language services would be available. - // - // If this is the case, we will not parse the snapshot. It's OK to ignore the request - // because when the buffer eventually get associated with the correct document in roslyn - // workspace, we will be invoked again. - // - // For example, if you open a xaml from from a WPF project in designer view, - // and then rename file extension from .xaml to .cs, then the document we received - // here would still belong to the special "-xaml" project. - - if (newDocument != null && newDocument.SupportsSyntaxTree) - { - _workQueue.EnqueueBackgroundTask(c => this.EnqueueParseSnapshotWorkerAsync(newDocument, c), GetType() + ".EnqueueParseSnapshotTask.1", CancellationToken.None); + if (newDocument != null) + { + _workQueue.EnqueueBackgroundTask(c => this.EnqueueProcessSnapshotWorkerAsync(newDocument, c), GetType() + ".EnqueueParseSnapshotTask.1", CancellationToken.None); } } - private async Task EnqueueParseSnapshotWorkerAsync(Document document, CancellationToken cancellationToken) + private async Task EnqueueProcessSnapshotWorkerAsync(Document document, CancellationToken cancellationToken) { // we will enqueue new one soon, cancel pending refresh right away _reportChangeCancellationSource.Cancel(); @@ -216,11 +200,13 @@ private async Task EnqueueParseSnapshotWorkerAsync(Document document, Cancellati } // preemptively parse file in background so that when we are called from tagger from UI thread, we have tree ready. - var syntaxTree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + // F#/typescript and other languages that doesn't support syntax tree will return null here. + _ = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + lock (_gate) { - _lastParsedSnapshot = snapshot; - _lastParsedDocument = document; + _lastProcessedSnapshot = snapshot; + _lastProcessedDocument = document; } _reportChangeCancellationSource = new CancellationTokenSource(); @@ -238,7 +224,7 @@ private void ReportChangedSpan(SnapshotSpan changeSpan) { lock (_gate) { - var snapshot = _lastParsedSnapshot; + var snapshot = _lastProcessedSnapshot; if (snapshot.Version.ReiteratedVersionNumber != changeSpan.Snapshot.Version.ReiteratedVersionNumber) { // wait for next call @@ -309,8 +295,8 @@ public IEnumerable> GetTags(NormalizedSnapshotSpanC lock (_gate) { - lastSnapshot = _lastParsedSnapshot; - lastDocument = _lastParsedDocument; + lastSnapshot = _lastProcessedSnapshot; + lastDocument = _lastProcessedDocument; } if (lastDocument == null) @@ -448,7 +434,7 @@ private void OnDocumentActiveContextChanged(object sender, DocumentActiveContext { if (_workspace != null && _workspace == args.Solution.Workspace) { - ParseIfThisDocument(args.Solution, args.NewActiveContextDocumentId); + ProcessIfThisDocument(args.Solution, args.NewActiveContextDocumentId); } } @@ -456,7 +442,7 @@ private void OnDocumentOpened(object sender, DocumentEventArgs args) { if (_workspace != null) { - ParseIfThisDocument(args.Document.Project.Solution, args.Document.Id); + ProcessIfThisDocument(args.Document.Project.Solution, args.Document.Id); } } @@ -485,13 +471,13 @@ private void OnWorkspaceChanged(object sender, WorkspaceChangeEventArgs args) // make sure in case of parse config change, we re-colorize whole document. not just edited section. var configChanged = !object.Equals(oldProject.ParseOptions, newProject.ParseOptions); - EnqueueParseSnapshotTask(newProject.GetDocument(documentId)); + EnqueueProcessSnapshotAsync(newProject.GetDocument(documentId)); break; } case WorkspaceChangeKind.DocumentChanged: { - ParseIfThisDocument(args.NewSolution, args.DocumentId); + ProcessIfThisDocument(args.NewSolution, args.DocumentId); break; } } @@ -507,7 +493,7 @@ private void OnWorkspaceChanged(object sender, WorkspaceChangeEventArgs args) private async Task UpdateLastParsedDocumentAsync(Solution newSolution, CancellationToken cancellationToken) { // lastParsedDocument only updated in the same sequential queue so don't need lock to use it - var lastDocument = Volatile.Read(ref _lastParsedDocument); + var lastDocument = Volatile.Read(ref _lastProcessedDocument); if (lastDocument == null) { return; @@ -549,7 +535,7 @@ private async Task UpdateLastParsedDocumentAsync(Solution newSolution, Cancellat // update document to new snapshot with same content lock (_gate) { - _lastParsedDocument = document; + _lastProcessedDocument = document; } } else @@ -559,18 +545,18 @@ private async Task UpdateLastParsedDocumentAsync(Solution newSolution, Cancellat // or some other workspace change (say a SolutionChanged) caused a text edit to happen and we didn't process // it directly. In that case, requeue a parse. This might be a redundant parse in the linked file case // since we might also get a DocumentChanged event for our ID. It's fine. - ParseIfThisDocument(newSolution, document.Id); + ProcessIfThisDocument(newSolution, document.Id); } } - private void ParseIfThisDocument(Solution newSolution, DocumentId documentId) + private void ProcessIfThisDocument(Solution newSolution, DocumentId documentId) { if (_workspace != null) { var openDocumentId = _workspace.GetDocumentIdInCurrentContext(_subjectBuffer.AsTextContainer()); if (openDocumentId == documentId) { - EnqueueParseSnapshotTask(newSolution.GetDocument(documentId)); + EnqueueProcessSnapshotAsync(newSolution.GetDocument(documentId)); } } } diff --git a/src/EditorFeatures/Test2/Classification/ClassificationTests.vb b/src/EditorFeatures/Test2/Classification/ClassificationTests.vb index 329ce6d599bf40da6c014ff97bd2c2b247ac0e1e..13f4b075f5338122adc1dfe2953b4a354ca0dec8 100644 --- a/src/EditorFeatures/Test2/Classification/ClassificationTests.vb +++ b/src/EditorFeatures/Test2/Classification/ClassificationTests.vb @@ -82,6 +82,37 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.Classification Assert.NotNull(run) End Sub + + Public Async Function TestWrongDocument() As Task + Dim workspaceDefinition = + + + + var x = {}; // e.g., TypeScript code or anything else that doesn't support compilations + + + + + + + Dim exportProvider = ExportProviderCache _ + .GetOrCreateExportProviderFactory(TestExportProvider.EntireAssemblyCatalogWithCSharpAndVisualBasic()) _ + .CreateExportProvider() + + Using workspace = TestWorkspace.Create(workspaceDefinition, exportProvider:=exportProvider) + Dim project = workspace.CurrentSolution.Projects.First(Function(p) p.Language = LanguageNames.CSharp) + Dim classificationService = project.LanguageServices.GetService(Of IClassificationService)() + + Dim wrongDocument = workspace.CurrentSolution.Projects.First(Function(p) p.Language = "NoCompilation").Documents.First() + Dim text = Await wrongDocument.GetTextAsync(CancellationToken.None) + + ' make sure we don't crash with wrong document + Dim result = New List(Of ClassifiedSpan)() + Await classificationService.AddSyntacticClassificationsAsync(wrongDocument, New TextSpan(0, text.Length), result, CancellationToken.None) + Await classificationService.AddSemanticClassificationsAsync(wrongDocument, New TextSpan(0, text.Length), result, CancellationToken.None) + End Using + End Function + #Disable Warning BC40000 ' Type or member is obsolete Private Class NoCompilationEditorClassificationService diff --git a/src/Workspaces/Core/Portable/Classification/AbstractClassificationService.cs b/src/Workspaces/Core/Portable/Classification/AbstractClassificationService.cs index e1fc3fdd02654ae3eb3d840f7ac8d99987e40888..09049f827678dba885a736b26e252c84e582fda4 100644 --- a/src/Workspaces/Core/Portable/Classification/AbstractClassificationService.cs +++ b/src/Workspaces/Core/Portable/Classification/AbstractClassificationService.cs @@ -19,6 +19,25 @@ internal abstract class AbstractClassificationService : IClassificationService public async Task AddSemanticClassificationsAsync(Document document, TextSpan textSpan, List result, CancellationToken cancellationToken) { var classificationService = document.GetLanguageService(); + if (classificationService == null) + { + // When renaming a file's extension through VS when it's opened in editor, + // the content type might change and the content type changed event can be + // raised before the renaming propagate through VS workspace. As a result, + // the document we got (based on the buffer) could still be the one in the workspace + // before rename happened. This would cause us problem if the document is supported + // by workspace but not a roslyn language (e.g. xaml, F#, etc.), since none of the roslyn + // language services would be available. + // + // If this is the case, we will simply bail out. It's OK to ignore the request + // because when the buffer eventually get associated with the correct document in roslyn + // workspace, we will be invoked again. + // + // For example, if you open a xaml from from a WPF project in designer view, + // and then rename file extension from .xaml to .cs, then the document we received + // here would still belong to the special "-xaml" project. + return; + } var extensionManager = document.Project.Solution.Workspace.Services.GetService(); var classifiers = classificationService.GetDefaultSyntaxClassifiers(); @@ -35,6 +54,26 @@ public async Task AddSemanticClassificationsAsync(Document document, TextSpan te public async Task AddSyntacticClassificationsAsync(Document document, TextSpan textSpan, List result, CancellationToken cancellationToken) { var classificationService = document.GetLanguageService(); + if (classificationService == null) + { + // When renaming a file's extension through VS when it's opened in editor, + // the content type might change and the content type changed event can be + // raised before the renaming propagate through VS workspace. As a result, + // the document we got (based on the buffer) could still be the one in the workspace + // before rename happened. This would cause us problem if the document is supported + // by workspace but not a roslyn language (e.g. xaml, F#, etc.), since none of the roslyn + // language services would be available. + // + // If this is the case, we will simply bail out. It's OK to ignore the request + // because when the buffer eventually get associated with the correct document in roslyn + // workspace, we will be invoked again. + // + // For example, if you open a xaml from from a WPF project in designer view, + // and then rename file extension from .xaml to .cs, then the document we received + // here would still belong to the special "-xaml" project. + return; + } + var syntaxTree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); var temp = ArrayBuilder.GetInstance();