/
githubmirror
/
roslyn
Обзор
Документация
Войти
/
githubmirror
/
roslyn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/VisualStudio/Core/Def/Venus/ContainedDocument.cs
1 129 строк
42 KB
Cyrus Najmabadi
Remove unnnecessary null suppressions in Roslyn.sln
17 окт 2025, 22:55
17 окт 2025, 22:55
ec722c4
Код
Авторство
О чём код?
// 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. #nullable disable using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; using System.Threading; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Editor; using Microsoft.CodeAnalysis.Editor.Shared.Extensions; using Microsoft.CodeAnalysis.Editor.Shared.Utilities; using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Formatting.Rules; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.Text.Shared.Extensions; using Microsoft.CodeAnalysis.Workspaces.ProjectSystem; using Microsoft.VisualStudio.ComponentModelHost; using Microsoft.VisualStudio.Editor; using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem; using Microsoft.VisualStudio.Shell.Interop; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Differencing; using Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods; using Microsoft.VisualStudio.Text.Projection; using Roslyn.Utilities; using IVsContainedLanguageHost = Microsoft.VisualStudio.TextManager.Interop.IVsContainedLanguageHost; using IVsTextBufferCoordinator = Microsoft.VisualStudio.TextManager.Interop.IVsTextBufferCoordinator; namespace Microsoft.VisualStudio.LanguageServices.Implementation.Venus; internal sealed partial class ContainedDocument : IContainedDocument { private const string ReturnReplacementString = @"{|r|}"; private const string NewLineReplacementString = @"{|n|}"; private const string HTML = nameof(HTML); private const string HTMLX = nameof(HTMLX); private const string LegacyRazor = nameof(LegacyRazor); private const string Razor = nameof(Razor); private const string XOML = nameof(XOML); private const string WebForms = nameof(WebForms); private const char RazorExplicit = '@'; private const string CSharpRazorBlock = "{"; private const string VBRazorBlock = "code"; private const string HelperRazor = "helper"; private const string FunctionsRazor = "functions"; private const string CodeRazor = "code"; private static readonly EditOptions s_venusEditOptions = new(new StringDifferenceOptions { DifferenceType = StringDifferenceTypes.Character, IgnoreTrimWhiteSpace = false }); private static readonly ConcurrentDictionary<DocumentId, ContainedDocument> s_containedDocuments = []; public static ContainedDocument TryGetContainedDocument(DocumentId id) { if (id == null) { return null; } s_containedDocuments.TryGetValue(id, out var document); return document; } private readonly IComponentModel _componentModel; private readonly Workspace _workspace; private readonly EditorOptionsService _editorOptionsService; private readonly ITextDifferencingSelectorService _differenceSelectorService; private readonly HostType _hostType; private readonly ReiteratedVersionSnapshotTracker _snapshotTracker; private readonly AbstractFormattingRule _vbHelperFormattingRule; private readonly ProjectSystemProject _project; public bool SupportsRename { get { return _hostType == HostType.Razor; } } public bool SupportsSemanticSnippets { get { return false; } } public DocumentId Id { get; } public ITextBuffer SubjectBuffer { get; } public ITextBuffer DataBuffer { get; } public IVsTextBufferCoordinator BufferCoordinator { get; } public IVsContainedLanguageHost ContainedLanguageHost { get; set; } public ContainedDocument( DocumentId documentId, ITextBuffer subjectBuffer, ITextBuffer dataBuffer, IVsTextBufferCoordinator bufferCoordinator, Workspace workspace, ProjectSystemProject project, IComponentModel componentModel, AbstractFormattingRule vbHelperFormattingRule) { _componentModel = componentModel; _workspace = workspace; _project = project; Id = documentId; SubjectBuffer = subjectBuffer; DataBuffer = dataBuffer; BufferCoordinator = bufferCoordinator; _differenceSelectorService = componentModel.GetService<ITextDifferencingSelectorService>(); _editorOptionsService = componentModel.GetService<EditorOptionsService>(); _snapshotTracker = new ReiteratedVersionSnapshotTracker(SubjectBuffer); _vbHelperFormattingRule = vbHelperFormattingRule; _hostType = GetHostType(); s_containedDocuments.TryAdd(documentId, this); } private HostType GetHostType() { if (DataBuffer is IProjectionBuffer projectionBuffer) { // RazorCSharp has an HTMLX base type but should not be associated with // the HTML host type, so we check for it first. if (projectionBuffer.SourceBuffers.Any(b => b.ContentType.IsOfType(Razor) || b.ContentType.IsOfType(LegacyRazor))) { return HostType.Razor; } // For TypeScript hosted in HTML the source buffers will have type names // HTMLX and TypeScript. if (projectionBuffer.SourceBuffers.Any(b => b.ContentType.IsOfType(HTML) || b.ContentType.IsOfType(WebForms) || b.ContentType.IsOfType(HTMLX))) { return HostType.HTML; } } else { // XOML is set up differently. For XOML, the secondary buffer (i.e. SubjectBuffer) // is a projection buffer, while the primary buffer (i.e. DataBuffer) is not. Instead, // the primary buffer is a regular unprojected ITextBuffer with the HTML content type. if (DataBuffer.CurrentSnapshot.ContentType.IsOfType(HTML)) { return HostType.XOML; } } throw ExceptionUtilities.Unreachable(); } public SourceTextContainer GetOpenTextContainer() => this.SubjectBuffer.AsTextContainer(); public void Dispose() { _snapshotTracker.StopTracking(SubjectBuffer); s_containedDocuments.TryRemove(Id, out _); } public DocumentId FindProjectDocumentIdWithItemId(uint itemidInsertionPoint) { // We cast to VisualStudioWorkspace because the expectation is this isn't being used in Live Share workspaces var hierarchy = ((VisualStudioWorkspace)_workspace).GetHierarchy(_project.Id); var containingDocumentItemid = GetContainingDocumentItemId(hierarchy); if (containingDocumentItemid == itemidInsertionPoint) { // No need to walk project documents if requesting containing document's id return this.Id; } foreach (var document in _workspace.CurrentSolution.GetProject(_project.Id).Documents) { if (document.FilePath != null && hierarchy.TryGetItemId(document.FilePath) == itemidInsertionPoint) { return document.Id; } } return null; } public uint FindItemIdOfDocument(Document document) { // We cast to VisualStudioWorkspace because the expectation is this isn't being used in Live Share workspaces var hierarchy = ((VisualStudioWorkspace)_workspace).GetHierarchy(_project.Id); if (document.Id == this.Id) { return GetContainingDocumentItemId(hierarchy); } return hierarchy.TryGetItemId(_workspace.CurrentSolution.GetDocument(document.Id).FilePath); } public void UpdateText(SourceText newText) { var originalText = SubjectBuffer.CurrentSnapshot.AsText(); ApplyChanges(originalText, newText.GetTextChanges(originalText)); } public ITextSnapshot ApplyChanges(IEnumerable<TextChange> changes) => ApplyChanges(SubjectBuffer.CurrentSnapshot.AsText(), changes); private uint GetContainingDocumentItemId(IVsHierarchy hierarchy) { var editorAdaptersFactoryService = _componentModel.GetService<IVsEditorAdaptersFactoryService>(); Marshal.ThrowExceptionForHR(BufferCoordinator.GetPrimaryBuffer(out var primaryTextLines)); var primaryBufferDocumentBuffer = editorAdaptersFactoryService.GetDocumentBuffer(primaryTextLines); var textDocumentFactoryService = _componentModel.GetService<ITextDocumentFactoryService>(); textDocumentFactoryService.TryGetTextDocument(primaryBufferDocumentBuffer, out var textDocument); var documentPath = textDocument.FilePath; var itemId = hierarchy.TryGetItemId(documentPath); return itemId; } private ITextSnapshot ApplyChanges(SourceText originalText, IEnumerable<TextChange> changes) { var subjectBuffer = (IProjectionBuffer)SubjectBuffer; IEnumerable<int> affectedVisibleSpanIndices = null; var editorVisibleSpansInOriginal = SharedPools.Default<List<TextSpan>>().AllocateAndClear(); try { var originalDocument = _workspace.CurrentSolution.GetDocument(this.Id); editorVisibleSpansInOriginal.AddRange(GetEditorVisibleSpans()); var newChanges = FilterTextChanges(originalText, editorVisibleSpansInOriginal, changes).ToList(); if (newChanges.Count > 0) { ApplyChanges(subjectBuffer, newChanges, editorVisibleSpansInOriginal, out affectedVisibleSpanIndices); AdjustIndentation(subjectBuffer, affectedVisibleSpanIndices); } return subjectBuffer.CurrentSnapshot; } finally { SharedPools.Default<HashSet<int>>().ClearAndFree((HashSet<int>)affectedVisibleSpanIndices); SharedPools.Default<List<TextSpan>>().ClearAndFree(editorVisibleSpansInOriginal); } } private IEnumerable<TextChange> FilterTextChanges(SourceText originalText, List<TextSpan> editorVisibleSpansInOriginal, IEnumerable<TextChange> changes) { // no visible spans or changes if (editorVisibleSpansInOriginal.Count == 0 || !changes.Any()) { // return empty one yield break; } using var pooledObject = SharedPools.Default<List<TextChange>>().GetPooledObject(); var changeQueue = pooledObject.Object; changeQueue.AddRange(changes); var spanIndex = 0; var changeIndex = 0; for (; spanIndex < editorVisibleSpansInOriginal.Count; spanIndex++) { var visibleSpan = editorVisibleSpansInOriginal[spanIndex]; var visibleTextSpan = GetVisibleTextSpan(originalText, visibleSpan, uptoFirstAndLastLine: true); for (; changeIndex < changeQueue.Count; changeIndex++) { var change = changeQueue[changeIndex]; // easy case first if (change.Span.End < visibleSpan.Start) { // move to next change continue; } if (visibleSpan.End < change.Span.Start) { // move to next visible span break; } // make sure we are not replacing whitespace around start and at the end of visible span if (WhitespaceOnEdges(visibleTextSpan, change)) { continue; } if (visibleSpan.Contains(change.Span)) { yield return change; continue; } // now it is complex case where things are intersecting each other var subChanges = GetSubTextChanges(originalText, change, visibleSpan).ToList(); if (subChanges.Count > 0) { if (subChanges.Count == 1 && subChanges[0] == change) { // we can't break it. not much we can do here. just don't touch and ignore this change continue; } changeQueue.InsertRange(changeIndex + 1, subChanges); continue; } } } } private static bool WhitespaceOnEdges(TextSpan visibleTextSpan, TextChange change) { if (!string.IsNullOrWhiteSpace(change.NewText)) { return false; } if (change.Span.End <= visibleTextSpan.Start) { return true; } if (visibleTextSpan.End <= change.Span.Start) { return true; } return false; } private IEnumerable<TextChange> GetSubTextChanges(SourceText originalText, TextChange changeInOriginalText, TextSpan visibleSpanInOriginalText) { using var changes = SharedPools.Default<List<TextChange>>().GetPooledObject(); var leftText = originalText.ToString(changeInOriginalText.Span); var rightText = changeInOriginalText.NewText; var offsetInOriginalText = changeInOriginalText.Span.Start; if (TryGetSubTextChanges(originalText, visibleSpanInOriginalText, leftText, rightText, offsetInOriginalText, changes.Object)) { return changes.Object.ToList(); } return GetSubTextChanges(originalText, visibleSpanInOriginalText, leftText, rightText, offsetInOriginalText); } private static bool TryGetSubTextChanges( SourceText originalText, TextSpan visibleSpanInOriginalText, string leftText, string rightText, int offsetInOriginalText, List<TextChange> changes) { // these are expensive. but hopefully we don't hit this as much except the boundary cases. using var leftPool = SharedPools.Default<List<TextSpan>>().GetPooledObject(); using var rightPool = SharedPools.Default<List<TextSpan>>().GetPooledObject(); var spansInLeftText = leftPool.Object; var spansInRightText = rightPool.Object; if (TryGetWhitespaceOnlyChanges(leftText, rightText, spansInLeftText, spansInRightText)) { for (var i = 0; i < spansInLeftText.Count; i++) { var spanInLeftText = spansInLeftText[i]; var spanInRightText = spansInRightText[i]; if (spanInLeftText.IsEmpty && spanInRightText.IsEmpty) { continue; } var spanInOriginalText = new TextSpan(offsetInOriginalText + spanInLeftText.Start, spanInLeftText.Length); if (TryGetSubTextChange(originalText, visibleSpanInOriginalText, rightText, spanInOriginalText, spanInRightText, out var textChange)) { changes.Add(textChange); } } return true; } return false; } private IEnumerable<TextChange> GetSubTextChanges( SourceText originalText, TextSpan visibleSpanInOriginalText, string leftText, string rightText, int offsetInOriginalText) { // these are expensive. but hopefully we don't hit this as much except the boundary cases. using var leftPool = SharedPools.Default<List<ValueTuple<int, int>>>().GetPooledObject(); using var rightPool = SharedPools.Default<List<ValueTuple<int, int>>>().GetPooledObject(); var leftReplacementMap = leftPool.Object; var rightReplacementMap = rightPool.Object; GetTextWithReplacements(leftText, rightText, leftReplacementMap, rightReplacementMap, out var leftTextWithReplacement, out var rightTextWithReplacement); var diffResult = DiffStrings(leftTextWithReplacement, rightTextWithReplacement); foreach (var difference in diffResult) { var spanInLeftText = AdjustSpan(diffResult.LeftDecomposition.GetSpanInOriginal(difference.Left), leftReplacementMap); var spanInRightText = AdjustSpan(diffResult.RightDecomposition.GetSpanInOriginal(difference.Right), rightReplacementMap); var spanInOriginalText = new TextSpan(offsetInOriginalText + spanInLeftText.Start, spanInLeftText.Length); if (TryGetSubTextChange(originalText, visibleSpanInOriginalText, rightText, spanInOriginalText, spanInRightText.ToTextSpan(), out var textChange)) { yield return textChange; } } } private static bool TryGetWhitespaceOnlyChanges(string leftText, string rightText, List<TextSpan> spansInLeftText, List<TextSpan> spansInRightText) => TryGetWhitespaceGroup(leftText, spansInLeftText) && TryGetWhitespaceGroup(rightText, spansInRightText) && spansInLeftText.Count == spansInRightText.Count; private static bool TryGetWhitespaceGroup(string text, List<TextSpan> groups) { if (text.Length == 0) { groups.Add(new TextSpan(0, 0)); return true; } var start = 0; for (var i = 0; i < text.Length; i++) { var ch = text[i]; switch (ch) { case ' ': case '\t': if (!TextAt(text, i - 1, ' ', '\t')) { start = i; } break; case '\r': case '\n': if (i == 0) { groups.Add(TextSpan.FromBounds(0, 0)); } else if (TextAt(text, i - 1, ' ', '\t')) { groups.Add(TextSpan.FromBounds(start, i)); } else if (TextAt(text, i - 1, '\n')) { groups.Add(TextSpan.FromBounds(start, i)); } start = i + 1; break; default: return false; } } if (start <= text.Length) { groups.Add(TextSpan.FromBounds(start, text.Length)); } return true; } private static bool TextAt(string text, int index, char ch1, char ch2 = default) { if (index < 0 || text.Length <= index) { return false; } var actual = text[index]; if (actual == ch1) { return true; } if (ch2 != default && actual == ch2) { return true; } return false; } private static bool TryGetSubTextChange( SourceText originalText, TextSpan visibleSpanInOriginalText, string rightText, TextSpan spanInOriginalText, TextSpan spanInRightText, out TextChange textChange) { textChange = default; var visibleFirstLineInOriginalText = originalText.Lines.GetLineFromPosition(visibleSpanInOriginalText.Start); var visibleLastLineInOriginalText = originalText.Lines.GetLineFromPosition(visibleSpanInOriginalText.End); // skip easy case // 1. things are out of visible span if (!visibleSpanInOriginalText.IntersectsWith(spanInOriginalText)) { return false; } // 2. there are no intersects var snippetInRightText = rightText.Substring(spanInRightText.Start, spanInRightText.Length); if (visibleSpanInOriginalText.Contains(spanInOriginalText) && visibleSpanInOriginalText.End != spanInOriginalText.End) { textChange = new TextChange(spanInOriginalText, snippetInRightText); return true; } // okay, more complex case. things are intersecting boundaries. var firstLineOfRightTextSnippet = snippetInRightText.GetFirstLineText(); var lastLineOfRightTextSnippet = snippetInRightText.GetLastLineText(); // there are 4 complex cases - these are all heuristic. not sure what better way I have. and the heuristic is heavily based on // text differ's behavior. // 1. it is a single line if (visibleFirstLineInOriginalText.LineNumber == visibleLastLineInOriginalText.LineNumber) { // don't do anything return false; } // 2. replacement contains visible spans if (spanInOriginalText.Contains(visibleSpanInOriginalText)) { // header // don't do anything // body textChange = new TextChange( TextSpan.FromBounds(visibleFirstLineInOriginalText.EndIncludingLineBreak, visibleLastLineInOriginalText.Start), snippetInRightText.Substring(firstLineOfRightTextSnippet.Length, snippetInRightText.Length - firstLineOfRightTextSnippet.Length - lastLineOfRightTextSnippet.Length)); // footer // don't do anything return true; } // 3. replacement intersects with start if (spanInOriginalText.Start < visibleSpanInOriginalText.Start && visibleSpanInOriginalText.Start <= spanInOriginalText.End && spanInOriginalText.End < visibleSpanInOriginalText.End) { // header // don't do anything // body if (visibleFirstLineInOriginalText.EndIncludingLineBreak <= spanInOriginalText.End) { textChange = new TextChange( TextSpan.FromBounds(visibleFirstLineInOriginalText.EndIncludingLineBreak, spanInOriginalText.End), snippetInRightText[firstLineOfRightTextSnippet.Length..]); return true; } return false; } // 4. replacement intersects with end if (visibleSpanInOriginalText.Start < spanInOriginalText.Start && spanInOriginalText.Start <= visibleSpanInOriginalText.End && visibleSpanInOriginalText.End <= spanInOriginalText.End) { // body if (spanInOriginalText.Start <= visibleLastLineInOriginalText.Start) { textChange = new TextChange( TextSpan.FromBounds(spanInOriginalText.Start, visibleLastLineInOriginalText.Start), snippetInRightText[..^lastLineOfRightTextSnippet.Length]); return true; } // footer // don't do anything return false; } // if it got hit, then it means there is a missing case throw ExceptionUtilities.Unreachable(); } private IHierarchicalDifferenceCollection DiffStrings(string leftTextWithReplacement, string rightTextWithReplacement) { var diffService = _differenceSelectorService.GetTextDifferencingService( _workspace.Services.GetLanguageServices(_project.Language).GetService<IContentTypeLanguageService>().GetDefaultContentType()); diffService ??= _differenceSelectorService.DefaultTextDifferencingService; return diffService.DiffStrings(leftTextWithReplacement, rightTextWithReplacement, s_venusEditOptions.DifferenceOptions); } private static void GetTextWithReplacements( string leftText, string rightText, List<ValueTuple<int, int>> leftReplacementMap, List<ValueTuple<int, int>> rightReplacementMap, out string leftTextWithReplacement, out string rightTextWithReplacement) { // to make diff works better, we choose replacement strings that don't appear in both texts. var returnReplacement = GetReplacementStrings(leftText, rightText, ReturnReplacementString); var newLineReplacement = GetReplacementStrings(leftText, rightText, NewLineReplacementString); leftTextWithReplacement = GetTextWithReplacementMap(leftText, returnReplacement, newLineReplacement, leftReplacementMap); rightTextWithReplacement = GetTextWithReplacementMap(rightText, returnReplacement, newLineReplacement, rightReplacementMap); } private static string GetReplacementStrings(string leftText, string rightText, string initialReplacement) { if (leftText.IndexOf(initialReplacement, StringComparison.Ordinal) < 0 && rightText.IndexOf(initialReplacement, StringComparison.Ordinal) < 0) { return initialReplacement; } // okay, there is already one in the given text. const string format = "{{|{0}|{1}|{0}|}}"; for (var i = 0; true; i++) { var replacement = string.Format(format, i.ToString(), initialReplacement); if (leftText.IndexOf(replacement, StringComparison.Ordinal) < 0 && rightText.IndexOf(replacement, StringComparison.Ordinal) < 0) { return replacement; } } } private static string GetTextWithReplacementMap(string text, string returnReplacement, string newLineReplacement, List<ValueTuple<int, int>> replacementMap) { var delta = 0; var returnLength = returnReplacement.Length; var newLineLength = newLineReplacement.Length; var sb = StringBuilderPool.Allocate(); for (var i = 0; i < text.Length; i++) { var ch = text[i]; if (ch == '\r') { sb.Append(returnReplacement); delta += returnLength - 1; replacementMap.Add(ValueTuple.Create(i + delta, delta)); continue; } else if (ch == '\n') { sb.Append(newLineReplacement); delta += newLineLength - 1; replacementMap.Add(ValueTuple.Create(i + delta, delta)); continue; } sb.Append(ch); } return StringBuilderPool.ReturnAndFree(sb); } private static Span AdjustSpan(Span span, List<ValueTuple<int, int>> replacementMap) { var start = span.Start; var end = span.End; for (var i = 0; i < replacementMap.Count; i++) { var positionAndDelta = replacementMap[i]; if (positionAndDelta.Item1 <= span.Start) { start = span.Start - positionAndDelta.Item2; } if (positionAndDelta.Item1 <= span.End) { end = span.End - positionAndDelta.Item2; } if (positionAndDelta.Item1 > span.End) { break; } } return Span.FromBounds(start, end); } public IEnumerable<TextSpan> GetEditorVisibleSpans() { var subjectBuffer = (IProjectionBuffer)this.SubjectBuffer; if (DataBuffer is IProjectionBuffer projectionDataBuffer) { return projectionDataBuffer.CurrentSnapshot .GetSourceSpans() .Where(ss => ss.Snapshot.TextBuffer == subjectBuffer) .Select(s => s.Span.ToTextSpan()) .OrderBy(s => s.Start); } else { return []; } } private static void ApplyChanges( IProjectionBuffer subjectBuffer, IEnumerable<TextChange> changes, IList<TextSpan> visibleSpansInOriginal, out IEnumerable<int> affectedVisibleSpansInNew) { using var edit = subjectBuffer.CreateEdit(s_venusEditOptions, reiteratedVersionNumber: null, editTag: null); var affectedSpans = SharedPools.Default<HashSet<int>>().AllocateAndClear(); affectedVisibleSpansInNew = affectedSpans; var currentVisibleSpanIndex = 0; foreach (var change in changes) { // Find the next visible span that either overlaps or intersects with while (currentVisibleSpanIndex < visibleSpansInOriginal.Count && visibleSpansInOriginal[currentVisibleSpanIndex].End < change.Span.Start) { currentVisibleSpanIndex++; } // no more place to apply text changes if (currentVisibleSpanIndex >= visibleSpansInOriginal.Count) { break; } var newText = change.NewText; var span = change.Span.ToSpan(); edit.Replace(span, newText); affectedSpans.Add(currentVisibleSpanIndex); } edit.ApplyAndLogExceptions(); } private void AdjustIndentation(IProjectionBuffer subjectBuffer, IEnumerable<int> visibleSpanIndex) { if (!visibleSpanIndex.Any()) { return; } var document = _workspace.CurrentSolution.GetDocument(this.Id); if (!document.SupportsSyntaxTree) { return; } var parsedDocument = ParsedDocument.CreateSynchronously(document, CancellationToken.None); Debug.Assert(ReferenceEquals(parsedDocument.Text, subjectBuffer.CurrentSnapshot.AsText())); var editorOptionsService = _componentModel.GetService<EditorOptionsService>(); var formattingOptions = subjectBuffer.GetSyntaxFormattingOptions(editorOptionsService, document.Project.GetFallbackAnalyzerOptions(), document.Project.Services, explicitFormat: false); using var pooledObject = SharedPools.Default<List<TextSpan>>().GetPooledObject(); var spans = pooledObject.Object; spans.AddRange(this.GetEditorVisibleSpans()); using var edit = subjectBuffer.CreateEdit(s_venusEditOptions, reiteratedVersionNumber: null, editTag: null); foreach (var spanIndex in visibleSpanIndex) { var rule = GetBaseIndentationRule(parsedDocument.Root, parsedDocument.Text, spans, spanIndex); var visibleSpan = spans[spanIndex]; AdjustIndentationForSpan(document, edit, visibleSpan, rule, formattingOptions); } edit.Apply(); } private void AdjustIndentationForSpan( Document document, ITextEdit edit, TextSpan visibleSpan, AbstractFormattingRule baseIndentationRule, SyntaxFormattingOptions options) { var root = document.GetSyntaxRootSynchronously(CancellationToken.None); using var rulePool = SharedPools.Default<List<AbstractFormattingRule>>().GetPooledObject(); using var spanPool = SharedPools.Default<List<TextSpan>>().GetPooledObject(); var venusFormattingRules = rulePool.Object; var visibleSpans = spanPool.Object; venusFormattingRules.Add(baseIndentationRule); venusFormattingRules.Add(ContainedDocumentPreserveFormattingRule.Instance); var services = document.Project.Solution.Services; var formatter = document.GetRequiredLanguageService<ISyntaxFormattingService>(); var changes = formatter.GetFormattingResult( root, [CommonFormattingHelpers.GetFormattingSpan(root, visibleSpan)], options, [.. venusFormattingRules, .. Formatter.GetDefaultFormattingRules(document)], CancellationToken.None).GetTextChanges(CancellationToken.None); visibleSpans.Add(visibleSpan); var newChanges = FilterTextChanges(document.GetTextSynchronously(CancellationToken.None), visibleSpans, changes.ToReadOnlyCollection()).Where(t => visibleSpan.Contains(t.Span)); foreach (var change in newChanges) { edit.Replace(change.Span.ToSpan(), change.NewText); } } public BaseIndentationFormattingRule GetBaseIndentationRule(SyntaxNode root, SourceText text, List<TextSpan> spans, int spanIndex) { if (_hostType == HostType.Razor) { var currentSpanIndex = spanIndex; GetVisibleAndTextSpan(text, spans, currentSpanIndex, out var visibleSpan, out var visibleTextSpan); var end = visibleSpan.End; var current = root.FindToken(visibleTextSpan.Start).Parent; while (current != null) { if (current.Span.Start == visibleTextSpan.Start) { var blockType = GetRazorCodeBlockType(visibleSpan.Start); if (blockType == RazorCodeBlockType.Explicit) { var baseIndentation = GetBaseIndentation(root, text, visibleSpan); return new BaseIndentationFormattingRule(root, TextSpan.FromBounds(visibleSpan.Start, end), baseIndentation, _vbHelperFormattingRule); } } if (current.Span.Start < visibleSpan.Start) { var blockType = GetRazorCodeBlockType(visibleSpan.Start); if (blockType is RazorCodeBlockType.Block or RazorCodeBlockType.Helper) { var baseIndentation = GetBaseIndentation(root, text, visibleSpan); return new BaseIndentationFormattingRule(root, TextSpan.FromBounds(visibleSpan.Start, end), baseIndentation, _vbHelperFormattingRule); } if (currentSpanIndex == 0) { break; } GetVisibleAndTextSpan(text, spans, --currentSpanIndex, out visibleSpan, out visibleTextSpan); continue; } current = current.Parent; } } var span = spans[spanIndex]; var indentation = GetBaseIndentation(root, text, span); return new BaseIndentationFormattingRule(root, span, indentation, _vbHelperFormattingRule); } private static void GetVisibleAndTextSpan(SourceText text, List<TextSpan> spans, int spanIndex, out TextSpan visibleSpan, out TextSpan visibleTextSpan) { visibleSpan = spans[spanIndex]; visibleTextSpan = GetVisibleTextSpan(text, visibleSpan); if (visibleTextSpan.IsEmpty) { // span has no text in them visibleTextSpan = visibleSpan; } } private int GetBaseIndentation(SyntaxNode root, SourceText text, TextSpan span) { var editorOptions = _editorOptionsService.Factory.GetOptions(DataBuffer); var additionalIndentation = GetAdditionalIndentation(root, text, span, hostIndentationSize: editorOptions.GetIndentSize()); // Skip over the first line, since it's in "Venus space" anyway. var startingLine = text.Lines.GetLineFromPosition(span.Start); for (var line = startingLine; line.Start < span.End; line = text.Lines[line.LineNumber + 1]) { Marshal.ThrowExceptionForHR( ContainedLanguageHost.GetLineIndent( line.LineNumber, out var baseIndentationString, out _, out _, out _, out _)); if (!string.IsNullOrEmpty(baseIndentationString)) { return baseIndentationString.GetColumnFromLineOffset(baseIndentationString.Length, editorOptions.GetTabSize()) + additionalIndentation; } } return additionalIndentation; } private static TextSpan GetVisibleTextSpan(SourceText text, TextSpan visibleSpan, bool uptoFirstAndLastLine = false) { var start = visibleSpan.Start; for (; start < visibleSpan.End; start++) { if (!char.IsWhiteSpace(text[start])) { break; } } var end = visibleSpan.End - 1; if (start <= end) { for (; start <= end; end--) { if (!char.IsWhiteSpace(text[end])) { break; } } } if (uptoFirstAndLastLine) { var firstLine = text.Lines.GetLineFromPosition(visibleSpan.Start); var lastLine = text.Lines.GetLineFromPosition(visibleSpan.End); if (firstLine.LineNumber < lastLine.LineNumber) { start = (start < firstLine.End) ? start : firstLine.End; end = (lastLine.Start < end + 1) ? end : lastLine.Start - 1; } } return (start <= end) ? TextSpan.FromBounds(start, end + 1) : default; } private int GetAdditionalIndentation(SyntaxNode root, SourceText text, TextSpan span, int hostIndentationSize) { if (_hostType == HostType.HTML) { return hostIndentationSize; } if (_hostType == HostType.Razor) { var type = GetRazorCodeBlockType(span.Start); // razor block if (type == RazorCodeBlockType.Block) { // more workaround for csharp razor case. when } for csharp razor code block is just typed, "}" exist // in both subject and surface buffer and there is no easy way to figure out who owns } just typed. // in this case, we let razor owns it. later razor will remove } from subject buffer if it is something // razor owns. if (_project.Language == LanguageNames.CSharp) { var textSpan = GetVisibleTextSpan(text, span); var end = textSpan.End - 1; if (end >= 0 && text[end] == '}') { var token = root.FindToken(end); var service = _workspace.Services.GetLanguageServices(_project.Language).GetService<IVenusBraceMatchingService>(); if (token.Span.Start == end && service != null) { if (service.TryGetCorrespondingOpenBrace(token, out var openBrace) && !textSpan.Contains(openBrace.Span)) { return 0; } } } } // same as C#, but different text is in the buffer if (_project.Language == LanguageNames.VisualBasic) { var textSpan = GetVisibleTextSpan(text, span); var subjectSnapshot = SubjectBuffer.CurrentSnapshot; var end = textSpan.End - 1; if (end >= 0) { var ch = subjectSnapshot[end]; if (CheckCode(subjectSnapshot, textSpan.End, ch, VBRazorBlock, checkAt: false) || CheckCode(subjectSnapshot, textSpan.End, ch, FunctionsRazor, checkAt: false)) { var token = root.FindToken(end, findInsideTrivia: true); var syntaxFact = _workspace.Services.GetLanguageServices(_project.Language).GetService<ISyntaxFactsService>(); if (token.Span.End == textSpan.End && syntaxFact != null) { if (syntaxFact.IsSkippedTokensTrivia(token.Parent)) { return 0; } } } } } return hostIndentationSize; } } return 0; } private RazorCodeBlockType GetRazorCodeBlockType(int position) { Debug.Assert(_hostType == HostType.Razor); var subjectBuffer = (IProjectionBuffer)this.SubjectBuffer; var subjectSnapshot = subjectBuffer.CurrentSnapshot; var surfaceSnapshot = ((IProjectionBuffer)DataBuffer).CurrentSnapshot; var surfacePoint = surfaceSnapshot.MapFromSourceSnapshot(new SnapshotPoint(subjectSnapshot, position), PositionAffinity.Predecessor); if (!surfacePoint.HasValue) { // how this can happen? return RazorCodeBlockType.Implicit; } var ch = char.ToLower(surfaceSnapshot[Math.Max(surfacePoint.Value - 1, 0)]); // razor block if (IsCodeBlock(surfaceSnapshot, surfacePoint.Value, ch)) { return RazorCodeBlockType.Block; } if (ch == RazorExplicit) { return RazorCodeBlockType.Explicit; } if (CheckCode(surfaceSnapshot, surfacePoint.Value, HelperRazor)) { return RazorCodeBlockType.Helper; } return RazorCodeBlockType.Implicit; } private bool IsCodeBlock(ITextSnapshot surfaceSnapshot, int position, char ch) { if (_project.Language == LanguageNames.CSharp) { return CheckCode(surfaceSnapshot, position, ch, CSharpRazorBlock) || CheckCode(surfaceSnapshot, position, ch, FunctionsRazor, CSharpRazorBlock) || CheckCode(surfaceSnapshot, position, ch, CodeRazor, CSharpRazorBlock); } if (_project.Language == LanguageNames.VisualBasic) { return CheckCode(surfaceSnapshot, position, ch, VBRazorBlock) || CheckCode(surfaceSnapshot, position, ch, FunctionsRazor); } return false; } private static bool CheckCode(ITextSnapshot snapshot, int position, char ch, string tag, bool checkAt = true) { if (ch != tag[^1] || position < tag.Length) { return false; } var start = position - tag.Length; var razorTag = snapshot.GetText(start, tag.Length); return string.Equals(razorTag, tag, StringComparison.OrdinalIgnoreCase) && (!checkAt || snapshot[start - 1] == RazorExplicit); } private static bool CheckCode(ITextSnapshot snapshot, int position, string tag) { var i = position - 1; if (i < 0) { return false; } for (; i >= 0; i--) { if (!char.IsWhiteSpace(snapshot[i])) { break; } } var ch = snapshot[i]; position = i + 1; return CheckCode(snapshot, position, ch, tag); } private static bool CheckCode(ITextSnapshot snapshot, int position, char ch, string tag1, string tag2) { if (!CheckCode(snapshot, position, ch, tag2, checkAt: false)) { return false; } return CheckCode(snapshot, position - tag2.Length, tag1); } private enum RazorCodeBlockType { Block, Explicit, Implicit, Helper } private enum HostType { HTML, Razor, XOML } }