Skip to content

Refactor Repl.Core: organize files, extract CoreReplApp engines, fix new analyzers#19

Merged
carldebilly merged 12 commits intomainfrom
dev/cdb/repl-core-refactoring
Apr 7, 2026
Merged

Refactor Repl.Core: organize files, extract CoreReplApp engines, fix new analyzers#19
carldebilly merged 12 commits intomainfrom
dev/cdb/repl-core-refactoring

Conversation

@carldebilly
Copy link
Copy Markdown
Member

@carldebilly carldebilly commented Apr 7, 2026

Summary

Major structural refactoring of Repl.Core to reduce complexity and improve maintainability.

  • Organize files: move 77 internal files from root into 12 thematic subdirectories (Routing/, Parsing/, Console/, Help/, Session/, Output/, Rendering/, Autocomplete/, Parameters/, Documentation/, Terminal/, ShellCompletion/). Root file count reduced from 122 to ~45.
  • Split oversized files: CoreReplApp.Interactive.cs, ConsoleLineReader.cs, HelpTextBuilder.cs split into focused partials. Flatten unnecessary Public/ subdirectories.
  • Extract CoreReplApp engines: break the 5,244-line CoreReplApp monolith into 5 standalone internal classes — DocumentationEngine, ShellCompletionEngine, AutocompleteEngine, RoutingEngine, InteractiveSession. CoreReplApp remains as facade with Execution as the only remaining substantial partial.
  • Reduce public surface: AmbientCommandDefinition made internal.
  • Fix new analyzer violations: resolve CA1068 (CancellationToken parameter ordering), MA0045 (sync call sites), MA0171 (pattern matching). Relax MA0045/MA0167 for test projects via .editorconfig.

Breaking changes

  • AskOptions, AskSecretOptions, AskNumberOptions<T>, AskMultiChoiceOptions: CancellationToken moved from first to last positional parameter (CA1068). Code using positional construction (new AskOptions(ct)) must switch to named parameters (new AskOptions(CancellationToken: ct)).
  • AmbientCommandDefinition: changed from public to internal.

Test plan

  • dotnet build src/ — 0 errors
  • Repl.Tests — 279/279 passed
  • Repl.IntegrationTests — 383/383 passed
  • Repl.McpTests — 113/113 passed

Open with Devin

Reorganize 52 internal implementation files from the root of Repl.Core
into six thematic subdirectories: Routing/ (15), Parsing/ (14),
Output/ (7), Console/ (9), Help/ (3), Session/ (4).

All files retain namespace Repl so there is no compilation impact.
Root file count drops from 122 to 75.
Split four oversized files into partial classes:
- CoreReplApp.Interactive.cs (1859) -> Interactive (628) + Autocomplete (1237)
- CoreReplApp.cs (1576) -> CoreReplApp (794) + Execution (806)
- ConsoleLineReader.cs (1425) -> ConsoleLineReader (598) + Autocomplete (833)
- HelpTextBuilder.cs (896) -> HelpTextBuilder (286) + Rendering (617)

Remove redundant Public/ subdirectories by moving their contents into
parent directories: Autocomplete, Interaction, Rendering, ShellCompletion,
and Terminal.
This type is only constructed internally by AmbientCommandOptions.MapAmbient
and stored in an internal dictionary. No external consumer needs direct access.
Relocate internal and public types from the Repl.Core root into their
logical subdirectories: Rendering (4), Output (1), Parsing (4),
Interaction (1), Console (4), Autocomplete (2), Parameters (3),
Documentation (1), Terminal (2), Session (2), ShellCompletion (1).

