|
5 | 5 | using System.Linq; |
6 | 6 | using System.Threading; |
7 | 7 | using Microsoft.CodeAnalysis.CSharp; |
| 8 | +using Microsoft.CodeAnalysis.CSharp.Syntax; |
8 | 9 | using Microsoft.CodeAnalysis.Editor.Shared.Extensions; |
9 | 10 | using Microsoft.CodeAnalysis.Editor.Shared.Utilities; |
10 | 11 | using Microsoft.CodeAnalysis.Indentation; |
@@ -50,74 +51,197 @@ public bool ExecuteCommand(ReturnKeyCommandArgs args, CommandExecutionContext co |
50 | 51 | if (position >= currentSnapshot.Length) |
51 | 52 | return false; |
52 | 53 |
|
53 | | - if (currentSnapshot[position] != '"') |
| 54 | + var cancellationToken = context.OperationContext.UserCancellationToken; |
| 55 | + |
| 56 | + var document = currentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); |
| 57 | + if (document == null) |
54 | 58 | return false; |
55 | 59 |
|
56 | | - var quotesBefore = 0; |
57 | | - var quotesAfter = 0; |
| 60 | + return currentSnapshot[position] == '"' |
| 61 | + ? ExecuteReturnCommandBeforeQuoteCharacter() |
| 62 | + : ExecuteReturnCommandNotBeforeQuoteCharacter(); |
58 | 63 |
|
59 | | - for (int i = position, n = currentSnapshot.Length; i < n; i++) |
| 64 | + bool ExecuteReturnCommandBeforeQuoteCharacter() |
60 | 65 | { |
61 | | - if (currentSnapshot[i] != '"') |
62 | | - break; |
63 | | - |
64 | | - quotesAfter++; |
| 66 | + var quotesBefore = 0; |
| 67 | + var quotesAfter = 0; |
| 68 | + |
| 69 | + // Ensure we're in between a balanced set of quotes, with at least 3 quotes on each side. |
| 70 | + |
| 71 | + var currentSnapshot = subjectBuffer.CurrentSnapshot; |
| 72 | + for (int i = position, n = currentSnapshot.Length; i < n; i++) |
| 73 | + { |
| 74 | + if (currentSnapshot[i] != '"') |
| 75 | + break; |
| 76 | + |
| 77 | + quotesAfter++; |
| 78 | + } |
| 79 | + |
| 80 | + for (var i = position - 1; i >= 0; i--) |
| 81 | + { |
| 82 | + if (currentSnapshot[i] != '"') |
| 83 | + break; |
| 84 | + |
| 85 | + quotesBefore++; |
| 86 | + } |
| 87 | + |
| 88 | + // We support two cases here. Something simple like `"""$$"""`. In this case, we have to be hitting enter |
| 89 | + // inside balanced quotes. But we also support `"""goo$$"""`. In this case it's ok if quotes are not |
| 90 | + // balanced. We're going to go through the non-empty path involving adding multiple newlines to the final |
| 91 | + // text. |
| 92 | + |
| 93 | + var isEmpty = quotesBefore > 0; |
| 94 | + if (isEmpty && quotesAfter != quotesBefore) |
| 95 | + return false; |
| 96 | + |
| 97 | + if (quotesAfter < 3) |
| 98 | + return false; |
| 99 | + |
| 100 | + // Looks promising based on text alone. Now ensure we're actually on a raw string token/expression. |
| 101 | + var parsedDocument = ParsedDocument.CreateSynchronously(document, cancellationToken); |
| 102 | + |
| 103 | + var token = parsedDocument.Root.FindToken(position); |
| 104 | + if (token.Kind() is not (SyntaxKind.SingleLineRawStringLiteralToken or |
| 105 | + SyntaxKind.MultiLineRawStringLiteralToken or |
| 106 | + SyntaxKind.InterpolatedSingleLineRawStringStartToken or |
| 107 | + SyntaxKind.InterpolatedMultiLineRawStringStartToken) || |
| 108 | + token.Parent is not ExpressionSyntax expression) |
| 109 | + { |
| 110 | + return false; |
| 111 | + } |
| 112 | + |
| 113 | + return MakeEdit(parsedDocument, expression, isEmpty); |
65 | 114 | } |
66 | 115 |
|
67 | | - for (var i = position - 1; i >= 0; i--) |
| 116 | + bool ExecuteReturnCommandNotBeforeQuoteCharacter() |
68 | 117 | { |
69 | | - if (currentSnapshot[i] != '"') |
70 | | - break; |
71 | | - |
72 | | - quotesBefore++; |
| 118 | + // If the caret is not on a quote, we need to find whether we are within the contents of a single-line raw |
| 119 | + // string literal but not inside an interpolation. If we are inside a raw string literal and the caret is not on |
| 120 | + // top of a quote, it is part of the literal's text. Here we try to ensure that the literal's closing quotes are |
| 121 | + // properly placed in their own line We could reach this point after pressing enter within a single-line raw |
| 122 | + // string |
| 123 | + |
| 124 | + var parsedDocument = ParsedDocument.CreateSynchronously(document, cancellationToken); |
| 125 | + |
| 126 | + var token = parsedDocument.Root.FindToken(position); |
| 127 | + ExpressionSyntax expression; |
| 128 | + switch (token.Kind()) |
| 129 | + { |
| 130 | + case SyntaxKind.SingleLineRawStringLiteralToken when token.Parent is ExpressionSyntax parentExpression: |
| 131 | + expression = parentExpression; |
| 132 | + break; |
| 133 | + |
| 134 | + case SyntaxKind.InterpolatedStringTextToken: |
| 135 | + case SyntaxKind.OpenBraceToken: |
| 136 | + if (token is not |
| 137 | + { |
| 138 | + Parent.Parent: InterpolatedStringExpressionSyntax |
| 139 | + { |
| 140 | + StringStartToken.RawKind: (int)SyntaxKind.InterpolatedSingleLineRawStringStartToken, |
| 141 | + } interpolatedStringExpression, |
| 142 | + }) |
| 143 | + { |
| 144 | + return false; |
| 145 | + } |
| 146 | + |
| 147 | + if (token.Kind() is SyntaxKind.OpenBraceToken && position != token.SpanStart) |
| 148 | + return false; |
| 149 | + |
| 150 | + expression = interpolatedStringExpression; |
| 151 | + break; |
| 152 | + |
| 153 | + default: |
| 154 | + return false; |
| 155 | + } |
| 156 | + |
| 157 | + return MakeEdit(parsedDocument, expression, isEmpty: false); |
73 | 158 | } |
74 | 159 |
|
75 | | - if (quotesAfter != quotesBefore) |
76 | | - return false; |
77 | | - |
78 | | - if (quotesAfter < 3) |
79 | | - return false; |
80 | | - |
81 | | - return SplitRawString(textView, subjectBuffer, span.Start.Position, CancellationToken.None); |
82 | | - } |
83 | | - |
84 | | - private bool SplitRawString(ITextView textView, ITextBuffer subjectBuffer, int position, CancellationToken cancellationToken) |
85 | | - { |
86 | | - var document = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); |
87 | | - if (document == null) |
88 | | - return false; |
89 | | - |
90 | | - var parsedDocument = ParsedDocument.CreateSynchronously(document, cancellationToken); |
91 | | - |
92 | | - var token = parsedDocument.Root.FindToken(position); |
93 | | - if (token.Kind() is not (SyntaxKind.SingleLineRawStringLiteralToken or |
94 | | - SyntaxKind.MultiLineRawStringLiteralToken or |
95 | | - SyntaxKind.InterpolatedSingleLineRawStringStartToken or |
96 | | - SyntaxKind.InterpolatedMultiLineRawStringStartToken)) |
| 160 | + bool MakeEdit( |
| 161 | + ParsedDocument parsedDocument, |
| 162 | + ExpressionSyntax expression, |
| 163 | + bool isEmpty) |
97 | 164 | { |
98 | | - return false; |
| 165 | + var project = document.Project; |
| 166 | + var indentationOptions = subjectBuffer.GetIndentationOptions(_editorOptionsService, project.GetFallbackAnalyzerOptions(), project.Services, explicitFormat: false); |
| 167 | + var indentation = expression.GetFirstToken().GetPreferredIndentation(parsedDocument, indentationOptions, cancellationToken); |
| 168 | + |
| 169 | + var newLine = indentationOptions.FormattingOptions.NewLine; |
| 170 | + |
| 171 | + using var transaction = CaretPreservingEditTransaction.TryCreate( |
| 172 | + CSharpEditorResources.Split_raw_string, textView, _undoHistoryRegistry, _editorOperationsFactoryService); |
| 173 | + var edit = subjectBuffer.CreateEdit(); |
| 174 | + |
| 175 | + if (isEmpty) |
| 176 | + { |
| 177 | + // If the literal is empty, we just want to help the user transform it into a multiline raw string |
| 178 | + // literal with the extra empty newline between the delimiters to place the caret at |
| 179 | + edit.Insert(position, newLine + newLine + indentation); |
| 180 | + |
| 181 | + var finalSnapshot = edit.Apply(); |
| 182 | + |
| 183 | + // move caret to the right location in virtual space for the blank line we added. |
| 184 | + var lineInNewSnapshot = finalSnapshot.GetLineFromPosition(position); |
| 185 | + var nextLine = finalSnapshot.GetLineFromLineNumber(lineInNewSnapshot.LineNumber + 1); |
| 186 | + textView.Caret.MoveTo(new VirtualSnapshotPoint(nextLine, indentation.Length)); |
| 187 | + } |
| 188 | + else |
| 189 | + { |
| 190 | + // Otherwise, we're starting with a raw string that has content in it. That's something like: |
| 191 | + // """GooBar""". If we hit enter at the `G` we only want to insert a single new line before the caret. |
| 192 | + // However, if we were to hit enter anywhere after that, we want two new lines inserted. One after the |
| 193 | + // `"""` and one at the caret itself. |
| 194 | + var newLineAndIndentation = newLine + indentation; |
| 195 | + |
| 196 | + // Add a newline at the position of the end literal |
| 197 | + var closingStart = GetStartPositionOfClosingDelimiter(expression); |
| 198 | + edit.Insert(closingStart, newLineAndIndentation); |
| 199 | + |
| 200 | + // Add a newline at the caret's position, to insert the newline that the user requested |
| 201 | + edit.Insert(position, newLineAndIndentation); |
| 202 | + |
| 203 | + // Also add a newline at the start of the text, only if there is text before the caret's position |
| 204 | + var insertedLinesBeforeCaret = 1; |
| 205 | + var openingEnd = GetEndPositionOfOpeningDelimiter(expression); |
| 206 | + if (openingEnd != position) |
| 207 | + { |
| 208 | + insertedLinesBeforeCaret = 2; |
| 209 | + edit.Insert(openingEnd, newLineAndIndentation); |
| 210 | + } |
| 211 | + |
| 212 | + var finalSnapshot = edit.Apply(); |
| 213 | + |
| 214 | + var lineInNewSnapshot = finalSnapshot.GetLineFromPosition(openingEnd); |
| 215 | + var nextLine = finalSnapshot.GetLineFromLineNumber(lineInNewSnapshot.LineNumber + insertedLinesBeforeCaret); |
| 216 | + textView.Caret.MoveTo(new VirtualSnapshotPoint(nextLine, indentation.Length)); |
| 217 | + } |
| 218 | + |
| 219 | + transaction?.Complete(); |
| 220 | + return true; |
99 | 221 | } |
100 | 222 |
|
101 | | - var indentationOptions = subjectBuffer.GetIndentationOptions(_editorOptionsService, document.Project.GetFallbackAnalyzerOptions(), document.Project.Services, explicitFormat: false); |
102 | | - var indentation = token.GetPreferredIndentation(parsedDocument, indentationOptions, cancellationToken); |
103 | | - |
104 | | - var newLine = indentationOptions.FormattingOptions.NewLine; |
| 223 | + int GetStartPositionOfClosingDelimiter(ExpressionSyntax expression) |
| 224 | + { |
| 225 | + if (expression is InterpolatedStringExpressionSyntax interpolatedStringExpression) |
| 226 | + return interpolatedStringExpression.StringEndToken.Span.Start; |
105 | 227 |
|
106 | | - using var transaction = CaretPreservingEditTransaction.TryCreate( |
107 | | - CSharpEditorResources.Split_string, textView, _undoHistoryRegistry, _editorOperationsFactoryService); |
| 228 | + var index = expression.Span.End; |
| 229 | + while (currentSnapshot[index - 1] == '"') |
| 230 | + index--; |
108 | 231 |
|
109 | | - var edit = subjectBuffer.CreateEdit(); |
| 232 | + return index; |
| 233 | + } |
110 | 234 |
|
111 | | - // apply the change: |
112 | | - edit.Insert(position, newLine + newLine + indentation); |
113 | | - var snapshot = edit.Apply(); |
| 235 | + int GetEndPositionOfOpeningDelimiter(ExpressionSyntax expression) |
| 236 | + { |
| 237 | + if (expression is InterpolatedStringExpressionSyntax interpolatedStringExpression) |
| 238 | + return interpolatedStringExpression.StringStartToken.Span.End; |
114 | 239 |
|
115 | | - // move caret: |
116 | | - var lineInNewSnapshot = snapshot.GetLineFromPosition(position); |
117 | | - var nextLine = snapshot.GetLineFromLineNumber(lineInNewSnapshot.LineNumber + 1); |
118 | | - textView.Caret.MoveTo(new VirtualSnapshotPoint(nextLine, indentation.Length)); |
| 240 | + var index = expression.Span.Start; |
| 241 | + while (currentSnapshot[index] == '"') |
| 242 | + index++; |
119 | 243 |
|
120 | | - transaction?.Complete(); |
121 | | - return true; |
| 244 | + return index; |
| 245 | + } |
122 | 246 | } |
123 | 247 | } |
0 commit comments