/
githubmirror
/
roslyn
Обзор
Документация
Войти
/
githubmirror
/
roslyn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/EditorFeatures/Core/CommentSelection/CommentUncommentSelectionCommandHandler.cs
350 строк
16 KB
Cyrus Najmabadi
Make sealed
26 мар 2025, 22:21
26 мар 2025, 22:21
d99b771
Код
Авторство
О чём код?
// 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.Immutable; using System.ComponentModel.Composition; using System.Diagnostics.CodeAnalysis; using System.Threading; using Microsoft.CodeAnalysis.Editor; using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.Text.Shared.Extensions; using Microsoft.VisualStudio.Commanding; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; using Microsoft.VisualStudio.Text.Operations; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CommentSelection; [Export(typeof(ICommandHandler))] [VisualStudio.Utilities.ContentType(ContentTypeNames.RoslynContentType)] [VisualStudio.Utilities.Name(PredefinedCommandHandlerNames.CommentSelection)] [method: ImportingConstructor] [method: SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] internal sealed class CommentUncommentSelectionCommandHandler( ITextUndoHistoryRegistry undoHistoryRegistry, IEditorOperationsFactoryService editorOperationsFactoryService, EditorOptionsService editorOptionsService) : AbstractCommentSelectionBase<Operation>(undoHistoryRegistry, editorOperationsFactoryService, editorOptionsService), ICommandHandler<CommentSelectionCommandArgs>, ICommandHandler<UncommentSelectionCommandArgs> { public CommandState GetCommandState(CommentSelectionCommandArgs args) => GetCommandState(args.SubjectBuffer); /// <summary> /// Comment the selected spans, and reset the selection. /// </summary> public bool ExecuteCommand(CommentSelectionCommandArgs args, CommandExecutionContext context) => this.ExecuteCommand(args.TextView, args.SubjectBuffer, Operation.Comment, context); public CommandState GetCommandState(UncommentSelectionCommandArgs args) => GetCommandState(args.SubjectBuffer); /// <summary> /// Uncomment the selected spans, and reset the selection. /// </summary> public bool ExecuteCommand(UncommentSelectionCommandArgs args, CommandExecutionContext context) => this.ExecuteCommand(args.TextView, args.SubjectBuffer, Operation.Uncomment, context); public override string DisplayName => EditorFeaturesResources.Comment_Uncomment_Selection; protected override string GetTitle(Operation operation) => operation == Operation.Comment ? EditorFeaturesResources.Comment_Selection : EditorFeaturesResources.Uncomment_Selection; protected override string GetMessage(Operation operation) => operation == Operation.Comment ? EditorFeaturesResources.Commenting_currently_selected_text : EditorFeaturesResources.Uncommenting_currently_selected_text; /// <summary> /// Add the necessary edits to the given spans. Also collect tracking spans over each span. /// /// Internal so that it can be called by unit tests. /// </summary> internal override CommentSelectionResult CollectEdits( Document document, ICommentSelectionService service, ITextBuffer subjectBuffer, NormalizedSnapshotSpanCollection selectedSpans, Operation operation, CancellationToken cancellationToken) { var spanTrackingList = ArrayBuilder<CommentTrackingSpan>.GetInstance(); var textChanges = ArrayBuilder<TextChange>.GetInstance(); foreach (var span in selectedSpans) { if (operation == Operation.Comment) { CommentSpan(service, span, textChanges, spanTrackingList); } else { UncommentSpan(service, span, textChanges, spanTrackingList); } } return new CommentSelectionResult(textChanges.ToArrayAndFree(), spanTrackingList.ToArrayAndFree(), operation); } /// <summary> /// Add the necessary edits to comment out a single span. /// </summary> private static void CommentSpan( ICommentSelectionService service, SnapshotSpan span, ArrayBuilder<TextChange> textChanges, ArrayBuilder<CommentTrackingSpan> trackingSpans) { var (firstLine, lastLine) = DetermineFirstAndLastLine(span); if (span.IsEmpty && firstLine.IsEmptyOrWhitespace()) { // No selection, and on an empty line, don't do anything. return; } if (!span.IsEmpty && string.IsNullOrWhiteSpace(span.GetText())) { // Just whitespace selected, don't do anything. return; } // Get the information from the language as to how they'd like to comment this region. var commentInfo = service.GetInfo(); if (!commentInfo.SupportsBlockComment && !commentInfo.SupportsSingleLineComment) { // Neither type of comment supported. return; } if (commentInfo.SupportsBlockComment && !commentInfo.SupportsSingleLineComment) { // Only block comments supported here. If there is a span, just surround that // span with a block comment. If tehre is no span then surround the entire line // with a block comment. if (span.IsEmpty) { var firstNonWhitespaceOnLine = firstLine.GetFirstNonWhitespacePosition(); var insertPosition = firstNonWhitespaceOnLine ?? firstLine.Start; span = new SnapshotSpan(span.Snapshot, Span.FromBounds(insertPosition, firstLine.End)); } AddBlockComment(span, textChanges, trackingSpans, commentInfo); } else if (!commentInfo.SupportsBlockComment && commentInfo.SupportsSingleLineComment) { // Only single line comments supported here. AddSingleLineComments(span, textChanges, trackingSpans, firstLine, lastLine, commentInfo); } else { // both comment forms supported. Do a block comment only if a portion of code is // selected on a single line, otherwise comment out all the lines using single-line // comments. if (!span.IsEmpty && !SpanIncludesAllTextOnIncludedLines(span) && firstLine.LineNumber == lastLine.LineNumber) { AddBlockComment(span, textChanges, trackingSpans, commentInfo); } else { AddSingleLineComments(span, textChanges, trackingSpans, firstLine, lastLine, commentInfo); } } } private static void AddSingleLineComments(SnapshotSpan span, ArrayBuilder<TextChange> textChanges, ArrayBuilder<CommentTrackingSpan> trackingSpans, ITextSnapshotLine firstLine, ITextSnapshotLine lastLine, CommentSelectionInfo commentInfo) { // Select the entirety of the lines, so that another comment operation will add more // comments, not insert block comments. trackingSpans.Add(new CommentTrackingSpan(TextSpan.FromBounds(firstLine.Start.Position, lastLine.End.Position))); var indentToCommentAt = DetermineSmallestIndent(span, firstLine, lastLine); ApplySingleLineCommentToNonBlankLines(commentInfo, textChanges, firstLine, lastLine, indentToCommentAt); } private static void AddBlockComment(SnapshotSpan span, ArrayBuilder<TextChange> textChanges, ArrayBuilder<CommentTrackingSpan> trackingSpans, CommentSelectionInfo commentInfo) { trackingSpans.Add(new CommentTrackingSpan(TextSpan.FromBounds(span.Start, span.End))); InsertText(textChanges, span.Start, commentInfo.BlockCommentStartString); InsertText(textChanges, span.End, commentInfo.BlockCommentEndString); } /// <summary> /// Add the necessary edits to uncomment out a single span. /// </summary> private static void UncommentSpan( ICommentSelectionService service, SnapshotSpan span, ArrayBuilder<TextChange> textChanges, ArrayBuilder<CommentTrackingSpan> spansToSelect) { var info = service.GetInfo(); // If the selection is exactly a block comment, use it as priority over single line comments. if (info.SupportsBlockComment && TryUncommentExactlyBlockComment(info, span, textChanges, spansToSelect)) { return; } if (info.SupportsSingleLineComment && TryUncommentSingleLineComments(info, span, textChanges, spansToSelect)) { return; } // We didn't make any single line changes. If the language supports block comments, see // if we're inside a containing block comment and uncomment that. if (info.SupportsBlockComment) { UncommentContainingBlockComment(info, span, textChanges, spansToSelect); } } /// <summary> /// Check if the selected span matches an entire block comment. /// If it does, uncomment it and return true. /// </summary> private static bool TryUncommentExactlyBlockComment(CommentSelectionInfo info, SnapshotSpan span, ArrayBuilder<TextChange> textChanges, ArrayBuilder<CommentTrackingSpan> spansToSelect) { var spanText = span.GetText(); var trimmedSpanText = spanText.Trim(); // See if the selection includes just a block comment (plus whitespace) if (trimmedSpanText.StartsWith(info.BlockCommentStartString, StringComparison.Ordinal) && trimmedSpanText.EndsWith(info.BlockCommentEndString, StringComparison.Ordinal)) { var positionOfStart = span.Start + spanText.IndexOf(info.BlockCommentStartString, StringComparison.Ordinal); var positionOfEnd = span.Start + spanText.LastIndexOf(info.BlockCommentEndString, StringComparison.Ordinal); UncommentPosition(info, textChanges, spansToSelect, positionOfStart, positionOfEnd); return true; } return false; } private static void UncommentContainingBlockComment(CommentSelectionInfo info, SnapshotSpan span, ArrayBuilder<TextChange> textChanges, ArrayBuilder<CommentTrackingSpan> spansToSelect) { // See if we are (textually) contained in a block comment. // This could allow a selection that spans multiple block comments to uncomment the beginning of // the first and end of the last. Oh well. var positionOfEnd = -1; var text = span.Snapshot.AsText(); var positionOfStart = text.LastIndexOf(info.BlockCommentStartString, span.Start, caseSensitive: true); // If we found a start comment marker, make sure there isn't an end comment marker after it but before our span. if (positionOfStart >= 0) { var lastEnd = text.LastIndexOf(info.BlockCommentEndString, span.Start, caseSensitive: true); if (lastEnd < positionOfStart) { positionOfEnd = text.IndexOf(info.BlockCommentEndString, span.End, caseSensitive: true); } else if (lastEnd + info.BlockCommentEndString.Length > span.End) { // The end of the span is *inside* the end marker, so searching backwards found it. positionOfEnd = lastEnd; } } UncommentPosition(info, textChanges, spansToSelect, positionOfStart, positionOfEnd); } private static void UncommentPosition(CommentSelectionInfo info, ArrayBuilder<TextChange> textChanges, ArrayBuilder<CommentTrackingSpan> spansToSelect, int positionOfStart, int positionOfEnd) { if (positionOfStart < 0 || positionOfEnd < 0) { return; } spansToSelect.Add(new CommentTrackingSpan(TextSpan.FromBounds(positionOfStart, positionOfEnd + info.BlockCommentEndString.Length))); DeleteText(textChanges, new TextSpan(positionOfStart, info.BlockCommentStartString.Length)); DeleteText(textChanges, new TextSpan(positionOfEnd, info.BlockCommentEndString.Length)); } private static bool TryUncommentSingleLineComments(CommentSelectionInfo info, SnapshotSpan span, ArrayBuilder<TextChange> textChanges, ArrayBuilder<CommentTrackingSpan> spansToSelect) { // First see if we're selecting any lines that have the single-line comment prefix. // If so, then we'll just remove the single-line comment prefix from those lines. var (firstLine, lastLine) = DetermineFirstAndLastLine(span); for (var lineNumber = firstLine.LineNumber; lineNumber <= lastLine.LineNumber; ++lineNumber) { var line = span.Snapshot.GetLineFromLineNumber(lineNumber); var lineText = line.GetText(); if (lineText.Trim().StartsWith(info.SingleLineCommentString, StringComparison.Ordinal)) { DeleteText(textChanges, new TextSpan(line.Start.Position + lineText.IndexOf(info.SingleLineCommentString, StringComparison.Ordinal), info.SingleLineCommentString.Length)); } } // If we made any changes, select the entirety of the lines we change, so that subsequent invocations will // affect the same lines. if (textChanges.Count == 0) { return false; } spansToSelect.Add(new CommentTrackingSpan(TextSpan.FromBounds(firstLine.Start.Position, lastLine.End.Position))); return true; } /// <summary> /// Adds edits to comment out each non-blank line, at the given indent. /// </summary> private static void ApplySingleLineCommentToNonBlankLines( CommentSelectionInfo info, ArrayBuilder<TextChange> textChanges, ITextSnapshotLine firstLine, ITextSnapshotLine lastLine, int indentToCommentAt) { var snapshot = firstLine.Snapshot; for (var lineNumber = firstLine.LineNumber; lineNumber <= lastLine.LineNumber; ++lineNumber) { var line = snapshot.GetLineFromLineNumber(lineNumber); if (!line.IsEmptyOrWhitespace()) { InsertText(textChanges, line.Start + indentToCommentAt, info.SingleLineCommentString); } } } /// <summary> /// Given a span, find the first and last line that are part of the span. NOTE: If the /// span ends in column zero, we back up to the previous line, to handle the case where /// the user used shift + down to select a bunch of lines. They probably don't want the /// last line commented in that case. /// </summary> private static (ITextSnapshotLine firstLine, ITextSnapshotLine lastLine) DetermineFirstAndLastLine(SnapshotSpan span) { var firstLine = span.Snapshot.GetLineFromPosition(span.Start.Position); var lastLine = span.Snapshot.GetLineFromPosition(span.End.Position); if (lastLine.Start == span.End.Position && !span.IsEmpty) { lastLine = lastLine.GetPreviousMatchingLine(_ => true); } return (firstLine, lastLine); } /// <summary> /// Returns true if the span includes all of the non-whitespace text on the first and last line. /// </summary> private static bool SpanIncludesAllTextOnIncludedLines(SnapshotSpan span) { var (firstLine, lastLine) = DetermineFirstAndLastLine(span); var firstNonWhitespacePosition = firstLine.GetFirstNonWhitespacePosition(); var lastNonWhitespacePosition = lastLine.GetLastNonWhitespacePosition(); var allOnFirst = !firstNonWhitespacePosition.HasValue || span.Start.Position <= firstNonWhitespacePosition.Value; var allOnLast = !lastNonWhitespacePosition.HasValue || span.End.Position > lastNonWhitespacePosition.Value; return allOnFirst && allOnLast; } }