diff --git a/src/EditorFeatures/Core/EditorFeaturesResources.Designer.cs b/src/EditorFeatures/Core/EditorFeaturesResources.Designer.cs
index 7fb5ff64856b00f19398809d9ca43676ede68d78..b9888f619bb7547fd7bbe359fcb23a0442dbdef3 100644
--- a/src/EditorFeatures/Core/EditorFeaturesResources.Designer.cs
+++ b/src/EditorFeatures/Core/EditorFeaturesResources.Designer.cs
@@ -1907,7 +1907,29 @@ internal class EditorFeaturesResources {
return ResourceManager.GetString("Toggling_block_comment", resourceCulture);
}
}
-
+
+ ///
+ /// Looks up a localized string similar to Toggle Line Comment.
+ ///
+ internal static string Toggle_Line_Comment
+ {
+ get
+ {
+ return ResourceManager.GetString("Toggle_Line_Comment", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Toggling line comment....
+ ///
+ internal static string Toggling_line_comment
+ {
+ get
+ {
+ return ResourceManager.GetString("Toggling_line_comment", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Type Parts.
///
diff --git a/src/EditorFeatures/Core/EditorFeaturesResources.resx b/src/EditorFeatures/Core/EditorFeaturesResources.resx
index 2eca216a14bccd006f65f9e6723a3a9291f63ef4..b191ad9139a69adb62f60283ec47e71ea909c84c 100644
--- a/src/EditorFeatures/Core/EditorFeaturesResources.resx
+++ b/src/EditorFeatures/Core/EditorFeaturesResources.resx
@@ -895,4 +895,10 @@ Do you want to proceed?
Toggling block comment...
+
+ Toggle Line Comment
+
+
+ Toggling line comment...
+
\ No newline at end of file
diff --git a/src/EditorFeatures/Core/Extensibility/Commands/PredefinedCommandHandlerNames.cs b/src/EditorFeatures/Core/Extensibility/Commands/PredefinedCommandHandlerNames.cs
index a6d007450f955932c14568532842e14fc525461e..9a9575c475709c771b28b8c7b17ac1badbe8c117 100644
--- a/src/EditorFeatures/Core/Extensibility/Commands/PredefinedCommandHandlerNames.cs
+++ b/src/EditorFeatures/Core/Extensibility/Commands/PredefinedCommandHandlerNames.cs
@@ -147,6 +147,12 @@ internal static class PredefinedCommandHandlerNames
///
public const string ToggleBlockComment = "Toggle Block Comment Command Handler";
+ ///
+ /// Command handler name for Toggle Line Comments.
+ ///
+ ///
+ public const string ToggleLineComment = "Toggle Line Comment Command Handler";
+
///
/// Command handler name for Paste Content in Interactive Format.
///
diff --git a/src/EditorFeatures/Core/Implementation/CommentSelection/AbstractCommentSelectionBase.cs b/src/EditorFeatures/Core/Implementation/CommentSelection/AbstractCommentSelectionBase.cs
index 80058b677e608b1a33af3a4cd60c2092ce39c107..0356f80eab6c407a4e00d38b5bb56014bf78b02a 100644
--- a/src/EditorFeatures/Core/Implementation/CommentSelection/AbstractCommentSelectionBase.cs
+++ b/src/EditorFeatures/Core/Implementation/CommentSelection/AbstractCommentSelectionBase.cs
@@ -1,5 +1,6 @@
// 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.Linq;
@@ -11,6 +12,7 @@
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;
@@ -200,5 +202,26 @@ private static Document Format(ICommentSelectionService service, ITextSnapshot s
var textSpans = changes.SelectAsArray(change => change.Span.ToTextSpan());
return service.FormatAsync(document, textSpans, cancellationToken).WaitAndGetResult(cancellationToken);
}
+
+ ///
+ /// Given a set of lines, find the minimum indent of all of the non-blank, non-whitespace lines.
+ ///
+ protected static int DetermineSmallestIndent(
+ SnapshotSpan span, ITextSnapshotLine firstLine, ITextSnapshotLine lastLine)
+ {
+ // TODO: This breaks if you have mixed tabs/spaces, and/or tabsize != indentsize.
+ var indentToCommentAt = int.MaxValue;
+ for (var lineNumber = firstLine.LineNumber; lineNumber <= lastLine.LineNumber; ++lineNumber)
+ {
+ var line = span.Snapshot.GetLineFromLineNumber(lineNumber);
+ var firstNonWhitespacePosition = line.GetFirstNonWhitespacePosition();
+ var firstNonWhitespaceOnLine = firstNonWhitespacePosition.HasValue
+ ? firstNonWhitespacePosition.Value - line.Start
+ : int.MaxValue;
+ indentToCommentAt = Math.Min(indentToCommentAt, firstNonWhitespaceOnLine);
+ }
+
+ return indentToCommentAt;
+ }
}
}
diff --git a/src/EditorFeatures/Core/Implementation/CommentSelection/CommentUncommentSelectionCommandHandler.cs b/src/EditorFeatures/Core/Implementation/CommentSelection/CommentUncommentSelectionCommandHandler.cs
index ede5e165dc21e7978b13cad14919034aed26f706..b128eebe318752368226b73d1a46664b4cc5ba6d 100644
--- a/src/EditorFeatures/Core/Implementation/CommentSelection/CommentUncommentSelectionCommandHandler.cs
+++ b/src/EditorFeatures/Core/Implementation/CommentSelection/CommentUncommentSelectionCommandHandler.cs
@@ -321,25 +321,6 @@ private void AddBlockComment(SnapshotSpan span, ArrayBuilder textCha
}
}
- /// Given a set of lines, find the minimum indent of all of the non-blank, non-whitespace lines.
- private static int DetermineSmallestIndent(
- SnapshotSpan span, ITextSnapshotLine firstLine, ITextSnapshotLine lastLine)
- {
- // TODO: This breaks if you have mixed tabs/spaces, and/or tabsize != indentsize.
- var indentToCommentAt = int.MaxValue;
- for (var lineNumber = firstLine.LineNumber; lineNumber <= lastLine.LineNumber; ++lineNumber)
- {
- var line = span.Snapshot.GetLineFromLineNumber(lineNumber);
- var firstNonWhitespacePosition = line.GetFirstNonWhitespacePosition();
- var firstNonWhitespaceOnLine = firstNonWhitespacePosition.HasValue
- ? firstNonWhitespacePosition.Value - line.Start
- : int.MaxValue;
- indentToCommentAt = Math.Min(indentToCommentAt, firstNonWhitespaceOnLine);
- }
-
- return indentToCommentAt;
- }
-
///
/// 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
diff --git a/src/EditorFeatures/Core/Implementation/CommentSelection/ToggleLineCommentCommandHandler.cs b/src/EditorFeatures/Core/Implementation/CommentSelection/ToggleLineCommentCommandHandler.cs
new file mode 100644
index 0000000000000000000000000000000000000000..43c273ab496989b48b37c790b7c3707a0239429a
--- /dev/null
+++ b/src/EditorFeatures/Core/Implementation/CommentSelection/ToggleLineCommentCommandHandler.cs
@@ -0,0 +1,191 @@
+// 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.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.CommentSelection;
+using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
+using Microsoft.CodeAnalysis.Experiments;
+using Microsoft.CodeAnalysis.Host.Mef;
+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 Microsoft.VisualStudio.Utilities;
+using Roslyn.Utilities;
+using VSCommanding = Microsoft.VisualStudio.Commanding;
+
+namespace Microsoft.CodeAnalysis.Editor.Implementation.CommentSelection
+{
+ [Export(typeof(VSCommanding.ICommandHandler))]
+ [ContentType(ContentTypeNames.RoslynContentType)]
+ [Name(PredefinedCommandHandlerNames.ToggleLineComment)]
+ internal class ToggleLineCommentCommandHandler :
+ // Value tuple to represent that there is no distinct command to be passed in.
+ AbstractCommentSelectionBase,
+ VSCommanding.ICommandHandler
+ {
+ private static readonly CommentSelectionResult s_emptyCommentSelectionResult =
+ new CommentSelectionResult(new List(), new List(), Operation.Uncomment);
+
+ [ImportingConstructor]
+ [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
+ internal ToggleLineCommentCommandHandler(
+ ITextUndoHistoryRegistry undoHistoryRegistry,
+ IEditorOperationsFactoryService editorOperationsFactoryService)
+ : base(undoHistoryRegistry, editorOperationsFactoryService)
+ {
+ }
+
+ public VSCommanding.CommandState GetCommandState(ToggleLineCommentCommandArgs args)
+ {
+ if (Workspace.TryGetWorkspace(args.SubjectBuffer.AsTextContainer(), out var workspace))
+ {
+ var experimentationService = workspace.Services.GetRequiredService();
+ if (!experimentationService.IsExperimentEnabled(WellKnownExperimentNames.RoslynToggleLineComment))
+ {
+ return VSCommanding.CommandState.Unspecified;
+ }
+ }
+ return 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 async override Task CollectEditsAsync(Document document, ICommentSelectionService service,
+ ITextBuffer subjectBuffer, NormalizedSnapshotSpanCollection selectedSpans, ValueTuple command, CancellationToken cancellationToken)
+ {
+ var experimentationService = document.Project.Solution.Workspace.Services.GetRequiredService();
+ if (!experimentationService.IsExperimentEnabled(WellKnownExperimentNames.RoslynToggleLineComment))
+ {
+ return s_emptyCommentSelectionResult;
+ }
+
+ var commentInfo = await service.GetInfoAsync(document, selectedSpans.First().Span.ToTextSpan(), cancellationToken).ConfigureAwait(false);
+ if (commentInfo.SupportsSingleLineComment)
+ {
+ return ToggleLineComment(commentInfo, selectedSpans);
+ }
+
+ return s_emptyCommentSelectionResult;
+ }
+
+ private CommentSelectionResult ToggleLineComment(CommentSelectionInfo commentInfo,
+ NormalizedSnapshotSpanCollection selectedSpans)
+ {
+ var textChanges = ArrayBuilder.GetInstance();
+ var trackingSpans = ArrayBuilder.GetInstance();
+
+ var linesInSelections = selectedSpans.ToDictionary(
+ span => span,
+ span => GetLinesFromSelectedSpan(span).ToImmutableArray());
+
+ Operation operation;
+ // If all the selections are fully commented, uncomment.
+ if (!linesInSelections.Values.Where(lines => SelectionHasUncommentedLines(lines, commentInfo)).Any())
+ {
+ foreach (var selection in linesInSelections)
+ {
+ UncommentLines(selection.Value, textChanges, trackingSpans, commentInfo);
+ }
+
+ operation = Operation.Uncomment;
+ }
+ else
+ {
+ foreach (var selection in linesInSelections)
+ {
+ CommentLines(selection.Key, selection.Value, textChanges, trackingSpans, commentInfo);
+ }
+
+ operation = Operation.Comment;
+ }
+
+ return new CommentSelectionResult(textChanges, trackingSpans, operation);
+ }
+
+ private static void UncommentLines(ImmutableArray commentedLines, ArrayBuilder textChanges,
+ ArrayBuilder 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);
+ }
+ }
+
+ trackingSpans.Add(new CommentTrackingSpan(TextSpan.FromBounds(commentedLines.First().Start, commentedLines.Last().End)));
+ }
+
+ private static void CommentLines(SnapshotSpan selectedSpan, ImmutableArray linesInSelection,
+ ArrayBuilder textChanges, ArrayBuilder 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);
+ }
+ }
+
+ trackingSpans.Add(new CommentTrackingSpan(
+ TextSpan.FromBounds(linesInSelection.First().Start, linesInSelection.Last().End)));
+ }
+
+ private List GetLinesFromSelectedSpan(SnapshotSpan span)
+ {
+ var lines = new List();
+ 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 bool SelectionHasUncommentedLines(ImmutableArray linesInSelection, CommentSelectionInfo commentInfo)
+ => linesInSelection.Where(l => !IsLineCommented(l, commentInfo)).Any();
+
+ private static bool IsLineCommented(ITextSnapshotLine line, CommentSelectionInfo info)
+ {
+ var lineText = line.GetText();
+ if (lineText.Trim().StartsWith(info.SingleLineCommentString, StringComparison.Ordinal) || line.IsEmptyOrWhitespace())
+ {
+ return true;
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.cs.xlf b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.cs.xlf
index 2fe3ae6419de19eb5b534392248695941fdbb746..eadafff847c5fc8d51196d27f98d4a2ae979eaf3 100644
--- a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.cs.xlf
+++ b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.cs.xlf
@@ -1294,6 +1294,16 @@ Chcete pokračovat?
Toggle Block Comment
+
+