Skip to content

Commit

Permalink
Update formatOnType handler to support formatting on NewLine (#76876)
Browse files Browse the repository at this point in the history
Enables format on type when the trigger character is a newline. Removes
text changes that contain the cursor as we do not want to remove the
indentation.



https://github.com/user-attachments/assets/962f8229-f41b-4632-a8e9-b7660575e448



Resolves dotnet/vscode-csharp#6834
  • Loading branch information
JoeRobich authored Jan 23, 2025
2 parents a4f3d29 + 05dbe1c commit fc33f3d
Show file tree
Hide file tree
Showing 2 changed files with 113 additions and 50 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,13 @@ public FormatDocumentOnTypeHandler(IGlobalOptionService globalOptions)
if (document is null)
return null;

var position = await document.GetPositionFromLinePositionAsync(ProtocolConversions.PositionToLinePosition(request.Position), cancellationToken).ConfigureAwait(false);

if (string.IsNullOrEmpty(request.Character) || SyntaxFacts.IsNewLine(request.Character[0]))
if (string.IsNullOrEmpty(request.Character))
{
return [];
}

var position = await document.GetPositionFromLinePositionAsync(ProtocolConversions.PositionToLinePosition(request.Position), cancellationToken).ConfigureAwait(false);

var formattingService = document.Project.Services.GetRequiredService<ISyntaxFormattingService>();
var documentSyntax = await ParsedDocument.CreateAsync(document, cancellationToken).ConfigureAwait(false);

Expand All @@ -72,6 +72,37 @@ public FormatDocumentOnTypeHandler(IGlobalOptionService globalOptions)
return [];
}

if (SyntaxFacts.IsNewLine(request.Character[0]))
{
// When formatting after a newline is pressed, the cursor line will be all whitespace
// and we do not want to remove the indentation from it.
//
// Take the following example of pressing enter after an opening brace.
//
// ```
// public void M() {||}
// ```
//
// The editor moves the cursor to the next line and uses it's languageconfig to add
// the appropriate level of indentation.
//
// ```
// public void M() {
// ||
// }
// ```
//
// At this point `formatOnType` is called. The formatting service will generate two
// text changes. The first moves the opening brace to a new line with proper
// indentation. The second removes the whitespace from the cursor line and rewrites
// the indentation prior to the closing brace.
//
// Letting the second change go through would be a bad experience for the user as they
// will now be responsible for adding back the proper indentation.

textChanges = textChanges.WhereAsArray(static (change, position) => !change.Span.Contains(position), position);
}

return [.. textChanges.Select(change => ProtocolConversions.TextChangeToTextEdit(change, documentSyntax.Text))];
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
// 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.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
Expand All @@ -24,74 +23,107 @@ public FormatDocumentOnTypeTests(ITestOutputHelper testOutputHelper) : base(test
public async Task TestFormatDocumentOnTypeAsync(bool mutatingLspWorkspace)
{
var markup =
@"class A
{
void M()
{
if (true)
{{|type:|}
}
}";
"""
class A
{
void M()
{
if (true)
{{|type:|}
}
}
""";
var expected =
@"class A
{
void M()
{
if (true)
{
}
}";
"""
class A
{
void M()
{
if (true)
{
}
}
""";
await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace);
var characterTyped = ";";
var locationTyped = testLspServer.GetLocations("type").Single();
var documentText = await testLspServer.GetDocumentTextAsync(locationTyped.Uri);

var results = await RunFormatDocumentOnTypeAsync(testLspServer, characterTyped, locationTyped);
var actualText = ApplyTextEdits(results, documentText);
Assert.Equal(expected, actualText);
await AssertFormatDocumentOnTypeAsync(testLspServer, characterTyped, locationTyped, expected);
}

[Theory, CombinatorialData]
public async Task TestFormatDocumentOnType_UseTabsAsync(bool mutatingLspWorkspace)
{
var markup =
@"class A
{
void M()
{
if (true)
{{|type:|}
}
}";
"""
class A
{
void M()
{
if (true)
{{|type:|}
}
}
""";
var expected =
@"class A
{
void M()
{
if (true)
{
}
}";
"""
class A
{
void M()
{
if (true)
{
}
}
""";
await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace);
var characterTyped = ";";
var locationTyped = testLspServer.GetLocations("type").Single();
var documentText = await testLspServer.GetDocumentTextAsync(locationTyped.Uri);
await AssertFormatDocumentOnTypeAsync(testLspServer, characterTyped, locationTyped, expected, insertSpaces: false, tabSize: 4);
}

var results = await RunFormatDocumentOnTypeAsync(testLspServer, characterTyped, locationTyped, insertSpaces: false, tabSize: 4);
var actualText = ApplyTextEdits(results, documentText);
Assert.Equal(expected, actualText);
[Theory, CombinatorialData]
public async Task TestFormatDocumentOnType_NewLine(bool mutatingLspWorkspace)
{
var markup =
"""
class A
{
void M() {
{|type:|}
}
}
""";
var expected =
"""
class A
{
void M()
{
}
}
""";
await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace);
var characterTyped = "\n";
var locationTyped = testLspServer.GetLocations("type").Single();
await AssertFormatDocumentOnTypeAsync(testLspServer, characterTyped, locationTyped, expected);
}

private static async Task<LSP.TextEdit[]> RunFormatDocumentOnTypeAsync(
private static async Task AssertFormatDocumentOnTypeAsync(
TestLspServer testLspServer,
string characterTyped,
LSP.Location locationTyped,
[StringSyntax(PredefinedEmbeddedLanguageNames.CSharpTest)] string expectedText,
bool insertSpaces = true,
int tabSize = 4)
{
return await testLspServer.ExecuteRequestAsync<LSP.DocumentOnTypeFormattingParams, LSP.TextEdit[]>(LSP.Methods.TextDocumentOnTypeFormattingName,
CreateDocumentOnTypeFormattingParams(
characterTyped, locationTyped, insertSpaces, tabSize), CancellationToken.None);
var documentText = await testLspServer.GetDocumentTextAsync(locationTyped.Uri);
var results = await testLspServer.ExecuteRequestAsync<LSP.DocumentOnTypeFormattingParams, LSP.TextEdit[]>(
LSP.Methods.TextDocumentOnTypeFormattingName,
CreateDocumentOnTypeFormattingParams(characterTyped, locationTyped, insertSpaces, tabSize),
CancellationToken.None);
var actualText = ApplyTextEdits(results, documentText);
Assert.Equal(expectedText, actualText);
}

private static LSP.DocumentOnTypeFormattingParams CreateDocumentOnTypeFormattingParams(
Expand Down

0 comments on commit fc33f3d

Please sign in to comment.