Skip to content

ADR 0111: LineNumber and LineRange as editor domain types

ADR Role
0081 Semantics of parametrics by strings; 0111 - VO LR/LN in IDE domain
0109 Directory, tail_signature; :ln - directory metadata
0110 Roslyn by range; future bridge to MCP arguments

Summary

  • Value objects LineNumber / LineRange (1-based, Start ≤ End).
  • Border to JSON/commands - int in args; ParsedLineRange for parsing.

Rejected alternatives

  • Only :ln aliases in TOML - do not replace API checks and self-documentation in C#.
  • record with int without invariants - duplication of checks in each consumer.
  • Complete transition of commands to typed DTO instead of JSON - outside scope v1; the boundary remains on serialization.
  • Special tokens “to the end of the file” (7:end, 7:*, etc.) - not included in v1: separate mini-grammar, interaction with file length in parse vs build, risk of collisions; left to a separate decision when there is a clear need.

Consequences (current)

  • New scripts only by lines in the parametric melody branch → preferably via LR/LN.
  • The "line is in file" check remains in ParametricLineRangeArgsBuilder, not in the order of the two numbers in the input and not in LN/LR per se.

Solution

  • Enter value objects in the CascadeIDE.Models.Editor namespace:
  • LN (LineNumber) — 1-based line number; invariant Value >= LineNumber.MinimumOneBasedInclusive (1); creation via TryCreate(int, out LineNumber); comparison operators (including for CA1036 / IComparable).
  • LR (LineRange) - a pair Start / End of type LN with the invariant Start <= End (inclusive range in terms of the editor and IDE commands); creation via TryCreate(LineNumber, LineNumber, out LineRange).
  • ParametricIntentMelody.ParsedLineRange stores the range as LineRange Lines, rather than two "naked" ints.
  • Melody tail parser (TryExtractLineRangeFromRemainder in ParametricIntentMelody):
  • One whole without a second slot - one line (<line> has the same meaning as <line>:<line> as LR).
  • Two integers — boundaries of one inclusive range; the input order is not important: after parsing, min..max is applied (for example 7:3 and 3:7 → one LR).
  • LineRange.TryCreate for explicit code still has a "second argument not before the first" contract; "inverted" input is processed before the TryCreate call, due to normalization in the parser.
  • At the border to JSON (ParametricLineRangeArgsBuilder) int are still sent to anonymous DTOs via .Value in LN - wire format IdeCommands.Select / IdeCommands.ApplyEdit does not change.
  • IntentMelodyTailSemantics.MinEditorLineNumber is consistent with LineNumber.MinimumOneBasedInclusive (one source constant for "minimal line 1-based").

Implementation v1 (sources in code)

Artifact Destination
Models/Editor/LineNumber.cs LN, minimum constant, TryCreate, equality and comparison
Models/Editor/LineRange.cs LR, TryCreate with End >= Start
Services/ParametricIntentMelody.cs ParsedLineRange, TryParseLineRangeTail, TryExtractLineRangeFromRemainder, delegation to ParametricLineRangeArgsBuilder
Services/ParametricLineRangeArgsBuilder.cs Assembling JSON-args from ParsedLineRange.Lines + checking for file length overshoot
Services/IntentMelodyTailSemantics.cs MinEditorLineNumber → link to minimum LN
CascadeIDE.Tests/EditorLineNumberRangeTests.cs LN/LR Unit Tests
CascadeIDE.Tests/ParametricIntentMelodyTests.cs Parsing, one line, min..max, build args
IntentMelody/intent-melody-aliases.toml and copy to publish-gh-release/IntentMelody/ Palette tooltips for els / eld (range and one line)

The directory still describes two numeric slots in tail_signature (<start:ln>:<end:ln>); the "single number" abbreviation is application parser convention, not a separate directory string.

Implementation v2 (roadmap after v1 - closed in code)

Artifact Destination
Models/Editor/ColumnNumber.cs CN - 1-based column (TryCreate, compare)
Models/Editor/EditorDocumentPath.cs Document path wrapper: CanonicalFilePath.TryNormalize, case-insensitive comparison
Models/Editor/EditorMcpSpans.cs EditorTextSpan.TryParse, EditorContentLineRangeMcpArgs.TryParse, EditorGoToPositionMcpArgs.TryParse
Services/RoslynLinePositionMapper.cs Microsoft.CodeAnalysis.Text.LinePosition (0-based) → (LineNumber, ColumnNumber) for UI/MCP
Services/ContextMinimizer.cs, Services/WorkspaceDiagnosticsCoordinator.cs Use mapper instead of duplicating +1
ViewModels/IdeMcpCommandExecutor.Handlers.Editor/*.cs Select / ApplyEdit / GoToPosition / GetEditorContentRange via general VO parsing
Services/ParametricLineRangeArgsBuilder.cs Column boundaries via ColumnNumber + canonical EditorDocumentPath
Features/WebAiPortal/Application/WebAiPortalChatMixInFormatter.cs Editor range snapshot: LineRange instead of "naked" int lines
CascadeIDE.Tests/EditorMcpSpansTests.cs, extension EditorLineNumberRangeTests MCP Parser Failures and Limits
ADR Role
0081 Semantics of parametrics by strings; 0111 - VO LR/LN in IDE domain
0109 Directory, tail_signature; :ln - directory metadata
0110 Roslyn by range; future bridge to MCP arguments

Rejected alternatives

  • Only :ln aliases in TOML - do not replace API checks and self-documentation in C#.
  • record with int without invariants - duplication of checks in each consumer.
  • Complete transition of commands to typed DTO instead of JSON - outside scope v1; the boundary remains on serialization.
  • Special tokens “to the end of the file” (7:end, 7:*, etc.) - not included in v1: separate mini-grammar, interaction with file length in parse vs build, risk of collisions; left to a separate decision when there is a clear need.

Consequences (current)

  • New scripts only by lines in the parametric melody branch → preferably via LR/LN.
  • The "line is in file" check remains in ParametricLineRangeArgsBuilder, not in the order of the two numbers in the input and not in LN/LR per se.

Roadmap (after v1) - Done (v2)

The goal of the v2 iterations is the same pattern: invariants in types up to the JSON/MCP boundary, without changing the IdeCommands wire contracts.

Priority 1 - MCP boundary → editor - done

  • EditorTextSpan + ColumnNumber, TryParse from IReadOnlyDictionary<string, JsonElement> for Select / ApplyEdit; EditorGoToPositionMcpArgs for go_to_position; EditorContentLineRangeMcpArgs for get_editor_content_range (if there are no keys - 1..1 as before; inverted explicit range - failure with message).

Priority 2 - web portal - done

  • WebAiPortalChatMixInFormatter: EditorRangeSnap stores LineRange; JSON parsing normalizes boundaries through min/max when ints are “reversed” in the wire response.

Priority 3 - parametric args - done

  • ParametricLineRangeArgsBuilder: columns via ColumnNumber.TryCreate, path via EditorDocumentPath.

Priority 4 - Roslyn - done

  • RoslynLinePositionMapper: LinePosition (0-based) → (LineNumber, ColumnNumber); ContextMinimizer, WorkspaceDiagnosticsCoordinator.

File path - done

  • EditorDocumentPath over CanonicalFilePath.TryNormalize.

Readiness criterion

  • Two call sites with one set of JSON fields (Select + ApplyEdit) → EditorTextSpan; tests in EditorMcpSpansTests.