From 1e0c9f0e887329f212075e28f01f8870f7043eab Mon Sep 17 00:00:00 2001 From: Will Fuqua Date: Fri, 26 Jun 2026 22:08:23 +0700 Subject: [PATCH] Auto-insert semicolon for simple variable declarations --- .../CSharpRepl.Services.csproj | 2 +- CSharpRepl.Services/Roslyn/RoslynServices.cs | 7 +++ .../Roslyn/Scripting/AutoInsertSemicolon.cs | 54 +++++++++++++++++++ CSharpRepl/CSharpRepl.csproj | 2 +- CSharpRepl/CSharpReplPromptCallbacks.cs | 13 ++++- .../AutoInsertSemicolonTests.cs | 49 +++++++++++++++++ .../CSharpRepl.Tests/CSharpRepl.Tests.csproj | 2 +- .../PromptConfigurationTests.cs | 53 +++++++++++++++++- Tests/CSharpRepl.Tests/RoslynServicesTests.cs | 1 + 9 files changed, 177 insertions(+), 6 deletions(-) create mode 100644 CSharpRepl.Services/Roslyn/Scripting/AutoInsertSemicolon.cs create mode 100644 Tests/CSharpRepl.Tests/AutoInsertSemicolonTests.cs diff --git a/CSharpRepl.Services/CSharpRepl.Services.csproj b/CSharpRepl.Services/CSharpRepl.Services.csproj index d98f2bea..23b13cd7 100644 --- a/CSharpRepl.Services/CSharpRepl.Services.csproj +++ b/CSharpRepl.Services/CSharpRepl.Services.csproj @@ -41,7 +41,7 @@ - + diff --git a/CSharpRepl.Services/Roslyn/RoslynServices.cs b/CSharpRepl.Services/Roslyn/RoslynServices.cs index 2c2325f7..6b97d2b4 100644 --- a/CSharpRepl.Services/Roslyn/RoslynServices.cs +++ b/CSharpRepl.Services/Roslyn/RoslynServices.cs @@ -340,6 +340,13 @@ public async Task IsTextCompleteStatementAsync(string text) return root is null || SyntaxFactory.IsCompleteSubmission(root.SyntaxTree); // if something's wrong and we can't get the syntax tree, we don't want to prevent evaluation. } + /// + /// When is a single-line statement the user didn't terminate (e.g. int i = 0), + /// returns it with the missing semicolon appended; otherwise . + /// It is deliberately editor-only; piped input is not auto-terminated. + /// + public string? TryAutoInsertSemicolon(string text) => AutoInsertSemicolon.TryAppend(text, parseOptions); + public async Task GetSpanToReplaceByCompletionAsync(string text, int caret, CancellationToken cancellationToken) { await Initialization.ConfigureAwait(false); diff --git a/CSharpRepl.Services/Roslyn/Scripting/AutoInsertSemicolon.cs b/CSharpRepl.Services/Roslyn/Scripting/AutoInsertSemicolon.cs new file mode 100644 index 00000000..9479f8c1 --- /dev/null +++ b/CSharpRepl.Services/Roslyn/Scripting/AutoInsertSemicolon.cs @@ -0,0 +1,54 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +using System; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace CSharpRepl.Services.Roslyn.Scripting; + +/// +/// If the user types "var x = 0" and submits, autoinsert the trailing semicolon. +/// +/// This is an interactive editor concern: the prompt uses it to decide whether Enter submits and to +/// show the inserted ; on the committed line. Evaluation itself is left untouched (piped input is not +/// auto-terminated). +/// +internal static class AutoInsertSemicolon +{ + /// + /// When is a single-line statement the user didn't terminate, returns it (trimmed) + /// with the missing semicolon appended, otherwise return + /// + public static string? TryAppend(string text, CSharpParseOptions parseOptions) + { + var trimmed = text.Trim(); + var tree = CSharpSyntaxTree.ParseText(trimmed, parseOptions); + if (SyntaxFactory.IsCompleteSubmission(tree)) + { + return null; // already complete — nothing to append (e.g. the expression `1 + 1`) + } + + // Only single-line input is auto-semicoloned; a statement spread across lines (e.g. a fluent chain whose + // cursor is at the end of line 2) is left alone so it can keep being edited. + if (trimmed.AsSpan().IndexOfAny('\n', '\r') >= 0) + { + return null; + } + + if (tree.GetRoot() is not CompilationUnitSyntax { Members: [.., FieldDeclarationSyntax { SemicolonToken.IsMissing: true }] }) + { + return null; + } + + var terminated = trimmed + ";"; + var fixedTree = CSharpSyntaxTree.ParseText(terminated, parseOptions); + return SyntaxFactory.IsCompleteSubmission(fixedTree) && + !fixedTree.GetDiagnostics().Any(d => d.Severity == DiagnosticSeverity.Error) + ? terminated + : null; + } +} diff --git a/CSharpRepl/CSharpRepl.csproj b/CSharpRepl/CSharpRepl.csproj index 90c46fd0..39578d4e 100644 --- a/CSharpRepl/CSharpRepl.csproj +++ b/CSharpRepl/CSharpRepl.csproj @@ -25,7 +25,7 @@ - +