Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Apply changes directly to text buffer #62337

Merged
merged 1 commit into from
Jul 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using Microsoft.CodeAnalysis.CSharp.Formatting;
using Microsoft.CodeAnalysis.CSharp.Indentation;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.UnitTests;
using Microsoft.CodeAnalysis.Editor.UnitTests.Formatting;
using Microsoft.CodeAnalysis.Editor.UnitTests.Utilities;
Expand Down Expand Up @@ -99,18 +100,7 @@ private static async Task TokenFormatWorkerAsync(TestWorkspace workspace, ITextB
var formatter = new CSharpSmartTokenFormatter(options, rules, (CompilationUnitSyntax)documentSyntax.Root, documentSyntax.Text);
var changes = formatter.FormatToken(token, CancellationToken.None);

ApplyChanges(buffer, changes);
}

private static void ApplyChanges(ITextBuffer buffer, IList<TextChange> changes)
{
using var edit = buffer.CreateEdit();
foreach (var change in changes)
{
edit.Replace(change.Span.ToSpan(), change.NewText);
}

edit.Apply();
buffer.ApplyChanges(changes);
}

protected static async Task<int> GetSmartTokenFormatterIndentationAsync(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,49 +125,39 @@ internal bool ExecuteCommand(ITextView textView, ITextBuffer subjectBuffer, TCom
/// </summary>
private void ApplyEdits(Document document, ITextView textView, ITextBuffer subjectBuffer, string title, CommentSelectionResult edits, CancellationToken cancellationToken)
{
var workspace = document.Project.Solution.Workspace;

// Create tracking spans to track the text changes.
var currentSnapshot = subjectBuffer.CurrentSnapshot;
var trackingSpans = edits.TrackingSpans
.SelectAsArray(textSpan => (originalSpan: textSpan, trackingSpan: CreateTrackingSpan(edits.ResultOperation, currentSnapshot, textSpan.TrackingTextSpan)));
var originalSnapshot = subjectBuffer.CurrentSnapshot;

// Apply the text changes.
SourceText newText;
using (var transaction = new CaretPreservingEditTransaction(title, textView, _undoHistoryRegistry, _editorOperationsFactoryService))
{
var oldSolution = workspace.CurrentSolution;

var oldDocument = oldSolution.GetRequiredDocument(document.Id);
var oldText = oldDocument.GetTextSynchronously(cancellationToken);
newText = oldText.WithChanges(edits.TextChanges.Distinct());

var newSolution = oldSolution.WithDocumentText(document.Id, newText, PreservationMode.PreserveIdentity);
workspace.TryApplyChanges(newSolution);

subjectBuffer.ApplyChanges(edits.TextChanges);
transaction.Complete();
}

// Convert the tracking spans into snapshot spans for formatting and selection.
var trackingSnapshotSpans = trackingSpans.Select(s => CreateSnapshotSpan(subjectBuffer.CurrentSnapshot, s.trackingSpan, s.originalSpan));

if (trackingSnapshotSpans.Any())
if (edits.TrackingSpans.Any())
{
// Create tracking spans to track the text changes.
var trackingSpans = edits.TrackingSpans
.SelectAsArray(textSpan => (originalSpan: textSpan, trackingSpan: CreateTrackingSpan(edits.ResultOperation, originalSnapshot, textSpan.TrackingTextSpan)));

// Convert the tracking spans into snapshot spans for formatting and selection.
var trackingSnapshotSpans = trackingSpans.Select(s => CreateSnapshotSpan(subjectBuffer.CurrentSnapshot, s.trackingSpan, s.originalSpan));

if (edits.ResultOperation == Operation.Uncomment && document.SupportsSyntaxTree)
{
// Format the document only during uncomment operations. Use second transaction so it can be undone.
using var transaction = new CaretPreservingEditTransaction(title, textView, _undoHistoryRegistry, _editorOperationsFactoryService);

var formattingOptions = subjectBuffer.GetSyntaxFormattingOptions(_editorOptionsService, document.Project.LanguageServices, explicitFormat: false);
var newText = subjectBuffer.CurrentSnapshot.AsText();
var oldSyntaxTree = document.GetSyntaxTreeSynchronously(cancellationToken);
var newRoot = oldSyntaxTree.WithChangedText(newText).GetRoot(cancellationToken);

var updatedDocument = workspace.CurrentSolution.GetRequiredDocument(document.Id);
var root = updatedDocument.GetRequiredSyntaxRootSynchronously(cancellationToken);
var formattingOptions = subjectBuffer.GetSyntaxFormattingOptions(_editorOptionsService, document.Project.LanguageServices, explicitFormat: false);
var formattingSpans = trackingSnapshotSpans.Select(change => CommonFormattingHelpers.GetFormattingSpan(newRoot, change.Span.ToTextSpan()));
var formattedChanges = Formatter.GetFormattedTextChanges(newRoot, formattingSpans, document.Project.Solution.Workspace.Services, formattingOptions, rules: null, cancellationToken);

var formattingSpans = trackingSnapshotSpans.Select(change => CommonFormattingHelpers.GetFormattingSpan(root, change.Span.ToTextSpan()));
var formattedRoot = Formatter.Format(root, formattingSpans, workspace.Services, formattingOptions, rules: null, cancellationToken);
var formattedDocument = document.WithSyntaxRoot(formattedRoot);
subjectBuffer.ApplyChanges(formattedChanges);

workspace.ApplyDocumentChanges(formattedDocument, cancellationToken);
transaction.Complete();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the part i'm least comfortable with. can you smoke test this otu a bit?

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ private void ExecuteCommandWorker(PasteCommandArgs args, SnapshotPoint? caretPos
return;
}

var document = args.SubjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges();
var subjectBuffer = args.SubjectBuffer;

var document = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges();
if (document == null)
{
return;
Expand Down Expand Up @@ -86,16 +88,16 @@ private void ExecuteCommandWorker(PasteCommandArgs args, SnapshotPoint? caretPos
}

var trackingSpan = caretPosition.Value.Snapshot.CreateTrackingSpan(caretPosition.Value.Position, 0, SpanTrackingMode.EdgeInclusive);
var span = trackingSpan.GetSpan(args.SubjectBuffer.CurrentSnapshot).Span.ToTextSpan();
var span = trackingSpan.GetSpan(subjectBuffer.CurrentSnapshot).Span.ToTextSpan();

// Note: C# always completes synchronously, TypeScript is async
var changes = formattingService.GetFormattingChangesOnPasteAsync(document, args.SubjectBuffer, span, cancellationToken).WaitAndGetResult(cancellationToken);
var changes = formattingService.GetFormattingChangesOnPasteAsync(document, subjectBuffer, span, cancellationToken).WaitAndGetResult(cancellationToken);
if (changes.IsEmpty)
{
return;
}

solution.Workspace.ApplyTextChanges(document.Id, changes, cancellationToken);
subjectBuffer.ApplyChanges(changes);
}
}
}
33 changes: 13 additions & 20 deletions src/EditorFeatures/Core/Formatting/FormatCommandHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,28 +75,21 @@ private void Format(ITextView textView, ITextBuffer textBuffer, Document documen
return;
}

var workspace = document.Project.Solution.Workspace;
ApplyChanges(workspace, document.Id, changes, selectionOpt, cancellationToken);
transaction.Complete();
}
}

private static void ApplyChanges(Workspace workspace, DocumentId documentId, IList<TextChange> changes, TextSpan? selectionOpt, CancellationToken cancellationToken)
{
if (selectionOpt.HasValue)
{
var ruleFactory = workspace.Services.GetRequiredService<IHostDependentFormattingRuleFactoryService>();
if (selectionOpt.HasValue)
{
var ruleFactory = document.Project.Solution.Workspace.Services.GetRequiredService<IHostDependentFormattingRuleFactoryService>();
changes = ruleFactory.FilterFormattedChanges(document.Id, selectionOpt.Value, changes).ToImmutableArray();
}

changes = ruleFactory.FilterFormattedChanges(documentId, selectionOpt.Value, changes).ToList();
if (changes.Count == 0)
if (!changes.IsEmpty)
{
return;
using (Logger.LogBlock(FunctionId.Formatting_ApplyResultToBuffer, cancellationToken))
{
textBuffer.ApplyChanges(changes);
}
}
}

using (Logger.LogBlock(FunctionId.Formatting_ApplyResultToBuffer, cancellationToken))
{
workspace.ApplyTextChanges(documentId, changes, cancellationToken);
transaction.Complete();
}
}

Expand Down Expand Up @@ -191,7 +184,7 @@ private void ExecuteReturnOrTypeCommandWorker(EditorCommandArgs args, Cancellati
using (var transaction = CreateEditTransaction(textView, EditorFeaturesResources.Automatic_Formatting))
{
transaction.MergePolicy = AutomaticCodeChangeMergePolicy.Instance;
document.Project.Solution.Workspace.ApplyTextChanges(document.Id, textChanges, cancellationToken);
subjectBuffer.ApplyChanges(textChanges);
transaction.Complete();
}

Expand All @@ -202,7 +195,7 @@ private void ExecuteReturnOrTypeCommandWorker(EditorCommandArgs args, Cancellati
return;
}

var snapshotAfterFormatting = args.SubjectBuffer.CurrentSnapshot;
var snapshotAfterFormatting = subjectBuffer.CurrentSnapshot;

var oldCaretPosition = caretPosition.Value.TranslateTo(snapshotAfterFormatting, PointTrackingMode.Negative);
var newCaretPosition = newCaretPositionMarker.Value.TranslateTo(snapshotAfterFormatting, PointTrackingMode.Negative);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,58 +249,59 @@ private AsyncCompletionData.CommitResult Commit(
return new AsyncCompletionData.CommitResult(isHandled: true, AsyncCompletionData.CommitBehavior.None);
}

ITextSnapshot updatedCurrentSnapshot;
using (var edit = subjectBuffer.CreateEdit(EditOptions.DefaultMinimalChange, reiteratedVersionNumber: null, editTag: null))
{
edit.Replace(mappedSpan.Span, change.TextChange.NewText);

// edit.Apply() may trigger changes made by extensions.
// updatedCurrentSnapshot will contain changes made by Roslyn but not by other extensions.
var updatedCurrentSnapshot = edit.Apply();
updatedCurrentSnapshot = edit.Apply();
}

if (change.NewPosition.HasValue)
if (change.NewPosition.HasValue)
{
// Roslyn knows how to position the caret in the snapshot we just created.
// If there were more edits made by extensions, TryMoveCaretToAndEnsureVisible maps the snapshot point to the most recent one.
view.TryMoveCaretToAndEnsureVisible(new SnapshotPoint(updatedCurrentSnapshot, change.NewPosition.Value));
}
else
{
// Or, If we're doing a minimal change, then the edit that we make to the
// buffer may not make the total text change that places the caret where we
// would expect it to go based on the requested change. In this case,
// determine where the item should go and set the care manually.

// Note: we only want to move the caret if the caret would have been moved
// by the edit. i.e. if the caret was actually in the mapped span that
// we're replacing.
var caretPositionInBuffer = view.GetCaretPoint(subjectBuffer);
if (caretPositionInBuffer.HasValue && mappedSpan.IntersectsWith(caretPositionInBuffer.Value))
{
// Roslyn knows how to position the caret in the snapshot we just created.
// If there were more edits made by extensions, TryMoveCaretToAndEnsureVisible maps the snapshot point to the most recent one.
view.TryMoveCaretToAndEnsureVisible(new SnapshotPoint(updatedCurrentSnapshot, change.NewPosition.Value));
view.TryMoveCaretToAndEnsureVisible(new SnapshotPoint(subjectBuffer.CurrentSnapshot, mappedSpan.Start.Position + textChange.NewText?.Length ?? 0));
}
else
{
// Or, If we're doing a minimal change, then the edit that we make to the
// buffer may not make the total text change that places the caret where we
// would expect it to go based on the requested change. In this case,
// determine where the item should go and set the care manually.

// Note: we only want to move the caret if the caret would have been moved
// by the edit. i.e. if the caret was actually in the mapped span that
// we're replacing.
var caretPositionInBuffer = view.GetCaretPoint(subjectBuffer);
if (caretPositionInBuffer.HasValue && mappedSpan.IntersectsWith(caretPositionInBuffer.Value))
{
view.TryMoveCaretToAndEnsureVisible(new SnapshotPoint(subjectBuffer.CurrentSnapshot, mappedSpan.Start.Position + textChange.NewText?.Length ?? 0));
}
else
{
view.Caret.EnsureVisible();
}
view.Caret.EnsureVisible();
}
}

includesCommitCharacter = change.IncludesCommitCharacter;
includesCommitCharacter = change.IncludesCommitCharacter;

if (roslynItem.Rules.FormatOnCommit)
{
// The edit updates the snapshot however other extensions may make changes there.
// Therefore, it is required to use subjectBuffer.CurrentSnapshot for further calculations rather than the updated current snapshot defined above.
var currentDocument = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges();
var formattingService = currentDocument?.GetRequiredLanguageService<IFormattingInteractionService>();
if (roslynItem.Rules.FormatOnCommit)
{
// The edit updates the snapshot however other extensions may make changes there.
// Therefore, it is required to use subjectBuffer.CurrentSnapshot for further calculations rather than the updated current snapshot defined above.
var currentDocument = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges();
var formattingService = currentDocument?.GetRequiredLanguageService<IFormattingInteractionService>();

if (currentDocument != null && formattingService != null)
{
var spanToFormat = triggerSnapshotSpan.TranslateTo(subjectBuffer.CurrentSnapshot, SpanTrackingMode.EdgeInclusive);
if (currentDocument != null && formattingService != null)
{
var spanToFormat = triggerSnapshotSpan.TranslateTo(subjectBuffer.CurrentSnapshot, SpanTrackingMode.EdgeInclusive);

// Note: C# always completes synchronously, TypeScript is async
var changes = formattingService.GetFormattingChangesAsync(currentDocument, subjectBuffer, spanToFormat.Span.ToTextSpan(), cancellationToken).WaitAndGetResult(cancellationToken);
currentDocument.Project.Solution.Workspace.ApplyTextChanges(currentDocument.Id, changes, cancellationToken);
}
// Note: C# always completes synchronously, TypeScript is async
var changes = formattingService.GetFormattingChangesAsync(currentDocument, subjectBuffer, spanToFormat.Span.ToTextSpan(), cancellationToken).WaitAndGetResult(cancellationToken);
subjectBuffer.ApplyChanges(changes);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.CodeAnalysis.AddImport;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.DocumentationComments;
using Microsoft.CodeAnalysis.Formatting;
Expand Down Expand Up @@ -67,6 +68,14 @@ public static IndentationOptions GetIndentationOptions(this ITextBuffer textBuff
};
}

public static AddImportPlacementOptions GetAddImportPlacementOptions(this ITextBuffer textBuffer, EditorOptionsService optionsProvider, HostLanguageServices languageServices, bool allowInHiddenRegions)
{
var editorOptions = optionsProvider.Factory.GetOptions(textBuffer);
var configOptions = new EditorAnalyzerConfigOptions(editorOptions);
var fallbackOptions = optionsProvider.GlobalOptions.GetAddImportPlacementOptions(languageServices);
return configOptions.GetAddImportPlacementOptions(allowInHiddenRegions, fallbackOptions, languageServices);
}

public static IndentingStyle ToEditorIndentStyle(this FormattingOptions2.IndentStyle value)
=> value switch
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
// 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.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Text.Shared.Extensions;
using Microsoft.VisualStudio.Text;

namespace Microsoft.CodeAnalysis.Editor.Shared.Extensions
Expand Down Expand Up @@ -59,5 +61,28 @@ private static bool TryGetSupportsFeatureService(ITextBuffer buffer, [NotNullWhe

return service != null;
}

public static ITextSnapshot ApplyChange(this ITextBuffer buffer, TextChange change)
{
using var edit = buffer.CreateEdit(EditOptions.DefaultMinimalChange, reiteratedVersionNumber: null, editTag: null);
edit.Replace(change.Span.ToSpan(), change.NewText);
return edit.Apply();
}

public static ITextSnapshot ApplyChanges(this ITextBuffer buffer, IEnumerable<TextChange> changes)
{
using var edit = buffer.CreateEdit(EditOptions.DefaultMinimalChange, reiteratedVersionNumber: null, editTag: null);
return ApplyChanges(edit, changes);
}

public static ITextSnapshot ApplyChanges(this ITextEdit edit, IEnumerable<TextChange> changes)
{
foreach (var change in changes)
{
edit.Replace(change.Span.ToSpan(), change.NewText);
}

return edit.Apply();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public static void FormatAndApplyToBuffer(

using (Logger.LogBlock(FunctionId.Formatting_ApplyResultToBuffer, cancellationToken))
{
document.Project.Solution.Workspace.ApplyTextChanges(document.Id, changes, cancellationToken);
textBuffer.ApplyChanges(changes);
}
}

Expand Down
Loading