|
// 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.Generic;
using System.Collections.Immutable;
using System.ComponentModel.Composition;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.Editor;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Internal.Log;
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.ToggleLineComment)]
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal class ToggleLineCommentCommandHandler(
ITextUndoHistoryRegistry undoHistoryRegistry,
IEditorOperationsFactoryService editorOperationsFactoryService,
EditorOptionsService editorOptionsService) :
// Value tuple to represent that there is no distinct command to be passed in.
AbstractCommentSelectionBase<ValueTuple>(undoHistoryRegistry, editorOperationsFactoryService, editorOptionsService),
ICommandHandler<ToggleLineCommentCommandArgs>
{
private static readonly CommentSelectionResult s_emptyCommentSelectionResult =
new([], [], Operation.Uncomment);
public CommandState GetCommandState(ToggleLineCommentCommandArgs args)
=> GetCommandState(args.SubjectBuffer);
public bool ExecuteCommand(ToggleLineCommentCommandArgs args, CommandExecutionContext context)
=> ExecuteCommand(args.TextView, args.SubjectBuffer, ValueTuple.Create(), context);
public override string DisplayName => EditorFeaturesResources.Toggle_Line_Comment;
protected override string GetTitle(ValueTuple command) => EditorFeaturesResources.Toggle_Line_Comment;
protected override string GetMessage(ValueTuple command) => EditorFeaturesResources.Toggling_line_comment;
internal override CommentSelectionResult CollectEdits(Document document, ICommentSelectionService service,
ITextBuffer subjectBuffer, NormalizedSnapshotSpanCollection selectedSpans, ValueTuple command, CancellationToken cancellationToken)
{
using (Logger.LogBlock(FunctionId.CommandHandler_ToggleLineComment, KeyValueLogMessage.Create(LogType.UserAction, m =>
{
m[LanguageNameString] = document.Project.Language;
m[LengthString] = subjectBuffer.CurrentSnapshot.Length;
}), cancellationToken))
{
var commentInfo = service.GetInfo();
if (commentInfo.SupportsSingleLineComment)
{
return ToggleLineComment(commentInfo, selectedSpans);
}
return s_emptyCommentSelectionResult;
}
}
private static CommentSelectionResult ToggleLineComment(CommentSelectionInfo commentInfo,
NormalizedSnapshotSpanCollection selectedSpans)
{
var textChanges = ArrayBuilder<TextChange>.GetInstance();
var trackingSpans = ArrayBuilder<CommentTrackingSpan>.GetInstance();
var linesInSelections = selectedSpans.ToDictionary(
span => span,
span => GetLinesFromSelectedSpan(span).ToImmutableArray());
var isMultiCaret = selectedSpans.Count > 1;
Operation operation;
// If any of the lines are uncommented, add comments.
if (linesInSelections.Values.Any(lines => SelectionHasUncommentedLines(lines, commentInfo)))
{
foreach (var selection in linesInSelections)
{
CommentLines(selection.Key, selection.Value, textChanges, trackingSpans, commentInfo);
}
operation = Operation.Comment;
}
else
{
foreach (var selection in linesInSelections)
{
UncommentLines(selection.Key, selection.Value, textChanges, trackingSpans, commentInfo);
}
operation = Operation.Uncomment;
}
return new CommentSelectionResult(textChanges, trackingSpans, operation);
}
private static void UncommentLines(
SnapshotSpan selectedSpan,
ImmutableArray<ITextSnapshotLine> commentedLines,
ArrayBuilder<TextChange> textChanges,
ArrayBuilder<CommentTrackingSpan> trackingSpans,
CommentSelectionInfo commentInfo)
{
foreach (var line in commentedLines)
{
if (!line.IsEmptyOrWhitespace())
{
var text = line.GetText();
var commentIndex = text.IndexOf(commentInfo.SingleLineCommentString) + line.Start;
var spanToRemove = TextSpan.FromBounds(commentIndex, commentIndex + commentInfo.SingleLineCommentString.Length);
DeleteText(textChanges, spanToRemove);
}
}
var commentTrackingSpan = new CommentTrackingSpan(selectedSpan.Span.ToTextSpan());
trackingSpans.Add(commentTrackingSpan);
}
private static void CommentLines(
SnapshotSpan selectedSpan,
ImmutableArray<ITextSnapshotLine> linesInSelection,
ArrayBuilder<TextChange> textChanges,
ArrayBuilder<CommentTrackingSpan> trackingSpans,
CommentSelectionInfo commentInfo)
{
var indentation = DetermineSmallestIndent(selectedSpan, linesInSelection.First(), linesInSelection.Last());
foreach (var line in linesInSelection)
{
if (!line.IsEmptyOrWhitespace())
{
InsertText(textChanges, line.Start + indentation, commentInfo.SingleLineCommentString);
}
}
var commentTrackingSpan = new CommentTrackingSpan(selectedSpan.Span.ToTextSpan());
trackingSpans.Add(commentTrackingSpan);
}
private static List<ITextSnapshotLine> GetLinesFromSelectedSpan(SnapshotSpan span)
{
var lines = new List<ITextSnapshotLine>();
var startLine = span.Snapshot.GetLineFromPosition(span.Start);
var endLine = span.Snapshot.GetLineFromPosition(span.End);
// Don't include the last line if the span is just the start of the line.
if (endLine.Start == span.End.Position && !span.IsEmpty)
{
endLine = endLine.GetPreviousMatchingLine(_ => true);
}
if (startLine.LineNumber <= endLine.LineNumber)
{
for (var i = startLine.LineNumber; i <= endLine.LineNumber; i++)
{
lines.Add(span.Snapshot.GetLineFromLineNumber(i));
}
}
return lines;
}
private static bool SelectionHasUncommentedLines(ImmutableArray<ITextSnapshotLine> linesInSelection, CommentSelectionInfo commentInfo)
=> linesInSelection.Any(static (l, commentInfo) => !IsLineCommentedOrEmpty(l, commentInfo), commentInfo);
private static bool IsLineCommentedOrEmpty(ITextSnapshotLine line, CommentSelectionInfo info)
{
var lineText = line.GetText();
// We don't add / remove anything for empty lines.
return lineText.Trim().StartsWith(info.SingleLineCommentString, StringComparison.Ordinal) || line.IsEmptyOrWhitespace();
}
}
|