diff --git a/src/EditorFeatures/CSharp/BlockCommentEditing/BlockCommentEditingCommandHandler.cs b/src/EditorFeatures/CSharp/BlockCommentEditing/BlockCommentEditingCommandHandler.cs
new file mode 100644
index 0000000000000000000000000000000000000000..f2968755a7667933c215e69e380c7828300a4b1e
--- /dev/null
+++ b/src/EditorFeatures/CSharp/BlockCommentEditing/BlockCommentEditingCommandHandler.cs
@@ -0,0 +1,177 @@
+// 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.ComponentModel.Composition;
+using System.Diagnostics;
+using System.Threading;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Editor.Implementation.BlockCommentEditing;
+using Microsoft.CodeAnalysis.Shared.Extensions;
+using Microsoft.CodeAnalysis.Text;
+using Microsoft.CodeAnalysis.Text.Shared.Extensions;
+using Microsoft.VisualStudio.Text;
+using Microsoft.VisualStudio.Text.Operations;
+using Microsoft.VisualStudio.Utilities;
+using Roslyn.Utilities;
+
+namespace Microsoft.CodeAnalysis.Editor.CSharp.BlockCommentEditing
+{
+ [ExportCommandHandler(nameof(BlockCommentEditingCommandHandler), ContentTypeNames.CSharpContentType)]
+ [Order(After = PredefinedCommandHandlerNames.Completion)]
+ internal class BlockCommentEditingCommandHandler : AbstractBlockCommentEditingCommandHandler
+ {
+ [ImportingConstructor]
+ public BlockCommentEditingCommandHandler(
+ ITextUndoHistoryRegistry undoHistoryRegistry,
+ IEditorOperationsFactoryService editorOperationsFactoryService) : base(undoHistoryRegistry, editorOperationsFactoryService)
+ {
+ }
+
+ protected override string GetExteriorTextForNextLine(SnapshotPoint caretPosition)
+ {
+ var currentLine = caretPosition.GetContainingLine();
+
+ var firstNonWhitespacePosition = currentLine.GetFirstNonWhitespacePosition() ?? -1;
+ if (firstNonWhitespacePosition == -1)
+ {
+ return null;
+ }
+
+ var currentLineStartsWithBlockCommentStartString = currentLine.StartsWith(firstNonWhitespacePosition, "/*", ignoreCase: false);
+ var currentLineStartsWithBlockCommentEndString = currentLine.StartsWith(firstNonWhitespacePosition, "*/", ignoreCase: false);
+ var currentLineStartsWithBlockCommentMiddleString = currentLine.StartsWith(firstNonWhitespacePosition, "*", ignoreCase: false);
+
+ if (!currentLineStartsWithBlockCommentStartString && !currentLineStartsWithBlockCommentMiddleString)
+ {
+ return null;
+ }
+
+ if (!IsCaretInsideBlockCommentSyntax(caretPosition))
+ {
+ return null;
+ }
+
+ if (currentLineStartsWithBlockCommentStartString)
+ {
+ if (BlockCommentEndsRightAfterCaret(caretPosition))
+ {
+ // /*|*/
+ return " ";
+ }
+ else if (caretPosition == firstNonWhitespacePosition + 1)
+ {
+ // /|*
+ return null; // The newline inserted could break the syntax in a way that this handler cannot fix, let's leave it.
+ }
+ else
+ {
+ // /*|
+ return " *" + GetPaddingOrIndentation(currentLine, caretPosition, firstNonWhitespacePosition, "/*");
+ }
+ }
+
+ if (currentLineStartsWithBlockCommentEndString)
+ {
+ if (BlockCommentEndsRightAfterCaret(caretPosition))
+ {
+ // /*
+ // |*/
+ return " ";
+ }
+ else if (caretPosition == firstNonWhitespacePosition + 1)
+ {
+ // *|/
+ return "*";
+ }
+ else
+ {
+ // /*
+ // | */
+ return " * ";
+ }
+ }
+
+ if (currentLineStartsWithBlockCommentMiddleString)
+ {
+ if (BlockCommentEndsRightAfterCaret(caretPosition))
+ {
+ // *|*/
+ return "";
+ }
+ else if (caretPosition > firstNonWhitespacePosition)
+ {
+ // *|
+ return "*" + GetPaddingOrIndentation(currentLine, caretPosition, firstNonWhitespacePosition, "*");
+ }
+ else
+ {
+ // /*
+ // | *
+ return " * ";
+ }
+ }
+
+ return null;
+ }
+
+ private static bool BlockCommentEndsRightAfterCaret(SnapshotPoint caretPosition)
+ {
+ var snapshot = caretPosition.Snapshot;
+ return ((int)caretPosition + 2 <= snapshot.Length) ? snapshot.GetText(caretPosition, 2) == "*/" : false;
+ }
+
+ private static string GetPaddingOrIndentation(ITextSnapshotLine currentLine, int caretPosition, int firstNonWhitespacePosition, string exteriorText)
+ {
+ Debug.Assert(caretPosition >= firstNonWhitespacePosition + exteriorText.Length);
+
+ var firstNonWhitespaceOffset = firstNonWhitespacePosition - currentLine.Start;
+ Debug.Assert(firstNonWhitespaceOffset > -1);
+
+ var lineText = currentLine.GetText();
+ if ((lineText.Length == firstNonWhitespaceOffset + exteriorText.Length))
+ {
+ // *|
+ return " ";
+ }
+
+ var interiorText = lineText.Substring(firstNonWhitespaceOffset + exteriorText.Length);
+ var interiorFirstNonWhitespaceOffset = interiorText.GetFirstNonWhitespaceOffset() ?? -1;
+
+ if (interiorFirstNonWhitespaceOffset == 0)
+ {
+ // /****|
+ return " ";
+ }
+
+ var interiorFirstWhitespacePosition = firstNonWhitespacePosition + exteriorText.Length;
+ if (interiorFirstNonWhitespaceOffset == -1 || caretPosition <= interiorFirstWhitespacePosition + interiorFirstNonWhitespaceOffset)
+ {
+ // * |
+ // or
+ // * | 1.
+ // ^^
+ return currentLine.Snapshot.GetText(interiorFirstWhitespacePosition, caretPosition - interiorFirstWhitespacePosition);
+ }
+ else
+ {
+ // * 1. |
+ // ^^^
+ return currentLine.Snapshot.GetText(interiorFirstWhitespacePosition, interiorFirstNonWhitespaceOffset);
+ }
+ }
+
+ private static bool IsCaretInsideBlockCommentSyntax(SnapshotPoint caretPosition)
+ {
+ var document = caretPosition.Snapshot.GetOpenDocumentInCurrentContextWithChanges();
+ if (document == null)
+ {
+ return false;
+ }
+
+ var syntaxTree = document.GetSyntaxTreeAsync().WaitAndGetResult(CancellationToken.None);
+ var trivia = syntaxTree.FindTriviaAndAdjustForEndOfFile(caretPosition, CancellationToken.None);
+
+ return trivia.IsKind(SyntaxKind.MultiLineCommentTrivia) || trivia.IsKind(SyntaxKind.MultiLineDocumentationCommentTrivia);
+ }
+ }
+}
diff --git a/src/EditorFeatures/CSharp/CSharpEditorFeatures.csproj b/src/EditorFeatures/CSharp/CSharpEditorFeatures.csproj
index 52bd73c4a06604782d75a7a9022537e18e43ac16..3619e913339897bab93738b035eccf26d82760ae 100644
--- a/src/EditorFeatures/CSharp/CSharpEditorFeatures.csproj
+++ b/src/EditorFeatures/CSharp/CSharpEditorFeatures.csproj
@@ -79,6 +79,7 @@
+
diff --git a/src/EditorFeatures/CSharpTest/BlockCommentEditing/BlockCommentEditingTests.cs b/src/EditorFeatures/CSharpTest/BlockCommentEditing/BlockCommentEditingTests.cs
new file mode 100644
index 0000000000000000000000000000000000000000..d195ace88037c5127c38f7b85f94e0b21d43ddc7
--- /dev/null
+++ b/src/EditorFeatures/CSharpTest/BlockCommentEditing/BlockCommentEditingTests.cs
@@ -0,0 +1,595 @@
+// 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.Threading.Tasks;
+using Microsoft.CodeAnalysis.Editor.Commands;
+using Microsoft.CodeAnalysis.Editor.CSharp.BlockCommentEditing;
+using Microsoft.CodeAnalysis.Editor.UnitTests.BlockCommentEditing;
+using Microsoft.CodeAnalysis.Editor.UnitTests.Utilities;
+using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces;
+using Microsoft.VisualStudio.Text.Operations;
+using Roslyn.Test.Utilities;
+using Xunit;
+
+namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.BlockCommentEditing
+{
+ public class BlockCommentEditingTests : AbstractBlockCommentEditingTests
+ {
+ [WpfFact, Trait(Traits.Feature, Traits.Features.BlockCommentEditing)]
+ public async Task InsertOnStartLine0()
+ {
+ var code = @"
+ /*$$
+";
+ var expected = @"
+ /*
+ * $$
+";
+ await VerifyAsync(code, expected);
+ }
+
+ [WpfFact, Trait(Traits.Feature, Traits.Features.BlockCommentEditing)]
+ public async Task InsertOnStartLine1()
+ {
+ var code = @"
+ /*$$*/
+";
+ var expected = @"
+ /*
+ $$*/
+";
+ await VerifyAsync(code, expected);
+ }
+
+ [WpfFact, Trait(Traits.Feature, Traits.Features.BlockCommentEditing)]
+ public async Task InsertOnStartLine2()
+ {
+ var code = @"
+ /*$$ */
+";
+ var expected = @"
+ /*
+ *$$ */
+";
+ await VerifyAsync(code, expected);
+ }
+
+ [WpfFact, Trait(Traits.Feature, Traits.Features.BlockCommentEditing)]
+ public async Task InsertOnStartLine3()
+ {
+ var code = @"
+ /* $$ 1.
+ */
+";
+ var expected = @"
+ /*
+ * $$ 1.
+ */
+";
+ await VerifyAsync(code, expected);
+ }
+
+ [WpfFact, Trait(Traits.Feature, Traits.Features.BlockCommentEditing)]
+ public async Task InsertOnStartLine4()
+ {
+ var code = @"
+ /* 1.$$
+ */
+";
+ var expected = @"
+ /* 1.
+ * $$
+ */
+";
+ await VerifyAsync(code, expected);
+ }
+
+ [WpfFact, Trait(Traits.Feature, Traits.Features.BlockCommentEditing)]
+ public async Task InsertOnStartLine5()
+ {
+ var code = @"
+ /********$$
+";
+ var expected = @"
+ /********
+ * $$
+";
+ await VerifyAsync(code, expected);
+ }
+
+ [WpfFact, Trait(Traits.Feature, Traits.Features.BlockCommentEditing)]
+ public async Task InsertOnStartLine6()
+ {
+ var code = @"
+ /**$$
+";
+ var expected = @"
+ /**
+ * $$
+";
+ await VerifyAsync(code, expected);
+ }
+
+ [WpfFact, Trait(Traits.Feature, Traits.Features.BlockCommentEditing)]
+ public async Task InsertOnStartLine7()
+ {
+ var code = @"
+ /* $$
+";
+ var expected = @"
+ /*
+ * $$
+";
+ await VerifyAsync(code, expected);
+ }
+
+ [WpfFact, Trait(Traits.Feature, Traits.Features.BlockCommentEditing)]
+ public async Task NotInsertOnStartLine0()
+ {
+ var code = @"
+ /$$*
+ */
+";
+ var expected = @"
+ /
+$$*
+ */
+";
+ await VerifyAsync(code, expected);
+ }
+
+ [WpfFact, Trait(Traits.Feature, Traits.Features.BlockCommentEditing)]
+ public async Task InsertOnMiddleLine0()
+ {
+ var code = @"
+ /*
+ *$$
+";
+ var expected = @"
+ /*
+ *
+ * $$
+";
+ await VerifyAsync(code, expected);
+ }
+
+ [WpfFact, Trait(Traits.Feature, Traits.Features.BlockCommentEditing)]
+ public async Task InsertOnMiddleLine1()
+ {
+ var code = @"
+ /*
+ *$$*/
+";
+ var expected = @"
+ /*
+ *
+ $$*/
+";
+ await VerifyAsync(code, expected);
+ }
+
+ [WpfFact, Trait(Traits.Feature, Traits.Features.BlockCommentEditing)]
+ public async Task InsertOnMiddleLine2()
+ {
+ var code = @"
+ /*
+ *$$ */
+";
+ var expected = @"
+ /*
+ *
+ *$$ */
+";
+ await VerifyAsync(code, expected);
+ }
+
+ [WpfFact, Trait(Traits.Feature, Traits.Features.BlockCommentEditing)]
+ public async Task InsertOnMiddleLine3()
+ {
+ var code = @"
+ /*
+ * $$ 1.
+ */
+";
+ var expected = @"
+ /*
+ *
+ * $$ 1.
+ */
+";
+ await VerifyAsync(code, expected);
+ }
+
+ [WpfFact, Trait(Traits.Feature, Traits.Features.BlockCommentEditing)]
+ public async Task InsertOnMiddleLine4()
+ {
+ var code = @"
+ /*
+ * 1.$$
+ */
+";
+ var expected = @"
+ /*
+ * 1.
+ * $$
+ */
+";
+ await VerifyAsync(code, expected);
+ }
+
+ [WpfFact, Trait(Traits.Feature, Traits.Features.BlockCommentEditing)]
+ public async Task InsertOnMiddleLine5()
+ {
+ var code = @"
+ /*
+ * 1.
+ * $$
+ */
+";
+ var expected = @"
+ /*
+ * 1.
+ *
+ * $$
+ */
+";
+ await VerifyAsync(code, expected);
+ }
+
+ [WpfFact, Trait(Traits.Feature, Traits.Features.BlockCommentEditing)]
+ public async Task InsertOnMiddleLine6()
+ {
+ var code = @"
+ /*
+ $$ *
+ */
+";
+ var expected = @"
+ /*
+
+ * $$ *
+ */
+";
+ await VerifyAsync(code, expected);
+ }
+
+ [WpfFact, Trait(Traits.Feature, Traits.Features.BlockCommentEditing)]
+ public async Task InsertOnMiddleLine7()
+ {
+ var code = @"
+ /*
+ *************$$
+ */
+";
+ var expected = @"
+ /*
+ *************
+ * $$
+ */
+";
+ await VerifyAsync(code, expected);
+ }
+
+ [WpfFact, Trait(Traits.Feature, Traits.Features.BlockCommentEditing)]
+ public async Task InsertOnMiddleLine8()
+ {
+ var code = @"
+ /**
+ *$$
+ */
+";
+ var expected = @"
+ /**
+ *
+ * $$
+ */
+";
+ await VerifyAsync(code, expected);
+ }
+
+ [WpfFact, Trait(Traits.Feature, Traits.Features.BlockCommentEditing)]
+ public async Task InsertOnMiddleLine9()
+ {
+ var code = @"
+ /**
+ *$$
+";
+ var expected = @"
+ /**
+ *
+ * $$
+";
+ await VerifyAsync(code, expected);
+ }
+
+ [WpfFact, Trait(Traits.Feature, Traits.Features.BlockCommentEditing)]
+ public async Task InsertOnEndLine0()
+ {
+ var code = @"
+ /*
+ *$$/
+";
+ var expected = @"
+ /*
+ *
+ *$$/
+";
+ await VerifyAsync(code, expected);
+ }
+
+ [WpfFact, Trait(Traits.Feature, Traits.Features.BlockCommentEditing)]
+ public async Task InsertOnEndLine1()
+ {
+ var code = @"
+ /**
+ *$$/
+";
+ var expected = @"
+ /**
+ *
+ *$$/
+";
+ await VerifyAsync(code, expected);
+ }
+
+ [WpfFact, Trait(Traits.Feature, Traits.Features.BlockCommentEditing)]
+ public async Task InsertOnEndLine2()
+ {
+ var code = @"
+ /**
+ *
+ *$$/
+";
+ var expected = @"
+ /**
+ *
+ *
+ *$$/
+";
+ await VerifyAsync(code, expected);
+ }
+
+ [WpfFact, Trait(Traits.Feature, Traits.Features.BlockCommentEditing)]
+ public async Task InsertOnEndLine3()
+ {
+ var code = @"
+ /*
+ $$ */
+";
+ var expected = @"
+ /*
+
+ * $$ */
+";
+ await VerifyAsync(code, expected);
+ }
+
+ [WpfFact, Trait(Traits.Feature, Traits.Features.BlockCommentEditing)]
+ public async Task InsertOnEndLine4()
+ {
+ var code = @"
+ /*
+ $$*/
+";
+ var expected = @"
+ /*
+
+ $$*/
+";
+ await VerifyAsync(code, expected);
+ }
+
+ [WpfFact, Trait(Traits.Feature, Traits.Features.BlockCommentEditing)]
+ public async Task NotInsertInVerbatimString0()
+ {
+ var code = @"
+var code = @""
+/*$$
+"";
+";
+ var expected = @"
+var code = @""
+/*
+$$
+"";
+";
+ await VerifyAsync(code, expected);
+ }
+
+ [WpfFact, Trait(Traits.Feature, Traits.Features.BlockCommentEditing)]
+ public async Task NotInsertInVerbatimString1()
+ {
+ var code = @"
+var code = @""
+/*
+ *$$
+"";
+";
+ var expected = @"
+var code = @""
+/*
+ *
+$$
+"";
+";
+ await VerifyAsync(code, expected);
+ }
+
+ [WpfFact, Trait(Traits.Feature, Traits.Features.BlockCommentEditing)]
+ public async Task BoundCheckInsertOnStartLine0()
+ {
+ var code = @"
+ /$$*";
+ var expected = @"
+ /
+$$*";
+ await VerifyAsync(code, expected);
+ }
+
+ [WpfFact, Trait(Traits.Feature, Traits.Features.BlockCommentEditing)]
+ public async Task BoundCheckInsertOnStartLine1()
+ {
+ var code = @"
+ /*$$ ";
+ var expected = @"
+ /*
+ *$$ ";
+ await VerifyAsync(code, expected);
+ }
+
+ [WpfFact, Trait(Traits.Feature, Traits.Features.BlockCommentEditing)]
+ public async Task BoundCheckInsertOnMiddleLine()
+ {
+ var code = @"
+ /*
+ *$$ ";
+ var expected = @"
+ /*
+ *
+ *$$ ";
+ await VerifyAsync(code, expected);
+ }
+
+ [WpfFact, Trait(Traits.Feature, Traits.Features.BlockCommentEditing)]
+ public async Task BoundCheckInsertOnEndLine()
+ {
+ var code = @"
+ /*
+ *$$/";
+ var expected = @"
+ /*
+ *
+ *$$/";
+ await VerifyAsync(code, expected);
+ }
+
+ [WpfFact, Trait(Traits.Feature, Traits.Features.BlockCommentEditing)]
+ public async Task InsertOnStartLine2_Tab()
+ {
+ var code = @"
+ /*$$*/
+";
+ var expected = @"
+ /*
+ *$$*/
+";
+ await VerifyTabsAsync(code, expected);
+ }
+
+ [WpfFact, Trait(Traits.Feature, Traits.Features.BlockCommentEditing)]
+ public async Task InsertOnStartLine3_Tab()
+ {
+ var code = @"
+ /*$$1.
+ */
+";
+ var expected = @"
+ /*
+ *$$1.
+ */
+";
+ await VerifyTabsAsync(code, expected);
+ }
+
+ [WpfFact, Trait(Traits.Feature, Traits.Features.BlockCommentEditing)]
+ public async Task InsertOnStartLine4_Tab()
+ {
+ var code = @"
+ /* 1.$$
+ */
+";
+ var expected = @"
+ /* 1.
+ * $$
+ */
+";
+ await VerifyTabsAsync(code, expected);
+ }
+
+ [WpfFact, Trait(Traits.Feature, Traits.Features.BlockCommentEditing)]
+ public async Task InsertOnStartLine6_Tab()
+ {
+ var code = @"
+ /*$$
+";
+ var expected = @"
+ /*
+ *$$
+";
+ await VerifyTabsAsync(code, expected);
+ }
+
+ [WpfFact, Trait(Traits.Feature, Traits.Features.BlockCommentEditing)]
+ public async Task InsertOnMiddleLine2_Tab()
+ {
+ var code = @"
+ /*
+ *$$*/
+";
+ var expected = @"
+ /*
+ *
+ *$$*/
+";
+ await VerifyTabsAsync(code, expected);
+ }
+
+ [WpfFact, Trait(Traits.Feature, Traits.Features.BlockCommentEditing)]
+ public async Task InsertOnMiddleLine3_Tab()
+ {
+ var code = @"
+ /*
+ * $$1.
+ */
+";
+ var expected = @"
+ /*
+ *
+ * $$1.
+ */
+";
+ await VerifyTabsAsync(code, expected);
+ }
+
+ [WpfFact, Trait(Traits.Feature, Traits.Features.BlockCommentEditing)]
+ public async Task InsertOnMiddleLine4_Tab()
+ {
+ var code = @"
+ /*
+ * 1.$$
+ */
+";
+ var expected = @"
+ /*
+ * 1.
+ * $$
+ */
+";
+ await VerifyTabsAsync(code, expected);
+ }
+
+ [WpfFact, Trait(Traits.Feature, Traits.Features.BlockCommentEditing)]
+ public async Task InsertOnMiddleLine5_Tab()
+ {
+ var code = @"
+ /*
+ * 1.
+ * $$
+ */
+";
+ var expected = @"
+ /*
+ * 1.
+ *
+ * $$
+ */
+";
+ await VerifyTabsAsync(code, expected);
+ }
+
+ protected override Task CreateTestWorkspaceAsync(string initialMarkup) => TestWorkspace.CreateCSharpAsync(initialMarkup);
+
+ internal override ICommandHandler CreateCommandHandler(ITextUndoHistoryRegistry undoHistoryRegistry, IEditorOperationsFactoryService editorOperationsFactoryService)
+ => new BlockCommentEditingCommandHandler(undoHistoryRegistry, editorOperationsFactoryService);
+ }
+}
diff --git a/src/EditorFeatures/CSharpTest/CSharpEditorServicesTest.csproj b/src/EditorFeatures/CSharpTest/CSharpEditorServicesTest.csproj
index ce6a54fb2111a632b238fc879203a11d30904858..8d1226269cd425f26a16631c77fec6d006547242 100644
--- a/src/EditorFeatures/CSharpTest/CSharpEditorServicesTest.csproj
+++ b/src/EditorFeatures/CSharpTest/CSharpEditorServicesTest.csproj
@@ -150,6 +150,7 @@
+
diff --git a/src/EditorFeatures/Core/EditorFeatures.csproj b/src/EditorFeatures/Core/EditorFeatures.csproj
index dcb9a28f5748c33fa582f6a16d30867224933c49..4cbf8dc59eeb190a77683ca46938c0632334ca79 100644
--- a/src/EditorFeatures/Core/EditorFeatures.csproj
+++ b/src/EditorFeatures/Core/EditorFeatures.csproj
@@ -321,6 +321,7 @@
+
diff --git a/src/EditorFeatures/Core/Implementation/BlockCommentEditing/AbstractBlockCommentEditingCommandHandler.cs b/src/EditorFeatures/Core/Implementation/BlockCommentEditing/AbstractBlockCommentEditingCommandHandler.cs
new file mode 100644
index 0000000000000000000000000000000000000000..760547edfe4fd1443368e7fb7fb925dfc54ca24a
--- /dev/null
+++ b/src/EditorFeatures/Core/Implementation/BlockCommentEditing/AbstractBlockCommentEditingCommandHandler.cs
@@ -0,0 +1,77 @@
+// 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 Microsoft.CodeAnalysis.Editor.Commands;
+using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
+using Microsoft.CodeAnalysis.Editor.Shared.Options;
+using Microsoft.VisualStudio.Text;
+using Microsoft.VisualStudio.Text.Operations;
+using Roslyn.Utilities;
+
+namespace Microsoft.CodeAnalysis.Editor.Implementation.BlockCommentEditing
+{
+ internal abstract class AbstractBlockCommentEditingCommandHandler : ICommandHandler
+ {
+ private readonly ITextUndoHistoryRegistry _undoHistoryRegistry;
+ private readonly IEditorOperationsFactoryService _editorOperationsFactoryService;
+
+ protected AbstractBlockCommentEditingCommandHandler(
+ ITextUndoHistoryRegistry undoHistoryRegistry,
+ IEditorOperationsFactoryService editorOperationsFactoryService)
+ {
+ Contract.ThrowIfNull(undoHistoryRegistry);
+ Contract.ThrowIfNull(editorOperationsFactoryService);
+
+ _undoHistoryRegistry = undoHistoryRegistry;
+ _editorOperationsFactoryService = editorOperationsFactoryService;
+ }
+
+ public CommandState GetCommandState(ReturnKeyCommandArgs args, Func nextHandler) => nextHandler();
+
+ public void ExecuteCommand(ReturnKeyCommandArgs args, Action nextHandler)
+ {
+ if (TryHandleReturnKey(args))
+ {
+ return;
+ }
+
+ nextHandler();
+ }
+
+ private bool TryHandleReturnKey(ReturnKeyCommandArgs args)
+ {
+ var subjectBuffer = args.SubjectBuffer;
+ var textView = args.TextView;
+
+ if (!subjectBuffer.GetOption(FeatureOnOffOptions.AutoInsertBlockCommentStartString))
+ {
+ return false;
+ }
+
+ var caretPosition = textView.GetCaretPoint(subjectBuffer);
+ if (caretPosition == null)
+ {
+ return false;
+ }
+
+ var exteriorText = GetExteriorTextForNextLine(caretPosition.Value);
+ if (exteriorText == null)
+ {
+ return false;
+ }
+
+ using (var transaction = _undoHistoryRegistry.GetHistory(args.TextView.TextBuffer).CreateTransaction(EditorFeaturesResources.InsertNewLine))
+ {
+ var editorOperations = _editorOperationsFactoryService.GetEditorOperations(args.TextView);
+
+ editorOperations.InsertNewLine();
+ editorOperations.InsertText(exteriorText);
+
+ transaction.Complete();
+ return true;
+ }
+ }
+
+ protected abstract string GetExteriorTextForNextLine(SnapshotPoint caretPosition);
+ }
+}
diff --git a/src/EditorFeatures/Core/Shared/Options/FeatureOnOffOptions.cs b/src/EditorFeatures/Core/Shared/Options/FeatureOnOffOptions.cs
index b58b60d3affdbc77d4a58f3328d3c7d31146523c..2053be4f625aedf48bf262b4bcef5310a95224ea 100644
--- a/src/EditorFeatures/Core/Shared/Options/FeatureOnOffOptions.cs
+++ b/src/EditorFeatures/Core/Shared/Options/FeatureOnOffOptions.cs
@@ -32,6 +32,9 @@ internal static class FeatureOnOffOptions
[ExportOption]
public static readonly PerLanguageOption AutoXmlDocCommentGeneration = new PerLanguageOption(OptionName, "Automatic XML Doc Comment Generation", defaultValue: true);
+ [ExportOption]
+ public static readonly PerLanguageOption AutoInsertBlockCommentStartString = new PerLanguageOption(OptionName, "Auto Insert Block Comment Start String", defaultValue: true);
+
[ExportOption]
public static readonly PerLanguageOption PrettyListing = new PerLanguageOption(OptionName, "Pretty List On Line Commit", defaultValue: true);
diff --git a/src/EditorFeatures/Test/BlockCommentEditing/AbstractBlockCommentEditingTests.cs b/src/EditorFeatures/Test/BlockCommentEditing/AbstractBlockCommentEditingTests.cs
new file mode 100644
index 0000000000000000000000000000000000000000..63e9663e76d035c577b1bb1bd890fdce4ae858ce
--- /dev/null
+++ b/src/EditorFeatures/Test/BlockCommentEditing/AbstractBlockCommentEditingTests.cs
@@ -0,0 +1,70 @@
+// 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.Linq;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.Editor.Commands;
+using Microsoft.CodeAnalysis.Editor.Host;
+using Microsoft.CodeAnalysis.Editor.Shared.Options;
+using Microsoft.CodeAnalysis.Editor.UnitTests.Utilities;
+using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces;
+using Microsoft.CodeAnalysis.Formatting;
+using Microsoft.CodeAnalysis.Options;
+using Microsoft.VisualStudio.Text;
+using Microsoft.VisualStudio.Text.Editor;
+using Microsoft.VisualStudio.Text.Operations;
+using Roslyn.Test.Utilities;
+using Xunit;
+
+namespace Microsoft.CodeAnalysis.Editor.UnitTests.BlockCommentEditing
+{
+ public abstract class AbstractBlockCommentEditingTests
+ {
+ internal abstract ICommandHandler CreateCommandHandler(
+ ITextUndoHistoryRegistry undoHistoryRegistry,
+ IEditorOperationsFactoryService editorOperationsFactoryService);
+
+ protected abstract Task CreateTestWorkspaceAsync(string initialMarkup);
+
+ protected async Task VerifyAsync(string initialMarkup, string expectedMarkup)
+ {
+ using (var workspace = await CreateTestWorkspaceAsync(initialMarkup))
+ {
+ var testDocument = workspace.Documents.Single();
+ var view = testDocument.GetTextView();
+ view.Caret.MoveTo(new SnapshotPoint(view.TextSnapshot, testDocument.CursorPosition.Value));
+
+ var commandHandler = CreateCommandHandler(workspace.GetService(), workspace.GetService());
+
+ var args = new ReturnKeyCommandArgs(view, view.TextBuffer);
+ var nextHandler = CreateInsertTextHandler(view, "\r\n");
+
+ commandHandler.ExecuteCommand(args, nextHandler);
+
+ string expectedCode;
+ int expectedPosition;
+ MarkupTestFile.GetPosition(expectedMarkup, out expectedCode, out expectedPosition);
+
+ Assert.Equal(expectedCode, view.TextSnapshot.GetText());
+
+ var caretPosition = view.Caret.Position.BufferPosition.Position;
+ Assert.True(expectedPosition == caretPosition,
+ string.Format("Caret positioned incorrectly. Should have been {0}, but was {1}.", expectedPosition, caretPosition));
+ }
+ }
+
+ protected Task VerifyTabsAsync(string initialMarkup, string expectedMarkup) => VerifyAsync(ReplaceTabTags(initialMarkup), ReplaceTabTags(expectedMarkup));
+
+ private string ReplaceTabTags(string markup) => markup.Replace("", "\t");
+
+ private Action CreateInsertTextHandler(ITextView textView, string text)
+ {
+ return () =>
+ {
+ var caretPosition = textView.Caret.Position.BufferPosition;
+ var newSpanshot = textView.TextBuffer.Insert(caretPosition, text);
+ textView.Caret.MoveTo(new SnapshotPoint(newSpanshot, (int)caretPosition + text.Length));
+ };
+ }
+ }
+}
diff --git a/src/EditorFeatures/Test/EditorServicesTest.csproj b/src/EditorFeatures/Test/EditorServicesTest.csproj
index 5bd13774359871c68ebdafa4e74152d381e6ca94..16230f641ae8ab254dae68822c6b67e14586c001 100644
--- a/src/EditorFeatures/Test/EditorServicesTest.csproj
+++ b/src/EditorFeatures/Test/EditorServicesTest.csproj
@@ -191,6 +191,7 @@
+
diff --git a/src/EditorFeatures/TestUtilities/Traits.cs b/src/EditorFeatures/TestUtilities/Traits.cs
index e88d274c516393403bfb25515569b1eb187cc2ba..7b2ac19ce62833ff9af76345f34205075d6ef3a7 100644
--- a/src/EditorFeatures/TestUtilities/Traits.cs
+++ b/src/EditorFeatures/TestUtilities/Traits.cs
@@ -19,6 +19,7 @@ public static class Features
public const string AsyncLazy = nameof(AsyncLazy);
public const string AutomaticEndConstructCorrection = nameof(AutomaticEndConstructCorrection);
public const string AutomaticCompletion = nameof(AutomaticCompletion);
+ public const string BlockCommentEditing = nameof(BlockCommentEditing);
public const string BraceHighlighting = nameof(BraceHighlighting);
public const string BraceMatching = nameof(BraceMatching);
public const string CallHierarchy = nameof(CallHierarchy);
diff --git a/src/EditorFeatures/Text/Shared/Extensions/ITextSnapshotLineExtensions.cs b/src/EditorFeatures/Text/Shared/Extensions/ITextSnapshotLineExtensions.cs
index d35af334614af0d03a01aa95e1c214934b422848..b37b2c9310c56ebbd5f86c54302e1ca0069b6097 100644
--- a/src/EditorFeatures/Text/Shared/Extensions/ITextSnapshotLineExtensions.cs
+++ b/src/EditorFeatures/Text/Shared/Extensions/ITextSnapshotLineExtensions.cs
@@ -148,7 +148,7 @@ public static int GetLineOffsetFromColumn(this ITextSnapshotLine line, int colum
public static bool StartsWith(this ITextSnapshotLine line, int index, string value, bool ignoreCase)
{
var snapshot = line.Snapshot;
- if (index + value.Length >= snapshot.Length)
+ if (index + value.Length > snapshot.Length)
{
return false;
}
diff --git a/src/VisualStudio/CSharp/Impl/CSharpVSResources.Designer.cs b/src/VisualStudio/CSharp/Impl/CSharpVSResources.Designer.cs
index 60e5fb63cd49eca06f082638bbfee665a5f60732..114fcf28a9130a03091ba3e3996d88f0f7c313ed 100644
--- a/src/VisualStudio/CSharp/Impl/CSharpVSResources.Designer.cs
+++ b/src/VisualStudio/CSharp/Impl/CSharpVSResources.Designer.cs
@@ -483,6 +483,15 @@ internal class CSharpVSResources {
}
}
+ ///
+ /// Looks up a localized string similar to _Insert * at the start of new lines when writing /* */ comments.
+ ///
+ internal static string Option_InsertAsteriskAtTheStartOfNewLinesWhenWritingBlockComments {
+ get {
+ return ResourceManager.GetString("Option_InsertAsteriskAtTheStartOfNewLinesWhenWritingBlockComments", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to _Add new line on enter after end of fully typed word.
///
diff --git a/src/VisualStudio/CSharp/Impl/CSharpVSResources.resx b/src/VisualStudio/CSharp/Impl/CSharpVSResources.resx
index 08cace935e75952194144f1b0d614c675403e073..ff977606cf486d3d50f61d7ac4576e58a3a90348 100644
--- a/src/VisualStudio/CSharp/Impl/CSharpVSResources.resx
+++ b/src/VisualStudio/CSharp/Impl/CSharpVSResources.resx
@@ -354,6 +354,9 @@
Highlighting
+
+ _Insert * at the start of new lines when writing /* */ comments
+
Optimize for solution size
diff --git a/src/VisualStudio/CSharp/Impl/Options/AdvancedOptionPageControl.xaml b/src/VisualStudio/CSharp/Impl/Options/AdvancedOptionPageControl.xaml
index dce6551ce1b2e35ea41b92a6e6c46136edc62e5c..ac8abb93bccf2ddb231c37116103474d3c45223e 100644
--- a/src/VisualStudio/CSharp/Impl/Options/AdvancedOptionPageControl.xaml
+++ b/src/VisualStudio/CSharp/Impl/Options/AdvancedOptionPageControl.xaml
@@ -34,6 +34,8 @@
+