提交 bb7794a2 编写于 作者: D David Barbet

Use ITextStructureNavigator instead of syntax tree to find caret location.

上级 f677a4fb
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Immutable;
using System.ComponentModel.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CommentSelection;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Editor.Implementation.CommentSelection;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Operations;
using Microsoft.VisualStudio.Utilities;
using Roslyn.Utilities;
using VSCommanding = Microsoft.VisualStudio.Commanding;
namespace Microsoft.CodeAnalysis.Editor.CSharp.CommentSelection
......@@ -21,23 +26,28 @@ namespace Microsoft.CodeAnalysis.Editor.CSharp.CommentSelection
internal class CSharpToggleBlockCommentCommandHandler :
ToggleBlockCommentCommandHandler
{
private ITextStructureNavigatorSelectorService _navigator;
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal CSharpToggleBlockCommentCommandHandler(
ITextUndoHistoryRegistry undoHistoryRegistry,
IEditorOperationsFactoryService editorOperationsFactoryService)
: base(undoHistoryRegistry, editorOperationsFactoryService)
IEditorOperationsFactoryService editorOperationsFactoryService,
ITextStructureNavigatorSelectorService navigatorSelectorService)
: base(undoHistoryRegistry, editorOperationsFactoryService, navigatorSelectorService)
{
}
/// <summary>
/// Retrieves the toggle block comment data provider with additional CSharp functionality.
/// Retrieves block comments in document using the CSharp syntax tree.
/// </summary>
protected override async Task<IToggleBlockCommentDocumentDataProvider> GetBlockCommentDocumentDataProvider(Document document, ITextSnapshot snapshot,
protected override async Task<ImmutableArray<TextSpan>> GetBlockCommentsInDocument(Document document, ITextSnapshot snapshot,
CommentSelectionInfo commentInfo, CancellationToken cancellationToken)
{
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
return new CSharpToggleBlockCommentDocumentDataProvider(root);
return root.DescendantTrivia()
.Where(trivia => trivia.IsKind(SyntaxKind.MultiLineCommentTrivia) || trivia.IsKind(SyntaxKind.MultiLineDocumentationCommentTrivia))
.SelectAsArray(blockCommentTrivia => blockCommentTrivia.Span);
}
}
}
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Editor.Implementation.CommentSelection;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.Editor.CSharp.CommentSelection
{
class CSharpToggleBlockCommentDocumentDataProvider : IToggleBlockCommentDocumentDataProvider
{
private readonly SyntaxNode _root;
public CSharpToggleBlockCommentDocumentDataProvider(SyntaxNode root)
{
_root = root;
}
/// <summary>
/// Get a location of itself or the end of the token it is located in.
/// </summary>
public int GetEmptyCommentStartLocation(int location)
{
var token = _root.FindToken(location);
if (token.Span.Contains(location))
{
return token.Span.End;
}
return location;
}
/// <summary>
/// Get the location of the comments from the syntax tree.
/// </summary>
/// <returns></returns>
public ImmutableArray<TextSpan> GetBlockCommentsInDocument()
{
return _root.DescendantTrivia()
.Where(trivia => trivia.IsKind(SyntaxKind.MultiLineCommentTrivia) || trivia.IsKind(SyntaxKind.MultiLineDocumentationCommentTrivia))
.SelectAsArray(blockCommentTrivia => blockCommentTrivia.Span);
}
}
}
......@@ -57,7 +57,7 @@ internal abstract class AbstractCommentSelectionBase<TCommand>
// Internal as tests currently rely on this method.
internal abstract Task<CommentSelectionResult> CollectEdits(
Document document, ICommentSelectionService service, NormalizedSnapshotSpanCollection selectedSpans,
Document document, ICommentSelectionService service, ITextBuffer textBuffer, NormalizedSnapshotSpanCollection selectedSpans,
TCommand command, CancellationToken cancellationToken);
protected static VSCommanding.CommandState GetCommandState(ITextBuffer buffer)
......@@ -104,7 +104,7 @@ internal bool ExecuteCommand(ITextView textView, ITextBuffer subjectBuffer, TCom
return true;
}
var edits = CollectEdits(document, service, selectedSpans, command, cancellationToken).WaitAndGetResult(cancellationToken);
var edits = CollectEdits(document, service, subjectBuffer, selectedSpans, command, cancellationToken).WaitAndGetResult(cancellationToken);
ApplyEdits(document, textView, subjectBuffer, service, title, edits);
}
......
......@@ -27,20 +27,24 @@ internal abstract class AbstractToggleBlockCommentBase :
AbstractCommentSelectionBase<ValueTuple>,
VSCommanding.ICommandHandler<CommentSelectionCommandArgs>
{
private static readonly CommentSelectionResult s_EmptyCommentSelectionResult =
private static readonly CommentSelectionResult s_emptyCommentSelectionResult =
new CommentSelectionResult(new List<TextChange>(), new List<CommentTrackingSpan>(), Operation.Uncomment);
private readonly ITextStructureNavigatorSelectorService _navigatorSelectorService;
internal AbstractToggleBlockCommentBase(
ITextUndoHistoryRegistry undoHistoryRegistry,
IEditorOperationsFactoryService editorOperationsFactoryService)
IEditorOperationsFactoryService editorOperationsFactoryService,
ITextStructureNavigatorSelectorService navigatorSelectorService)
: base(undoHistoryRegistry, editorOperationsFactoryService)
{
_navigatorSelectorService = navigatorSelectorService;
}
/// <summary>
/// Retrieves data about the commented selection that can be optionally overriden by subclasses.
/// </summary>
protected abstract Task<IToggleBlockCommentDocumentDataProvider> GetBlockCommentDocumentDataProvider(Document document, ITextSnapshot snapshot,
protected abstract Task<ImmutableArray<TextSpan>> GetBlockCommentsInDocument(Document document, ITextSnapshot snapshot,
CommentSelectionInfo commentInfo, CancellationToken cancellationToken);
// TODO - Change to toggle handler.
......@@ -62,29 +66,31 @@ public bool ExecuteCommand(CommentSelectionCommandArgs args, CommandExecutionCon
protected override string GetMessage(ValueTuple command) => EditorFeaturesResources.Toggling_block_comment;
internal async override Task<CommentSelectionResult> CollectEdits(Document document, ICommentSelectionService service,
NormalizedSnapshotSpanCollection selectedSpans, ValueTuple command, CancellationToken cancellationToken)
ITextBuffer subjectBuffer, NormalizedSnapshotSpanCollection selectedSpans, ValueTuple command, CancellationToken cancellationToken)
{
var experimentationService = document.Project.Solution.Workspace.Services.GetRequiredService<IExperimentationService>();
if (!experimentationService.IsExperimentEnabled(WellKnownExperimentNames.RoslynToggleBlockComment))
{
return s_EmptyCommentSelectionResult;
return s_emptyCommentSelectionResult;
}
var navigator = _navigatorSelectorService.GetTextStructureNavigator(subjectBuffer);
var commentInfo = await service.GetInfoAsync(document, selectedSpans.First().Span.ToTextSpan(), cancellationToken).ConfigureAwait(false);
if (commentInfo.SupportsBlockComment)
{
return await ToggleBlockComments(document, commentInfo, selectedSpans, cancellationToken).ConfigureAwait(false);
return await ToggleBlockComments(document, commentInfo, navigator, selectedSpans, cancellationToken).ConfigureAwait(false);
}
return s_EmptyCommentSelectionResult;
return s_emptyCommentSelectionResult;
}
private async Task<CommentSelectionResult> ToggleBlockComments(Document document, CommentSelectionInfo commentInfo,
NormalizedSnapshotSpanCollection selectedSpans, CancellationToken cancellationToken)
ITextStructureNavigator navigator, NormalizedSnapshotSpanCollection selectedSpans, CancellationToken cancellationToken)
{
var blockCommentDataProvider = await GetBlockCommentDocumentDataProvider(document, selectedSpans.First().Snapshot, commentInfo, cancellationToken).ConfigureAwait(false);
var blockCommentedSpans = await GetBlockCommentsInDocument(
document, selectedSpans.First().Snapshot, commentInfo, cancellationToken).ConfigureAwait(false);
var blockCommentedSpans = blockCommentDataProvider.GetBlockCommentsInDocument();
var blockCommentSelections = selectedSpans.SelectAsArray(span => new BlockCommentSelectionHelper(blockCommentedSpans, span));
var returnOperation = Operation.Uncomment;
......@@ -108,7 +114,7 @@ public bool ExecuteCommand(CommentSelectionCommandArgs args, CommandExecutionCon
trackingSpans.Clear();
foreach (var blockCommentSelection in blockCommentSelections)
{
BlockCommentSpan(blockCommentSelection, blockCommentDataProvider, textChanges, trackingSpans, commentInfo);
BlockCommentSpan(blockCommentSelection, navigator, textChanges, trackingSpans, commentInfo);
}
}
......@@ -145,7 +151,7 @@ public bool ExecuteCommand(CommentSelectionCommandArgs args, CommandExecutionCon
}
}
private static void BlockCommentSpan(BlockCommentSelectionHelper blockCommentSelection, IToggleBlockCommentDocumentDataProvider blockCommentDataProvider,
private static void BlockCommentSpan(BlockCommentSelectionHelper blockCommentSelection, ITextStructureNavigator navigator,
ArrayBuilder<TextChange> textChanges, ArrayBuilder<CommentTrackingSpan> trackingSpans, CommentSelectionInfo commentInfo)
{
// Add sequential block comments if the selection contains any intersecting comments.
......@@ -159,9 +165,8 @@ public bool ExecuteCommand(CommentSelectionCommandArgs args, CommandExecutionCon
var spanToAdd = blockCommentSelection.SelectedSpan;
if (spanToAdd.IsEmpty)
{
// The location for the comment should be the caret or the location after the end of the token the caret is inside of.
var locationAfterToken = blockCommentDataProvider.GetEmptyCommentStartLocation(spanToAdd.Start);
spanToAdd = TextSpan.FromBounds(locationAfterToken, locationAfterToken);
var caretLocation = GetCaretLocationAfterToken(navigator, blockCommentSelection);
spanToAdd = TextSpan.FromBounds(caretLocation, caretLocation);
}
trackingSpans.Add(new CommentTrackingSpan(spanToAdd));
......@@ -169,6 +174,33 @@ public bool ExecuteCommand(CommentSelectionCommandArgs args, CommandExecutionCon
}
}
/// <summary>
/// Returns a caret location of itself or the location after the token the caret is inside of.
/// </summary>
private static int GetCaretLocationAfterToken(ITextStructureNavigator navigator, BlockCommentSelectionHelper blockCommentSelection)
{
var snapshotSpan = blockCommentSelection.SnapshotSpan;
if (navigator == null)
{
return snapshotSpan.Start;
}
var extent = navigator.GetExtentOfWord(snapshotSpan.Start);
int locationAfterToken = extent.Span.End;
// Don't move to the end if it's already before the token.
if (snapshotSpan.Start == extent.Span.Start)
{
locationAfterToken = extent.Span.Start;
}
// If the 'word' is just whitespace, use the selected location.
if (blockCommentSelection.IsSpanWhitespace(TextSpan.FromBounds(extent.Span.Start, extent.Span.End)))
{
locationAfterToken = snapshotSpan.Start;
}
return locationAfterToken;
}
/// <summary>
/// Adds a block comment when the selection already contains block comment(s).
/// The result will be sequential block comments with the entire selection being commented out.
......@@ -235,7 +267,8 @@ private class BlockCommentSelectionHelper
/// Trimmed text of the selection.
/// </summary>
private readonly string _trimmedText;
private readonly ITextSnapshot _snapshot;
public SnapshotSpan SnapshotSpan { get; }
public TextSpan SelectedSpan { get; }
......@@ -246,7 +279,7 @@ private class BlockCommentSelectionHelper
public BlockCommentSelectionHelper(ImmutableArray<TextSpan> allBlockComments, SnapshotSpan selectedSnapshotSpan)
{
_trimmedText = selectedSnapshotSpan.GetText().Trim();
_snapshot = selectedSnapshotSpan.Snapshot;
SnapshotSpan = selectedSnapshotSpan;
SelectedSpan = TextSpan.FromBounds(selectedSnapshotSpan.Start, selectedSnapshotSpan.End);
IntersectingBlockComments = GetIntersectingBlockComments(allBlockComments, SelectedSpan);
......@@ -260,7 +293,7 @@ public bool IsSpanWhitespace(TextSpan span)
{
for (var i = span.Start; i < span.End; i++)
{
if (!char.IsWhiteSpace(_snapshot[i]))
if (!char.IsWhiteSpace(SnapshotSpan.Snapshot[i]))
{
return false;
}
......@@ -315,7 +348,7 @@ public bool HasIntersectingBlockComments()
public string GetSubstringFromText(int position, int length)
{
return _snapshot.GetText().Substring(position, length);
return SnapshotSpan.Snapshot.GetText().Substring(position, length);
}
/// <summary>
......@@ -325,14 +358,15 @@ public string GetSubstringFromText(int position, int length)
/// </summary>
public bool TryGetBlockCommentOnSameLine(ImmutableArray<TextSpan> allBlockComments, out TextSpan commentedSpanOnSameLine)
{
var selectedLine = _snapshot.GetLineFromPosition(SelectedSpan.Start);
var snapshot = SnapshotSpan.Snapshot;
var selectedLine = snapshot.GetLineFromPosition(SelectedSpan.Start);
var lineStartToCaretIsWhitespace = IsSpanWhitespace(TextSpan.FromBounds(selectedLine.Start, SelectedSpan.Start));
var caretToLineEndIsWhitespace = IsSpanWhitespace(TextSpan.FromBounds(SelectedSpan.Start, selectedLine.End));
foreach (var blockComment in allBlockComments)
{
if (lineStartToCaretIsWhitespace
&& SelectedSpan.Start < blockComment.Start
&& _snapshot.AreOnSameLine(SelectedSpan.Start, blockComment.Start))
&& snapshot.AreOnSameLine(SelectedSpan.Start, blockComment.Start))
{
if (IsSpanWhitespace(TextSpan.FromBounds(SelectedSpan.Start, blockComment.Start)))
{
......@@ -342,7 +376,7 @@ public bool TryGetBlockCommentOnSameLine(ImmutableArray<TextSpan> allBlockCommen
}
else if (caretToLineEndIsWhitespace
&& SelectedSpan.Start > blockComment.End
&& _snapshot.AreOnSameLine(SelectedSpan.Start, blockComment.End))
&& snapshot.AreOnSameLine(SelectedSpan.Start, blockComment.End))
{
if (IsSpanWhitespace(TextSpan.FromBounds(blockComment.End, SelectedSpan.Start)))
{
......
......@@ -81,7 +81,7 @@ public bool ExecuteCommand(UncommentSelectionCommandArgs args, CommandExecutionC
/// Internal so that it can be called by unit tests.
/// </summary>
internal override Task<CommentSelectionResult> CollectEdits(
Document document, ICommentSelectionService service, NormalizedSnapshotSpanCollection selectedSpans,
Document document, ICommentSelectionService service, ITextBuffer subjectBuffer, NormalizedSnapshotSpanCollection selectedSpans,
Operation operation, CancellationToken cancellationToken)
{
var spanTrackingList = ArrayBuilder<CommentTrackingSpan>.GetInstance();
......
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis.Text;
namespace Microsoft.CodeAnalysis.Editor.Implementation.CommentSelection
{
interface IToggleBlockCommentDocumentDataProvider
{
/// <summary>
/// Gets the location to insert an empty comment.
/// </summary>
int GetEmptyCommentStartLocation(int location);
/// <summary>
/// Gets all block comments in a particular document.
/// </summary>
ImmutableArray<TextSpan> GetBlockCommentsInDocument();
}
}
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel.Composition;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CommentSelection;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Operations;
using Microsoft.VisualStudio.Utilities;
......@@ -23,19 +27,38 @@ internal class ToggleBlockCommentCommandHandler : AbstractToggleBlockCommentBase
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal ToggleBlockCommentCommandHandler(
ITextUndoHistoryRegistry undoHistoryRegistry,
IEditorOperationsFactoryService editorOperationsFactoryService)
: base(undoHistoryRegistry, editorOperationsFactoryService)
IEditorOperationsFactoryService editorOperationsFactoryService,
ITextStructureNavigatorSelectorService navigatorSelectorService)
: base(undoHistoryRegistry, editorOperationsFactoryService, navigatorSelectorService)
{
}
/// <summary>
/// Gets the default text based document data provider for block comments.
/// Gets block comments by parsing the text for comment markers.
/// </summary>
protected override Task<IToggleBlockCommentDocumentDataProvider> GetBlockCommentDocumentDataProvider(Document document, ITextSnapshot snapshot,
protected override Task<ImmutableArray<TextSpan>> GetBlockCommentsInDocument(Document document, ITextSnapshot snapshot,
CommentSelectionInfo commentInfo, CancellationToken cancellationToken)
{
IToggleBlockCommentDocumentDataProvider provider = new ToggleBlockCommentDocumentDataProvider(snapshot, commentInfo);
return Task.FromResult(provider);
var allText = snapshot.AsText();
var commentedSpans = new List<TextSpan>();
var openIdx = 0;
while ((openIdx = allText.IndexOf(commentInfo.BlockCommentStartString, openIdx, caseSensitive: true)) >= 0)
{
// Retrieve the first closing marker located after the open index.
var closeIdx = allText.IndexOf(commentInfo.BlockCommentEndString, openIdx + commentInfo.BlockCommentStartString.Length, caseSensitive: true);
// If an open marker is found without a close marker, it's an unclosed comment.
if (closeIdx < 0)
{
closeIdx = allText.Length - commentInfo.BlockCommentEndString.Length;
}
var blockCommentSpan = new TextSpan(openIdx, closeIdx + commentInfo.BlockCommentEndString.Length - openIdx);
commentedSpans.Add(blockCommentSpan);
openIdx = closeIdx;
}
return Task.FromResult(commentedSpans.ToImmutableArray());
}
}
}
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis.CommentSelection;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Text;
namespace Microsoft.CodeAnalysis.Editor.Implementation.CommentSelection
{
/// <summary>
/// Provides a language agnostic way to retrieve data about block comments.
/// </summary>
internal class ToggleBlockCommentDocumentDataProvider : IToggleBlockCommentDocumentDataProvider
{
private readonly ITextSnapshot _snapshot;
private readonly CommentSelectionInfo _commentInfo;
public ToggleBlockCommentDocumentDataProvider(ITextSnapshot textSnapshot, CommentSelectionInfo commentInfo)
{
_snapshot = textSnapshot;
_commentInfo = commentInfo;
}
public int GetEmptyCommentStartLocation(int location)
{
return location;
}
public ImmutableArray<TextSpan> GetBlockCommentsInDocument()
{
var allText = _snapshot.AsText();
var commentedSpans = new List<TextSpan>();
var openIdx = 0;
while ((openIdx = allText.IndexOf(_commentInfo.BlockCommentStartString, openIdx, caseSensitive: true)) >= 0)
{
// Retrieve the first closing marker located after the open index.
var closeIdx = allText.IndexOf(_commentInfo.BlockCommentEndString, openIdx + _commentInfo.BlockCommentStartString.Length, caseSensitive: true);
// If an open marker is found without a close marker, it's an unclosed comment.
if (closeIdx < 0)
{
closeIdx = allText.Length - _commentInfo.BlockCommentEndString.Length;
}
var blockCommentSpan = new TextSpan(openIdx, closeIdx + _commentInfo.BlockCommentEndString.Length - openIdx);
commentedSpans.Add(blockCommentSpan);
openIdx = closeIdx;
}
return commentedSpans.ToImmutableArray();
}
}
}
......@@ -14,56 +14,6 @@ namespace Microsoft.CodeAnalysis.Editor.UnitTests.CommentSelection
[UseExportProvider]
public class CSharpToggleBlockCommentCommandHandlerTests : AbstractToggleBlockCommentTestBase
{
[WpfFact, Trait(Traits.Feature, Traits.Features.ToggleBlockComment)]
public void AddComment_CaretInsideToken()
{
var markup =
@"
class C
{
void M()
{
va$$r i = 1;
}
}";
var expected =
@"
class C
{
void M()
{
var[|/**/|] i = 1;
}
}";
ToggleBlockComment(markup, expected);
}
[WpfFact, Trait(Traits.Feature, Traits.Features.ToggleBlockComment)]
public void AddComment_CaretInsideOperatorToken()
{
var markup = @"
class C
{
void M()
{
Func<int, bool> myFunc = x =$$> x == 5;
}
}";
var expected =
@"
class C
{
void M()
{
Func<int, bool> myFunc = x =>[|/**/|] x == 5;
}
}";
ToggleBlockComment(markup, expected);
}
[WpfFact, Trait(Traits.Feature, Traits.Features.ToggleBlockComment)]
public void AddComment_CommentMarkerStringBeforeSelection()
{
......
......@@ -768,7 +768,7 @@ private static void CommentSelection(ITextView textView, IEnumerable<TextChange>
var service = new MockCommentSelectionService(supportBlockComments);
var edits = commandHandler.CollectEdits(
null, service, textView.Selection.GetSnapshotSpansOnBuffer(textView.TextBuffer), operation, CancellationToken.None).GetAwaiter().GetResult();
null, service, textView.TextBuffer, textView.Selection.GetSnapshotSpansOnBuffer(textView.TextBuffer), operation, CancellationToken.None).GetAwaiter().GetResult();
Roslyn.Test.Utilities.AssertEx.SetEqual(expectedChanges, edits.TextChanges);
......
......@@ -149,7 +149,7 @@ class C
{
void M()
{
va[|/**/|]r i = 1;
var[|/**/|] i = 1;
}
}";
......@@ -173,7 +173,32 @@ class C
{
void M()
{
Func<int, bool> myFunc = x =[|/**/|]> x == 5;
Func<int, bool> myFunc = x =>[|/**/|] x == 5;
}
}";
ToggleBlockComment(markup, expected);
}
[WpfFact, Trait(Traits.Feature, Traits.Features.ToggleBlockComment)]
public void AddComment_CaretInsideNewline()
{
var markup =
@"
class C
{
void M()
{
var i = 1;$$
}
}";
var expected =
@"
class C
{
void M()
{
var i = 1;[|/**/|]
}
}";
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册