|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Tagging;
using Microsoft.CodeAnalysis.Editor.Tagging;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.MetadataAsSource;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Structure;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Text.Shared.Extensions;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Projection;
using Microsoft.VisualStudio.Text.Tagging;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.Editor.Implementation.Structure;
/// <summary>
/// Shared implementation of the outliner tagger provider.
///
/// Note: the outliner tagger is a normal buffer tagger provider and not a view tagger provider.
/// This is important for two reasons. The first is that if it were view-based then we would lose
/// the state of the collapsed/open regions when they scrolled in and out of view. Also, if the
/// editor doesn't know about all the regions in the file, then it wouldn't be able to
/// persist them to the SUO file to persist this data across sessions.
/// </summary>
#pragma warning disable CS0618 // Type or member is obsolete
internal abstract partial class AbstractStructureTaggerProvider(
TaggerHost taggerHost,
EditorOptionsService editorOptionsService,
IProjectionBufferFactoryService projectionBufferFactoryService)
: AsynchronousTaggerProvider<IContainerStructureTag>(taggerHost, FeatureAttribute.Outlining)
{
private const string RegionDirective = "#region";
private const string UsingDirective = "using";
private const string ExternDeclaration = "extern";
private const string ImportsStatement = "Imports";
protected readonly EditorOptionsService EditorOptionsService = editorOptionsService;
protected readonly IProjectionBufferFactoryService ProjectionBufferFactoryService = projectionBufferFactoryService;
protected override TaggerDelay EventChangeDelay => TaggerDelay.OnIdle;
protected override bool ComputeInitialTagsSynchronously(ITextBuffer subjectBuffer)
{
// If we can't find this doc, or outlining is not enabled for it, no need to computed anything synchronously.
var openDocument = subjectBuffer.AsTextContainer().GetRelatedDocuments().FirstOrDefault();
if (openDocument == null)
return false;
// If the main Outlining option is turned off, we can just skip computing tags synchronously
// so when the document first opens, there won't be any tags yet. When the tags do come in
// the IsDefaultCollapsed property, which controls the initial collapsing, won't have any effect
// because the document will already be open.
if (!GlobalOptions.GetOption(OutliningOptionsStorage.Outlining, openDocument.Project.Language))
return false;
var options = BlockStructureOptionsStorage.GetBlockStructureOptions(GlobalOptions, openDocument.Project);
// If we're a metadata-as-source doc, we need to compute the initial set of tags synchronously
// so that we can collapse all the .IsImplementation tags to keep the UI clean and condensed.
if (openDocument.Project.Solution.Workspace is MetadataAsSourceWorkspace masWorkspace &&
masWorkspace.FileService.ShouldCollapseOnOpen(openDocument.FilePath, options))
{
return true;
}
// If the user wants to collapse imports or #regions then we need to compute
// synchronously, but only if there are imports or #regions in the file. To
// save some work, we'll look for both in a single pass.
var collapseRegions = GlobalOptions.GetOption(BlockStructureOptionsStorage.CollapseRegionsWhenFirstOpened, openDocument.Project.Language);
var collapseImports = GlobalOptions.GetOption(BlockStructureOptionsStorage.CollapseImportsWhenFirstOpened, openDocument.Project.Language);
if (!collapseRegions && !collapseImports)
{
return false;
}
if (ContainsRegionOrImport(subjectBuffer.CurrentSnapshot, collapseRegions, collapseImports, openDocument.Project.Language))
{
return true;
}
return false;
}
// Internal for testing
internal static bool ContainsRegionOrImport(ITextSnapshot textSnapshot, bool collapseRegions, bool collapseImports, string language)
{
foreach (var line in textSnapshot.Lines)
{
if (collapseRegions && StartsWithRegionTag(line))
{
return true;
}
else if (collapseImports && IsImport(line, language))
{
return true;
}
}
return false;
static bool StartsWithRegionTag(ITextSnapshotLine line)
{
var start = line.GetFirstNonWhitespacePosition();
return start != null && line.StartsWith(start.Value, RegionDirective, ignoreCase: true);
}
static bool IsImport(ITextSnapshotLine line, string language)
{
var start = line.GetFirstNonWhitespacePosition();
if (start is null)
return false;
// For VB we only need to find "Imports" at the start of a line
if (language == LanguageNames.VisualBasic)
{
return line.StartsWith(start.Value, ImportsStatement, ignoreCase: true);
}
// For the purposes of collapsing, extern aliases are grouped with usings
if (line.StartsWith(start.Value, ExternDeclaration, ignoreCase: false))
return true;
return line.StartsWith(start.Value, UsingDirective, ignoreCase: false);
}
}
protected sealed override ITaggerEventSource CreateEventSource(ITextView? textView, ITextBuffer subjectBuffer)
{
// We listen to the following events:
// 1) Text changes. These can obviously affect outlining, so we need to recompute when
// we hear about them.
// 2) Parse option changes. These can affect outlining when, for example, we change from
// DEBUG to RELEASE (affecting the inactive/active regions).
// 3) When we hear about a workspace being registered. Outlining may run before a
// we even know about a workspace. This can happen, for example, in the TypeScript
// case. With TypeScript a file is opened, but the workspace is not generated until
// some time later when they have examined the file system. As such, initially,
// the file will not have outline spans. When the workspace is created, we want to
// then produce the right outlining spans.
return TaggerEventSources.Compose(
TaggerEventSources.OnTextChanged(subjectBuffer),
TaggerEventSources.OnParseOptionChanged(subjectBuffer),
TaggerEventSources.OnWorkspaceRegistrationChanged(subjectBuffer),
TaggerEventSources.OnGlobalOptionChanged(GlobalOptions, static option =>
option.Equals(BlockStructureOptionsStorage.ShowBlockStructureGuidesForCodeLevelConstructs) ||
option.Equals(BlockStructureOptionsStorage.ShowBlockStructureGuidesForDeclarationLevelConstructs) ||
option.Equals(BlockStructureOptionsStorage.ShowBlockStructureGuidesForCommentsAndPreprocessorRegions) ||
option.Equals(BlockStructureOptionsStorage.ShowOutliningForCodeLevelConstructs) ||
option.Equals(BlockStructureOptionsStorage.ShowOutliningForDeclarationLevelConstructs) ||
option.Equals(BlockStructureOptionsStorage.ShowOutliningForCommentsAndPreprocessorRegions) ||
option.Equals(BlockStructureOptionsStorage.CollapseRegionsWhenCollapsingToDefinitions) ||
option.Equals(BlockStructureOptionsStorage.CollapseLocalFunctionsWhenCollapsingToDefinitions)));
}
protected sealed override async Task ProduceTagsAsync(
TaggerContext<IContainerStructureTag> context, DocumentSnapshotSpan documentSnapshotSpan, int? caretPosition, CancellationToken cancellationToken)
{
try
{
var document = documentSnapshotSpan.Document;
if (document == null)
return;
// Let LSP handle producing tags in the cloud scenario
if (documentSnapshotSpan.SnapshotSpan.Snapshot.TextBuffer.IsInLspEditorContext())
return;
var outliningService = BlockStructureService.GetService(document);
if (outliningService == null)
return;
var options = GlobalOptions.GetBlockStructureOptions(document.Project);
var blockStructure = await outliningService.GetBlockStructureAsync(
documentSnapshotSpan.Document, options, cancellationToken).ConfigureAwait(false);
ProcessSpans(
context, documentSnapshotSpan.SnapshotSpan, outliningService,
blockStructure.Spans);
}
catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken))
{
throw ExceptionUtilities.Unreachable();
}
}
private void ProcessSpans(
TaggerContext<IContainerStructureTag> context,
SnapshotSpan snapshotSpan,
BlockStructureService outliningService,
ImmutableArray<BlockSpan> spans)
{
var snapshot = snapshotSpan.Snapshot;
// Use the returned enumerable directly instead of allocating into an array. The returned
// enumeration can contain a fairly large number of items for large files, so even
// using an ArrayBuilder could result in allocation issues without using a custom pool.
var multiLineSpans = GetMultiLineRegions(outliningService, spans, snapshot);
foreach (var span in multiLineSpans)
{
var tag = new StructureTag(this, span, snapshot);
context.AddTag(new TagSpan<IContainerStructureTag>(span.TextSpan.ToSnapshotSpan(snapshot), tag));
}
}
protected override bool TagEquals(IContainerStructureTag tag1, IContainerStructureTag tag2)
{
Contract.ThrowIfFalse(tag1 is StructureTag);
Contract.ThrowIfFalse(tag2 is StructureTag);
return tag1.Equals(tag2);
}
internal abstract object? GetCollapsedHintForm(StructureTag structureTag);
private static bool s_exceptionReported = false;
private static IEnumerable<BlockSpan> GetMultiLineRegions(
BlockStructureService service,
ImmutableArray<BlockSpan> regions, ITextSnapshot snapshot)
{
// Remove any spans that aren't multiline.
foreach (var region in regions)
{
if (region.TextSpan.Length > 0)
{
// Check if any clients produced an invalid OutliningSpan. If so, filter them
// out and report a non-fatal watson so we can attempt to determine the source
// of the issue.
var snapshotSpan = snapshot.GetFullSpan().Span;
var regionSpan = region.TextSpan.ToSpan();
if (!snapshotSpan.Contains(regionSpan))
{
if (!s_exceptionReported)
{
s_exceptionReported = true;
try
{
throw new InvalidOutliningRegionException(service, snapshot, snapshotSpan, regionSpan);
}
catch (InvalidOutliningRegionException e) when (FatalError.ReportAndCatch(e))
{
}
}
continue;
}
var startLine = snapshot.GetLineNumberFromPosition(region.TextSpan.Start);
var endLine = snapshot.GetLineNumberFromPosition(region.TextSpan.End);
if (startLine != endLine)
{
yield return region;
}
}
}
}
#region Creating Preview Buffers
private const int MaxPreviewText = 1000;
/// <summary>
/// Given a <see cref="StructureTag"/>, creates an ITextBuffer with the content to display
/// in the tooltip.
/// </summary>
protected ITextBuffer CreateElisionBufferForTagTooltip(StructureTag tag)
{
// Remove any starting whitespace.
var span = TrimLeadingWhitespace(new SnapshotSpan(tag.Snapshot, tag.CollapsedHintFormSpan));
// Trim the length if it's too long.
var shortSpan = span;
if (span.Length > MaxPreviewText)
{
shortSpan = ComputeShortSpan(span);
}
// Create an elision buffer for that span, also trimming the
// leading whitespace.
var elisionBuffer = CreateElisionBufferWithoutIndentation(shortSpan);
var finalBuffer = elisionBuffer;
// If we trimmed the length, then make a projection buffer that
// has the above elision buffer and follows it with "..."
if (span.Length != shortSpan.Length)
{
finalBuffer = CreateTrimmedProjectionBuffer(elisionBuffer);
}
return finalBuffer;
}
private ITextBuffer CreateTrimmedProjectionBuffer(ITextBuffer elisionBuffer)
{
// The elision buffer is too long. We've already trimmed it, but now we want to add
// a "..." to it. We do that by creating a projection of both the elision buffer and
// a new text buffer wrapping the ellipsis.
var elisionSpan = elisionBuffer.CurrentSnapshot.GetFullSpan();
var sourceSpans = new List<object>()
{
elisionSpan.Snapshot.CreateTrackingSpan(elisionSpan, SpanTrackingMode.EdgeExclusive),
"..."
};
var projectionBuffer = ProjectionBufferFactoryService.CreateProjectionBuffer(
projectionEditResolver: null,
sourceSpans: sourceSpans,
options: ProjectionBufferOptions.None);
return projectionBuffer;
}
private static SnapshotSpan ComputeShortSpan(SnapshotSpan span)
{
var endIndex = span.Start + MaxPreviewText;
var line = span.Snapshot.GetLineFromPosition(endIndex);
return new SnapshotSpan(span.Snapshot, Span.FromBounds(span.Start, line.EndIncludingLineBreak));
}
internal static SnapshotSpan TrimLeadingWhitespace(SnapshotSpan span)
{
int start = span.Start;
while (start < span.End && char.IsWhiteSpace(span.Snapshot[start]))
start++;
return new SnapshotSpan(span.Snapshot, Span.FromBounds(start, span.End));
}
private ITextBuffer CreateElisionBufferWithoutIndentation(
SnapshotSpan shortHintSpan)
{
return ProjectionBufferFactoryService.CreateProjectionBufferWithoutIndentation(
EditorOptionsService.Factory.GlobalOptions,
contentType: null,
exposedSpans: shortHintSpan);
}
#endregion
}
#pragma warning restore CS0618 // Type or member is obsolete
|