diff --git a/src/EditorFeatures/CSharp/QuickInfo/SyntacticQuickInfoProvider.cs b/src/EditorFeatures/CSharp/QuickInfo/SyntacticQuickInfoProvider.cs index 729030f62aa8da47f4441c2e082fec466aafa932..3339a8c76887bbe455afa4a53fa4bf0e69590429 100644 --- a/src/EditorFeatures/CSharp/QuickInfo/SyntacticQuickInfoProvider.cs +++ b/src/EditorFeatures/CSharp/QuickInfo/SyntacticQuickInfoProvider.cs @@ -84,7 +84,7 @@ internal class SyntacticQuickInfoProvider : AbstractQuickInfoProvider } var span = new SnapshotSpan(textSnapshot, Span.FromBounds(spanStart, spanEnd)); - return this.CreateElisionBufferDeferredContent(span); + return this.CreateProjectionBufferDeferredContent(span); } private static bool IsScopeBlock(SyntaxNode node) diff --git a/src/EditorFeatures/CSharpTest/QuickInfo/SyntacticQuickInfoSourceTests.cs b/src/EditorFeatures/CSharpTest/QuickInfo/SyntacticQuickInfoSourceTests.cs index ea1fa2108836c3752456401b83d0bc1d3f8e1ee1..40c0de5f52564f68fca961f586a52a22af9fc4f0 100644 --- a/src/EditorFeatures/CSharpTest/QuickInfo/SyntacticQuickInfoSourceTests.cs +++ b/src/EditorFeatures/CSharpTest/QuickInfo/SyntacticQuickInfoSourceTests.cs @@ -292,16 +292,9 @@ private IQuickInfoProvider CreateProvider(TestWorkspace workspace) var state = await provider.GetItemAsync(document, position, cancellationToken: CancellationToken.None); Assert.NotNull(state); - var viewHostingControl = (ViewHostingControl)((ElisionBufferDeferredContent)state.Content).Create(); - try - { - var actualContent = viewHostingControl.ToString(); - Assert.Equal(expectedContent, actualContent); - } - finally - { - viewHostingControl.TextView_TestOnly.Close(); - } + var viewHostingControl = (ViewHostingControl)((ProjectionBufferDeferredContent)state.Content).Create(); + var actualContent = viewHostingControl.GetText_TestOnly(); + Assert.Equal(expectedContent, actualContent); } protected override Task TestInMethodAsync(string code, string expectedContent, string expectedDocumentationComment = null) diff --git a/src/EditorFeatures/Core/EditorFeatures.csproj b/src/EditorFeatures/Core/EditorFeatures.csproj index 6099a662a8abd4ccb4db3dc6ff9ee2992e48df9a..db68ede2853f51f51bbb40116fbff1f916a99722 100644 --- a/src/EditorFeatures/Core/EditorFeatures.csproj +++ b/src/EditorFeatures/Core/EditorFeatures.csproj @@ -573,7 +573,7 @@ - + diff --git a/src/EditorFeatures/Core/Implementation/IntelliSense/QuickInfo/DeferredContent/ElisionBufferDeferredContent.cs b/src/EditorFeatures/Core/Implementation/IntelliSense/QuickInfo/DeferredContent/ProjectionBufferDeferredContent.cs similarity index 87% rename from src/EditorFeatures/Core/Implementation/IntelliSense/QuickInfo/DeferredContent/ElisionBufferDeferredContent.cs rename to src/EditorFeatures/Core/Implementation/IntelliSense/QuickInfo/DeferredContent/ProjectionBufferDeferredContent.cs index ed6ead8bcf5f21a2373827f2669335498130e81a..87e7080a40bed7b3529905c3d29484d3a61cc5cf 100644 --- a/src/EditorFeatures/Core/Implementation/IntelliSense/QuickInfo/DeferredContent/ElisionBufferDeferredContent.cs +++ b/src/EditorFeatures/Core/Implementation/IntelliSense/QuickInfo/DeferredContent/ProjectionBufferDeferredContent.cs @@ -14,10 +14,10 @@ namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.QuickInfo { /// /// Creates quick info content out of the span of an existing snapshot. The span will be - /// used to create an elision buffer out that will then be displayed in the quick info + /// used to create an projection buffer out that will then be displayed in the quick info /// window. /// - internal class ElisionBufferDeferredContent : IDeferredQuickInfoContent + internal class ProjectionBufferDeferredContent : IDeferredQuickInfoContent { private readonly SnapshotSpan _span; private readonly IProjectionBufferFactoryService _projectionBufferFactoryService; @@ -26,7 +26,7 @@ internal class ElisionBufferDeferredContent : IDeferredQuickInfoContent private readonly IContentType _contentType; private readonly ITextViewRoleSet _roleSet; - public ElisionBufferDeferredContent( + public ProjectionBufferDeferredContent( SnapshotSpan span, IProjectionBufferFactoryService projectionBufferFactoryService, IEditorOptionsFactoryService editorOptionsFactoryService, @@ -63,9 +63,9 @@ private IWpfTextView CreateView(ITextBuffer buffer) return view; } - private IElisionBuffer CreateBuffer() + private IProjectionBuffer CreateBuffer() { - return _projectionBufferFactoryService.CreateElisionBufferWithoutIndentation( + return _projectionBufferFactoryService.CreateProjectionBufferWithoutIndentation( _editorOptionsFactoryService.GlobalOptions, _contentType, _span); } } diff --git a/src/EditorFeatures/Core/Implementation/IntelliSense/QuickInfo/Providers/AbstractQuickInfoProvider.cs b/src/EditorFeatures/Core/Implementation/IntelliSense/QuickInfo/Providers/AbstractQuickInfoProvider.cs index 01394b77c401073981a50b9fe5dcb8b943a49c6d..d5068d52c9b51c47b7fb77c2029c3196abae4f96 100644 --- a/src/EditorFeatures/Core/Implementation/IntelliSense/QuickInfo/Providers/AbstractQuickInfoProvider.cs +++ b/src/EditorFeatures/Core/Implementation/IntelliSense/QuickInfo/Providers/AbstractQuickInfoProvider.cs @@ -151,9 +151,9 @@ protected IDeferredQuickInfoContent CreateGlyphDeferredContent(ISymbol symbol) return new DocumentationCommentDeferredContent(documentationComment, _typeMap); } - protected IDeferredQuickInfoContent CreateElisionBufferDeferredContent(SnapshotSpan span) + protected IDeferredQuickInfoContent CreateProjectionBufferDeferredContent(SnapshotSpan span) { - return new ElisionBufferDeferredContent( + return new ProjectionBufferDeferredContent( span, _projectionBufferFactoryService, _editorOptionsFactoryService, _textEditorFactoryService); } } diff --git a/src/EditorFeatures/Core/Implementation/Structure/BlockTagState.cs b/src/EditorFeatures/Core/Implementation/Structure/BlockTagState.cs index 424449fd8d27f20591639a6c3daa53ad4da56f64..9b9d87342719bb4d2ba759bd2677d152d105ed2d 100644 --- a/src/EditorFeatures/Core/Implementation/Structure/BlockTagState.cs +++ b/src/EditorFeatures/Core/Implementation/Structure/BlockTagState.cs @@ -156,7 +156,7 @@ private Span TrimStartingNewlines(Span span) private ITextBuffer CreateElisionBufferWithoutIndentation( ITextBuffer dataBuffer, Span shortHintSpan) { - return _projectionBufferFactoryService.CreateElisionBufferWithoutIndentation( + return _projectionBufferFactoryService.CreateProjectionBufferWithoutIndentation( _editorOptionsFactoryService.GlobalOptions, contentType: null, exposedSpans: new SnapshotSpan(dataBuffer.CurrentSnapshot, shortHintSpan)); diff --git a/src/EditorFeatures/Core/Shared/Extensions/IProjectionBufferFactoryServiceExtensions.cs b/src/EditorFeatures/Core/Shared/Extensions/IProjectionBufferFactoryServiceExtensions.cs index 9938b814b8e252381a2960b8f6cbdea0c704a760..7fac9c5926f037bea9dddc56028c1a597b20f487 100644 --- a/src/EditorFeatures/Core/Shared/Extensions/IProjectionBufferFactoryServiceExtensions.cs +++ b/src/EditorFeatures/Core/Shared/Extensions/IProjectionBufferFactoryServiceExtensions.cs @@ -30,19 +30,19 @@ internal static class IProjectionBufferFactoryServiceExtensions [BaseDefinition("projection")] public static readonly ContentTypeDefinition RoslynPreviewContentTypeDefinition; - public static IElisionBuffer CreateElisionBufferWithoutIndentation( + public static IProjectionBuffer CreateProjectionBufferWithoutIndentation( this IProjectionBufferFactoryService factoryService, IEditorOptions editorOptions, IContentType contentType = null, params SnapshotSpan[] exposedSpans) { - return factoryService.CreateElisionBufferWithoutIndentation( + return factoryService.CreateProjectionBufferWithoutIndentation( editorOptions, contentType, (IEnumerable)exposedSpans); } - public static IElisionBuffer CreateElisionBufferWithoutIndentation( + public static IProjectionBuffer CreateProjectionBufferWithoutIndentation( this IProjectionBufferFactoryService factoryService, IEditorOptions editorOptions, IContentType contentType, @@ -61,37 +61,55 @@ internal static class IProjectionBufferFactoryServiceExtensions } contentType = contentType ?? factoryService.ProjectionContentType; - var elisionBuffer = factoryService.CreateElisionBuffer( - null, spans, ElisionBufferOptions.None, contentType); + var projectionBuffer = factoryService.CreateProjectionBuffer( + projectionEditResolver: null, + sourceSpans: Array.Empty(), + options: ProjectionBufferOptions.None, + contentType: contentType); if (spans.Count > 0) { - var snapshot = spans.First().Snapshot; - var buffer = snapshot.TextBuffer; + var finalSpans = new List(); // We need to figure out the shorted indentation level of the exposed lines. We'll // then remove that indentation from all lines. var indentationColumn = DetermineIndentationColumn(editorOptions, spans); - var spansToElide = new List(); - foreach (var span in spans) { + var snapshot = span.Snapshot; var startLineNumber = snapshot.GetLineNumberFromPosition(span.Start); var endLineNumber = snapshot.GetLineNumberFromPosition(span.End); for (var lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) { + // Compute the span clamped to this line var line = snapshot.GetLineFromLineNumber(lineNumber); - var lineOffsetOfColumn = line.GetLineOffsetFromColumn(indentationColumn, editorOptions); - spansToElide.Add(Span.FromBounds(line.Start, line.Start + lineOffsetOfColumn)); + var finalSpanStart = Math.Max(line.Start, span.Start); + var finalSpanEnd = Math.Min(line.EndIncludingLineBreak, span.End); + + // We'll only offset if our span doesn't already start at the start of the line. See the similar exclusion in + // DetermineIndentationColumn that this matches. + if (line.Start == finalSpanStart) + { + finalSpanStart += line.GetLineOffsetFromColumn(indentationColumn, editorOptions); + + // Paranoia: what if the indentation reversed our ordering? + if (finalSpanStart > finalSpanEnd) + { + finalSpanStart = finalSpanEnd; + } + } + + // We don't expect edits to happen while this projection buffer is active. We'll choose EdgeExclusive so + // if they do we don't end up in any cases where there is overlapping source spans. + finalSpans.Add(snapshot.CreateTrackingSpan(Span.FromBounds(finalSpanStart, finalSpanEnd), SpanTrackingMode.EdgeExclusive)); } } - - elisionBuffer.ElideSpans(new NormalizedSpanCollection(spansToElide)); + projectionBuffer.InsertSpans(0, finalSpans); } - return elisionBuffer; + return projectionBuffer; } private static int DetermineIndentationColumn( diff --git a/src/EditorFeatures/Core/Shared/Utilities/ViewHostingControl.cs b/src/EditorFeatures/Core/Shared/Utilities/ViewHostingControl.cs index 6ded97c51b90a8725eeff85f2476cea621dc28a9..bd925c72306e0b166e72f6c4d5e48afbeca8ab0d 100644 --- a/src/EditorFeatures/Core/Shared/Utilities/ViewHostingControl.cs +++ b/src/EditorFeatures/Core/Shared/Utilities/ViewHostingControl.cs @@ -6,6 +6,7 @@ using System.Windows.Media; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Projection; namespace Microsoft.CodeAnalysis.Editor.Shared.Utilities { @@ -14,6 +15,9 @@ internal class ViewHostingControl : ContentControl private readonly Func _createView; private readonly Func _createBuffer; + private ITextBuffer _createdTextBuffer; + private IWpfTextView _createdView; + public ViewHostingControl( Func createView, Func createBuffer) @@ -25,18 +29,30 @@ internal class ViewHostingControl : ContentControl this.IsVisibleChanged += OnIsVisibleChanged; } + private void EnsureBufferCreated() + { + if (_createdTextBuffer == null) + { + _createdTextBuffer = _createBuffer(); + } + } + + private void EnsureContentCreated() + { + if (this.Content == null) + { + EnsureBufferCreated(); + _createdView = _createView(_createdTextBuffer); + this.Content = _createdView.VisualElement; + } + } + public ITextView TextView_TestOnly { get { - var view = (IWpfTextView)this.Content; - if (view == null) - { - view = _createView(_createBuffer()); - this.Content = view.VisualElement; - } - - return view; + EnsureContentCreated(); + return (ITextView)this.Content; } } @@ -45,26 +61,31 @@ private void OnIsVisibleChanged(object sender, DependencyPropertyChangedEventArg var nowVisible = (bool)e.NewValue; if (nowVisible) { - if (this.Content == null) - { - this.Content = _createView(_createBuffer()).VisualElement; - } + EnsureContentCreated(); } else { - ((ITextView)this.Content).Close(); this.Content = null; + + _createdView.Close(); + _createdView = null; + + // If a projection buffer has a source span from another buffer, the projection buffer is held alive by the other buffer too. + // This means that a one-off projection buffer created for a tooltip would be kept alive as long as the underlying file + // is still open. Removing the source spans from the projection buffer ensures the projection buffer can be GC'ed. + if (_createdTextBuffer is IProjectionBuffer projectionBuffer) + { + projectionBuffer.DeleteSpans(0, projectionBuffer.CurrentSnapshot.SpanCount); + } + + _createdTextBuffer = null; } } - public override string ToString() + public string GetText_TestOnly() { - if (this.Content != null) - { - return ((ITextView)this.Content).TextBuffer.CurrentSnapshot.GetText(); - } - - return _createBuffer().CurrentSnapshot.GetText(); + EnsureBufferCreated(); + return _createdTextBuffer.CurrentSnapshot.GetText(); } } } diff --git a/src/EditorFeatures/Test/Extensions/IProjectionBufferFactoryServiceExtensionsTests.cs b/src/EditorFeatures/Test/Extensions/IProjectionBufferFactoryServiceExtensionsTests.cs index e4bc96bbfe8e33bd12d5ce2cf078d62c7d7bed85..804b20f4ed896b31f821bf17fe552c920ed714a8 100644 --- a/src/EditorFeatures/Test/Extensions/IProjectionBufferFactoryServiceExtensionsTests.cs +++ b/src/EditorFeatures/Test/Extensions/IProjectionBufferFactoryServiceExtensionsTests.cs @@ -27,7 +27,7 @@ public void TestCreateElisionBufferWithoutIndentation() line 2 line 3", contentTypeRegistryService.GetContentType("text")); - var elisionBuffer = IProjectionBufferFactoryServiceExtensions.CreateElisionBufferWithoutIndentation( + var elisionBuffer = IProjectionBufferFactoryServiceExtensions.CreateProjectionBufferWithoutIndentation( exportProvider.GetExportedValue(), exportProvider.GetExportedValue().GlobalOptions, contentType: null, diff --git a/src/EditorFeatures/Test/Structure/StructureTaggerTests.cs b/src/EditorFeatures/Test/Structure/StructureTaggerTests.cs index 526a10697aca25ea5fc5e64626f8d275e5cae677..afae21dd38dee7d934728355f8a2a6fb92120445 100644 --- a/src/EditorFeatures/Test/Structure/StructureTaggerTests.cs +++ b/src/EditorFeatures/Test/Structure/StructureTaggerTests.cs @@ -155,7 +155,7 @@ End Sub var tags = await GetTagsFromWorkspaceAsync(workspace); var hints = tags.Select(x => x.CollapsedHintForm).Cast().ToArray(); - Assert.Equal("Sub Main(args As String())\r\nEnd Sub", hints[1].ToString()); // method + Assert.Equal("Sub Main(args As String())\r\nEnd Sub", hints[1].GetText_TestOnly()); // method hints.Do(v => v.TextView_TestOnly.Close()); } } @@ -180,4 +180,4 @@ private static async Task> GetTagsFromWorkspaceAsync(T return context.tagSpans.Select(x => x.Tag).ToList(); } } -} \ No newline at end of file +} diff --git a/src/VisualStudio/Core/Next/FindReferences/Entries/DocumentSpanEntry.cs b/src/VisualStudio/Core/Next/FindReferences/Entries/DocumentSpanEntry.cs index 9e47ed3ffeb80f2e78654288a75e7fb63e7a5cc5..e83e1406642e6802068fdc3b0b47419ceaceee63 100644 --- a/src/VisualStudio/Core/Next/FindReferences/Entries/DocumentSpanEntry.cs +++ b/src/VisualStudio/Core/Next/FindReferences/Entries/DocumentSpanEntry.cs @@ -140,7 +140,7 @@ private ContentControl CreateToolTipContent(ITextBuffer textBuffer) PredefinedTextViewRoles.Document, PredefinedTextViewRoles.Editable); - var content = new ElisionBufferDeferredContent( + var content = new ProjectionBufferDeferredContent( snapshotSpan, Presenter.ProjectionBufferFactoryService, Presenter.EditorOptionsFactoryService,