diff --git a/src/EditorFeatures/CSharp/BlockCommentEditing/BlockCommentEditingCommandHandler.cs b/src/EditorFeatures/CSharp/BlockCommentEditing/BlockCommentEditingCommandHandler.cs index b2da3e9604a630da3762f1d28577c8b324e22701..13c126937f219ab7e890bb7b8e222c2d5d48ae04 100644 --- a/src/EditorFeatures/CSharp/BlockCommentEditing/BlockCommentEditingCommandHandler.cs +++ b/src/EditorFeatures/CSharp/BlockCommentEditing/BlockCommentEditingCommandHandler.cs @@ -9,6 +9,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Editor.Shared.Extensions; using Microsoft.CodeAnalysis.Editor.Shared.Options; +using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.Text.Shared.Extensions; @@ -55,186 +56,199 @@ public bool ExecuteCommand(ReturnKeyCommandArgs args, CommandExecutionContext co private bool TryHandleReturnKey(ITextBuffer subjectBuffer, ITextView textView) { if (!subjectBuffer.GetFeatureOnOffOption(FeatureOnOffOptions.AutoInsertBlockCommentStartString)) - { return false; - } var caretPosition = textView.GetCaretPoint(subjectBuffer); if (caretPosition == null) - { return false; - } - var exteriorText = GetExteriorTextForNextLine(caretPosition.Value); + var exteriorText = GetExteriorTextForCurrentLine(caretPosition.Value); if (exteriorText == null) - { return false; - } using var transaction = _undoHistoryRegistry.GetHistory(textView.TextBuffer).CreateTransaction(EditorFeaturesResources.Insert_new_line); var editorOperations = _editorOperationsFactoryService.GetEditorOperations(textView); - - editorOperations.InsertNewLine(); - editorOperations.InsertText(exteriorText); + editorOperations.ReplaceText(GetReplacementSpan(caretPosition.Value), exteriorText); transaction.Complete(); return true; } - private string GetExteriorTextForNextLine(SnapshotPoint caretPosition) + private Span GetReplacementSpan(SnapshotPoint caretPosition) { - var currentLine = caretPosition.GetContainingLine(); + // We want to replace all the whitespace following the caret. This is standard behavior in VS that + // we want to mimic. + var snapshot = caretPosition.Snapshot; + var start = caretPosition.Position; + var end = caretPosition; + while (end < snapshot.Length && char.IsWhiteSpace(end.GetChar()) && !SyntaxFacts.IsNewLine(end.GetChar())) + end = end + 1; + + return Span.FromBounds(start, end); + } + private string GetExteriorTextForCurrentLine(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); + var startsWithBlockCommentStartString = currentLine.StartsWith(firstNonWhitespacePosition, "/*", ignoreCase: false); + var startsWithBlockCommentEndString = currentLine.StartsWith(firstNonWhitespacePosition, "*/", ignoreCase: false); + var startsWithBlockCommentMiddleString = currentLine.StartsWith(firstNonWhitespacePosition, "*", ignoreCase: false); - if (!currentLineStartsWithBlockCommentStartString && !currentLineStartsWithBlockCommentMiddleString) + if (!startsWithBlockCommentStartString && + !startsWithBlockCommentMiddleString) { return null; } - if (!IsCaretInsideBlockCommentSyntax(caretPosition)) - { + if (!IsCaretInsideBlockCommentSyntax(caretPosition, out var document, out var blockComment)) 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, "/*"); - } - } + var textSnapshot = caretPosition.Snapshot; - if (currentLineStartsWithBlockCommentEndString) + // The whitespace indentation on the line where the block-comment starts. + var commentIndentation = textSnapshot.GetText(Span.FromBounds( + textSnapshot.GetLineFromPosition(blockComment.FullSpan.Start).Start, + blockComment.FullSpan.Start)); + + // The whitespace indentation on the current line up to the first non-whitespace char. + var lineIndentation = textSnapshot.GetText(Span.FromBounds( + currentLine.Start, + firstNonWhitespacePosition)); + + var exteriorText = GetExteriorText(); + if (exteriorText == null) + return null; + + var options = document.Project.Solution.Options; + var newLine = options.GetOption(FormattingOptions.NewLine, LanguageNames.CSharp); + return newLine + exteriorText; + + string GetExteriorText() { - if (BlockCommentEndsRightAfterCaret(caretPosition)) + if (startsWithBlockCommentStartString) { - // /* - // |*/ - return " "; + if (BlockCommentEndsRightAfterCaret(caretPosition)) + { + // /*|*/ + return commentIndentation + " "; + } + 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 + { + // /*| + // This is directly after the comment starts. Insert ' * ' to continue the comment and to put + // the user one space in. This is the idiomatic style for C#. Note: if the user is hitting + // enter after + // + // /* + // * + // *$$ + // + // Then we don't add the space. In this case, they are indicating they don't want this extra + // space added. + var padding = GetPaddingAfterCommentCharacter(); + return commentIndentation + " *" + (padding == "" ? " " : padding); + } } - else if (caretPosition == firstNonWhitespacePosition + 1) + + if (startsWithBlockCommentEndString) { - // *|/ - return "*"; + if (BlockCommentEndsRightAfterCaret(caretPosition)) + { + // /* + // |*/ + return commentIndentation + " "; + } + else if (caretPosition == firstNonWhitespacePosition + 1) + { + // *|/ + return lineIndentation + "*"; + } + else + { + // /* + // | */ + return commentIndentation + " "; + } } - else + + if (startsWithBlockCommentMiddleString) { - // /* - // | */ - return " * "; + if (BlockCommentEndsRightAfterCaret(caretPosition)) + { + // *|*/ + return lineIndentation; + } + else if (caretPosition > firstNonWhitespacePosition) + { + // *| + return lineIndentation + "*" + GetPaddingAfterCommentCharacter(); + } + else + { + // /* + // | * + return commentIndentation + " "; + } } + + return null; } - if (currentLineStartsWithBlockCommentMiddleString) + string GetPaddingAfterCommentCharacter() { - if (BlockCommentEndsRightAfterCaret(caretPosition)) - { - // *|*/ - return ""; - } - else if (caretPosition > firstNonWhitespacePosition) - { - // *| - return "*" + GetPaddingOrIndentation(currentLine, caretPosition, firstNonWhitespacePosition, "*"); - } - else - { - // /* - // | * - return " * "; - } - } + var currentChar = firstNonWhitespacePosition; + Debug.Assert(textSnapshot[currentChar] == '/' || textSnapshot[currentChar] == '*'); + + // Skip past the first comment char. + currentChar++; - return null; + // Skip past any banner of ****'s + while (currentChar < caretPosition && textSnapshot[currentChar] == '*') + currentChar++; + + var start = currentChar; + while (currentChar < caretPosition && char.IsWhiteSpace(textSnapshot[currentChar])) + currentChar++; + + return textSnapshot.GetText(Span.FromBounds(start, currentChar)); + } } private static bool BlockCommentEndsRightAfterCaret(SnapshotPoint caretPosition) { var snapshot = caretPosition.Snapshot; - return ((int)caretPosition + 2 <= snapshot.Length) ? snapshot.GetText(caretPosition, 2) == "*/" : false; + return (int)caretPosition + 2 <= snapshot.Length && snapshot.GetText(caretPosition, 2) == "*/"; } - private static string GetPaddingOrIndentation(ITextSnapshotLine currentLine, int caretPosition, int firstNonWhitespacePosition, string exteriorText) + public static bool IsCaretInsideBlockCommentSyntax( + SnapshotPoint caretPosition, out Document document, out SyntaxTrivia trivia) { - Debug.Assert(caretPosition >= firstNonWhitespacePosition + exteriorText.Length); + trivia = default; - 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); - } - } - - public static bool IsCaretInsideBlockCommentSyntax(SnapshotPoint caretPosition) - { var snapshot = caretPosition.Snapshot; - var document = snapshot.GetOpenDocumentInCurrentContextWithChanges(); + document = snapshot.GetOpenDocumentInCurrentContextWithChanges(); if (document == null) - { return false; - } var syntaxTree = document.GetSyntaxTreeSynchronously(CancellationToken.None); - var trivia = syntaxTree.FindTriviaAndAdjustForEndOfFile(caretPosition, CancellationToken.None); + trivia = syntaxTree.FindTriviaAndAdjustForEndOfFile(caretPosition, CancellationToken.None); var isBlockComment = trivia.IsKind(SyntaxKind.MultiLineCommentTrivia) || trivia.IsKind(SyntaxKind.MultiLineDocumentationCommentTrivia); if (isBlockComment) { var span = trivia.FullSpan; if (span.Start < caretPosition && caretPosition < span.End) - { return true; - } // FindTriviaAndAdjustForEndOfFile always returns something if position is EOF, // whether or not the result includes the position. @@ -246,16 +260,12 @@ public static bool IsCaretInsideBlockCommentSyntax(SnapshotPoint caretPosition) if (caretPosition == snapshot.Length) { if (span.Length < "/**/".Length) - { return true; - } // If the block comment is not closed, SyntaxTrivia contains diagnostics // So when the SyntaxTrivia is clean, the block comment should be closed if (!trivia.ContainsDiagnostics) - { return false; - } var textBeforeCaret = snapshot.GetText(caretPosition.Position - 2, 2); return textBeforeCaret != "*/"; diff --git a/src/EditorFeatures/CSharp/BlockCommentEditing/CloseBlockCommentCommandHandler.cs b/src/EditorFeatures/CSharp/BlockCommentEditing/CloseBlockCommentCommandHandler.cs index b7ab141482b5e95ef160181735c49fe214f419c7..f40d7b175c917d4e0d34de359c128766bc3cd80a 100644 --- a/src/EditorFeatures/CSharp/BlockCommentEditing/CloseBlockCommentCommandHandler.cs +++ b/src/EditorFeatures/CSharp/BlockCommentEditing/CloseBlockCommentCommandHandler.cs @@ -45,7 +45,7 @@ public bool ExecuteCommand(TypeCharCommandArgs args, CommandExecutionContext exe line.IsEmptyOrWhitespace(0, line.Length - 2)) { if (args.SubjectBuffer.GetFeatureOnOffOption(FeatureOnOffOptions.AutoInsertBlockCommentStartString) && - BlockCommentEditingCommandHandler.IsCaretInsideBlockCommentSyntax(caret.Value)) + BlockCommentEditingCommandHandler.IsCaretInsideBlockCommentSyntax(caret.Value, out _, out _)) { args.SubjectBuffer.Replace(new VisualStudio.Text.Span(position - 1, 1), "/"); return true; diff --git a/src/EditorFeatures/CSharpTest/BlockCommentEditing/BlockCommentEditingTests.cs b/src/EditorFeatures/CSharpTest/BlockCommentEditing/BlockCommentEditingTests.cs index 75fb85f1eb7e69eb21d9771505b7d16643030e0d..41718ca377df1fea046df3b57548da1071ba2072 100644 --- a/src/EditorFeatures/CSharpTest/BlockCommentEditing/BlockCommentEditingTests.cs +++ b/src/EditorFeatures/CSharpTest/BlockCommentEditing/BlockCommentEditingTests.cs @@ -144,7 +144,7 @@ public void InsertOnStartLine2() "; var expected = @" /* - *$$*/ + * $$*/ "; Verify(code, expected); } @@ -243,7 +243,7 @@ public void InsertOnMiddleLine0() var expected = @" /* * - * $$ + *$$ "; Verify(code, expected); } @@ -342,7 +342,7 @@ public void InsertOnMiddleLine6() var expected = @" /* - * $$* + $$* */ "; Verify(code, expected); @@ -359,7 +359,7 @@ public void InsertOnMiddleLine7() var expected = @" /* ************* - * $$ + *$$ */ "; Verify(code, expected); @@ -376,7 +376,7 @@ public void InsertOnMiddleLine8() var expected = @" /** * - * $$ + *$$ */ "; Verify(code, expected); @@ -392,7 +392,7 @@ public void InsertOnMiddleLine9() var expected = @" /** * - * $$ + *$$ "; Verify(code, expected); } @@ -454,7 +454,7 @@ public void InsertOnEndLine3() var expected = @" /* - * $$*/ + $$*/ "; Verify(code, expected); } @@ -528,7 +528,7 @@ public void BoundCheckInsertOnStartLine1() /*$$ "; var expected = @" /* - *$$"; + * $$"; Verify(code, expected); } @@ -566,7 +566,7 @@ public void InsertOnStartLine2_Tab() "; var expected = @" /* - *$$*/ + * $$*/ "; VerifyTabs(code, expected); } diff --git a/src/Workspaces/CSharp/Portable/Indentation/CSharpIndentationService.Indenter.cs b/src/Workspaces/CSharp/Portable/Indentation/CSharpIndentationService.Indenter.cs index dfe7292f1f60513f68aa92e523fdb66cf6c79282..bcd53246f399e20c4b3c92d90979e6e3256a067d 100644 --- a/src/Workspaces/CSharp/Portable/Indentation/CSharpIndentationService.Indenter.cs +++ b/src/Workspaces/CSharp/Portable/Indentation/CSharpIndentationService.Indenter.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Linq; +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Extensions; using Microsoft.CodeAnalysis.CSharp.Formatting; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -49,15 +50,15 @@ protected override IndentationResult GetDesiredIndentationWorker(Indenter indent return null; var trivia = triviaOpt.Value; - if (!trivia.IsKind(SyntaxKind.SingleLineCommentTrivia)) + if (!trivia.IsSingleOrMultiLineComment() && !trivia.IsDocComment()) return null; - var line = indenter.Text.Lines.GetLineFromPosition(trivia.SpanStart); - if (line.GetFirstNonWhitespacePosition() != trivia.SpanStart) + var line = indenter.Text.Lines.GetLineFromPosition(trivia.FullSpan.Start); + if (line.GetFirstNonWhitespacePosition() != trivia.FullSpan.Start) return null; // Previous line just contained this single line comment. Align us with it. - return new IndentationResult(trivia.SpanStart, 0); + return new IndentationResult(trivia.FullSpan.Start, 0); } private IndentationResult? TryGetDesiredIndentation(Indenter indenter, SyntaxToken? tokenOpt)