Root file count drops from 77 to 52.
Reorder CancellationToken to last position in Ask*Options records
(CA1068 breaking change). Suppress MA0045 for intentionally synchronous
console I/O code (Console.ReadKey/KeyAvailable have no async
alternatives). Use pattern matching instead of HasValue (MA0171).
Relax MA0045 and MA0167 in test/sample projects via .editorconfig.
Promote private nested types (ActiveRoutingGraph, PrefixTemplate,
ContextValidationOutcome, AmbientCommandOutcome) to standalone internal
files. Promote key methods from private to internal for cross-class
access. Merge OptionParsing partial (62 lines) into Execution partial.
Move all documentation model generation logic into a standalone
DocumentationEngine class. CoreReplApp.Documentation.cs becomes a thin
delegation layer. No behavioral changes.
Move shell completion candidate resolution into a standalone
ShellCompletionEngine class. Promote shared utility methods
(IsGlobalOptionToken, TokenizeInputSpans, CollectVisibleMatchingRoutes)
to internal visibility for cross-class access.
Move the entire autocomplete suggestion engine (1237 lines) into a
standalone AutocompleteEngine class including token classification,
candidate collection, live hints, and input analysis. Move shared
utilities (IsGlobalOptionToken, TokenizeInputSpans, TokenSpan) into
AutocompleteEngine as internal members.
Move context registration/validation, route resolution helpers, banner
rendering, discoverable route/context filtering, prefix resolution, and
error generation into a standalone RoutingEngine class. CoreReplApp
Routing partial becomes a thin delegation layer.
Move interactive REPL session management, ambient command dispatch,
history handling, prompt building, and input tokenization into a
standalone InteractiveSession class. Execution calls Interactive
through the app facade, breaking the bidirectional partial coupling.
Comment on lines +505 to +516
foreach (var route in matchingRoutes)
{
if (commandPrefix.Length < route.Template.Segments.Count
&& route.Template.Segments[commandPrefix.Length] is LiteralRouteSegment literal
&& literal.Value.StartsWith(currentTokenPrefix, prefixComparison))
{
candidates.Add(new ConsoleLineReader.AutocompleteSuggestion(
literal.Value,
Description: route.Command.Description,
Kind: ConsoleLineReader.AutocompleteSuggestionKind.Command));
}
}
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged — keeping the existing foreach+if pattern here for consistency and readability. The LINQ Where alternative adds allocations without meaningful clarity gain in these hot paths.

Comment on lines +227 to +233
foreach (var option in StaticShellGlobalOptions)
{
if (option.StartsWith(currentTokenPrefix, comparison))
{
TryAddShellCompletionCandidate(option, dedupe, candidates);
}
}
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged — keeping the existing foreach+if pattern here for consistency and readability. The LINQ Where alternative adds allocations without meaningful clarity gain in these hot paths.

Comment on lines +260 to +266
foreach (var alias in custom.Aliases)
{
if (alias.StartsWith(currentTokenPrefix, comparison))
{
TryAddShellCompletionCandidate(alias, dedupe, candidates);
}
}
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged — keeping the existing foreach+if pattern here for consistency and readability. The LINQ Where alternative adds allocations without meaningful clarity gain in these hot paths.

devin-ai-integration[bot]

This comment was marked as resolved.

- Fix bug: AddRouteShellOptionCandidates now reads OptionCaseSensitivity
  instead of hardcoding OrdinalIgnoreCase
- Remove dead store (quote = null) in AutocompleteEngine tokenizer
- Remove redundant ToString() calls on StringBuilder in ConsoleLineReader
- Fix misaligned pragma indentation in InvocationOptionParser
- Fix pragma restore splitting method call in RichPromptInteractionHandler
Comment on lines +279 to +285
foreach (var token in route.OptionSchema.KnownTokens)
{
if (token.StartsWith(currentTokenPrefix, comparison))
{
TryAddShellCompletionCandidate(token, dedupe, candidates);
}
}
Comment on lines +312 to +318
foreach (var token in tokens)
{
if (token.End <= cursor)
{
trailingPriorCount++;
}
}
Comment on lines +327 to +333
foreach (var token in tokens)
{
if (token.End <= cursor)
{
trailingPrior[index++] = token.Value;
}
}
Comment on lines +378 to +385
foreach (var extension in KnownExecutableExtensions)
{
if (fileName.EndsWith(extension, StringComparison.OrdinalIgnoreCase))
{
var head = fileName[..^extension.Length];
return string.IsNullOrWhiteSpace(head) ? null : head;
}
}
Comment on lines +555 to +562
foreach (var contextMatch in contextMatches)
{
var prefixLength = contextMatch.Context.Template.Segments.Count;
if (prefixLength > longestPrefixLength)
{
longestPrefixLength = prefixLength;
}
}
Comment on lines +235 to +242
foreach (var alias in options.Output.Aliases.Keys)
{
var opt = $"--{alias}";
if (opt.StartsWith(currentTokenPrefix, comparison))
{
TryAddShellCompletionCandidate(opt, dedupe, candidates);
}
}
Comment on lines +244 to +251
foreach (var format in options.Output.Transformers.Keys)
{
var opt = $"--output:{format}";
if (opt.StartsWith(currentTokenPrefix, comparison))
{
TryAddShellCompletionCandidate(opt, dedupe, candidates);
}
}
@carldebilly carldebilly merged commit 51e70e4 into main Apr 7, 2026
12 checks passed
@carldebilly carldebilly deleted the dev/cdb/repl-core-refactoring branch April 7, 2026 02:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant