| 1 | <!-- English translation of adr/0111-editor-linenumber-linerange-value-objects.md. Canonical Russian: ../../adr/0111-editor-linenumber-linerange-value-objects.md --> |
| 2 | |
| 3 | # ADR 0111: `LineNumber` and `LineRange` as editor domain types |
| 4 | |
| 5 | ## Related ADRs |
| 6 | |
| 7 | | ADR | Role | |
| 8 | |-----|------| |
| 9 | | [0081](0081-parametric-intent-melodies-editor-line-ranges.md) | Semantics of parametrics by strings; 0111 - VO LR/LN in IDE domain | |
| 10 | | [0109](0109-declarative-parametric-melody-catalog-toml-and-code-binders.md) | Directory, `tail_signature`; `:ln` - directory metadata | |
| 11 | | [0110](0110-roslyn-refactor-intent-melody-bridge.md) | Roslyn by range; future bridge to MCP arguments | |
| 12 | |
| 13 | ## Summary |
| 14 | |
| 15 | - Value objects **`LineNumber`** / **`LineRange`** (1-based, Start ≤ End). |
| 16 | - Border to JSON/commands - `int` in args; `ParsedLineRange` for parsing. |
| 17 | |
| 18 | |
| 19 | ## Rejected alternatives |
| 20 | |
| 21 | - **Only `:ln` aliases in TOML** - do not replace API checks and self-documentation in C#. |
| 22 | - **`record` with int without invariants** - duplication of checks in each consumer. |
| 23 | - **Complete transition of commands to typed DTO instead of JSON** - outside scope v1; the boundary remains on serialization. |
| 24 | - **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. |
| 25 | |
| 26 | ## Consequences (current) |
| 27 | |
| 28 | - New scripts **only by lines** in the parametric melody branch → preferably via LR/LN. |
| 29 | - 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. |
| 30 | |
| 31 | --- |
| 32 | ## Solution |
| 33 | |
| 34 | - Enter **value objects** in the `CascadeIDE.Models.Editor` namespace: |
| 35 | - **LN (`LineNumber`)** — 1-based line number; invariant `Value >= LineNumber.MinimumOneBasedInclusive` (1); creation via `TryCreate(int, out LineNumber)`; comparison operators (including for CA1036 / `IComparable`). |
| 36 | - **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)`. |
| 37 | - **`ParametricIntentMelody.ParsedLineRange`** stores the range as **`LineRange Lines`**, rather than two "naked" `ints`. |
| 38 | - Melody tail parser (`TryExtractLineRangeFromRemainder` in `ParametricIntentMelody`): |
| 39 | - **One whole** without a second slot - **one line** (`<line>` has the same meaning as `<line>:<line>` as LR). |
| 40 | - **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). |
| 41 | - **`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. |
| 42 | - 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. |
| 43 | - **`IntentMelodyTailSemantics.MinEditorLineNumber`** is consistent with **`LineNumber.MinimumOneBasedInclusive`** (one source constant for "minimal line 1-based"). |
| 44 | |
| 45 | ## Implementation v1 (sources in code) |
| 46 | |
| 47 | | Artifact | Destination | |
| 48 | |----------|-----------| |
| 49 | | `Models/Editor/LineNumber.cs` | LN, minimum constant, `TryCreate`, equality and comparison | |
| 50 | | `Models/Editor/LineRange.cs` | LR, `TryCreate` with `End >= Start` | |
| 51 | | `Services/ParametricIntentMelody.cs` | `ParsedLineRange`, `TryParseLineRangeTail`, `TryExtractLineRangeFromRemainder`, delegation to `ParametricLineRangeArgsBuilder` | |
| 52 | | `Services/ParametricLineRangeArgsBuilder.cs` | Assembling JSON-args from `ParsedLineRange.Lines` + checking for file length overshoot | |
| 53 | | `Services/IntentMelodyTailSemantics.cs` | `MinEditorLineNumber` → link to minimum LN | |
| 54 | | `CascadeIDE.Tests/EditorLineNumberRangeTests.cs` | LN/LR Unit Tests | |
| 55 | | `CascadeIDE.Tests/ParametricIntentMelodyTests.cs` | Parsing, one line, `min..max`, build args | |
| 56 | | `IntentMelody/intent-melody-aliases.toml` and copy to `publish-gh-release/IntentMelody/` | Palette tooltips for `els` / `eld` (range and one line) | |
| 57 | |
| 58 | 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. |
| 59 | |
| 60 | ## Implementation v2 (roadmap after v1 - closed in code) |
| 61 | | Artifact | Destination | |
| 62 | |----------|-----------| |
| 63 | | `Models/Editor/ColumnNumber.cs` | CN - 1-based column (`TryCreate`, compare) | |
| 64 | | `Models/Editor/EditorDocumentPath.cs` | Document path wrapper: `CanonicalFilePath.TryNormalize`, case-insensitive comparison | |
| 65 | | `Models/Editor/EditorMcpSpans.cs` | `EditorTextSpan.TryParse`, `EditorContentLineRangeMcpArgs.TryParse`, `EditorGoToPositionMcpArgs.TryParse` | |
| 66 | | `Services/RoslynLinePositionMapper.cs` | `Microsoft.CodeAnalysis.Text.LinePosition` (0-based) → `(LineNumber, ColumnNumber)` for UI/MCP | |
| 67 | | `Services/ContextMinimizer.cs`, `Services/WorkspaceDiagnosticsCoordinator.cs` | Use mapper instead of duplicating `+1` | |
| 68 | | `ViewModels/IdeMcpCommandExecutor.Handlers.Editor/*.cs` | `Select` / `ApplyEdit` / `GoToPosition` / `GetEditorContentRange` via general VO parsing | |
| 69 | | `Services/ParametricLineRangeArgsBuilder.cs` | Column boundaries via `ColumnNumber` + canonical `EditorDocumentPath` | |
| 70 | | `Features/WebAiPortal/Application/WebAiPortalChatMixInFormatter.cs` | Editor range snapshot: `LineRange` instead of "naked" int lines | |
| 71 | | `CascadeIDE.Tests/EditorMcpSpansTests.cs`, extension `EditorLineNumberRangeTests` | MCP Parser Failures and Limits | |
| 72 | |
| 73 | ## Related ADRs |
| 74 | |
| 75 | | ADR | Role | |
| 76 | |-----|------| |
| 77 | | [0081](0081-parametric-intent-melodies-editor-line-ranges.md) | Semantics of parametrics by strings; 0111 - VO LR/LN in IDE domain | |
| 78 | | [0109](0109-declarative-parametric-melody-catalog-toml-and-code-binders.md) | Directory, `tail_signature`; `:ln` - directory metadata | |
| 79 | | [0110](0110-roslyn-refactor-intent-melody-bridge.md) | Roslyn by range; future bridge to MCP arguments | |
| 80 | |
| 81 | ## Rejected alternatives |
| 82 | |
| 83 | - **Only `:ln` aliases in TOML** - do not replace API checks and self-documentation in C#. |
| 84 | - **`record` with int without invariants** - duplication of checks in each consumer. |
| 85 | - **Complete transition of commands to typed DTO instead of JSON** - outside scope v1; the boundary remains on serialization. |
| 86 | - **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. |
| 87 | |
| 88 | ## Consequences (current) |
| 89 | |
| 90 | - New scripts **only by lines** in the parametric melody branch → preferably via LR/LN. |
| 91 | - 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. |
| 92 | |
| 93 | --- |
| 94 | |
| 95 | ## Roadmap (after v1) - Done (v2) |
| 96 | |
| 97 | 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. |
| 98 | |
| 99 | ### Priority 1 - MCP boundary → editor - **done** |
| 100 | |
| 101 | - **`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). |
| 102 | |
| 103 | ### Priority 2 - web portal - **done** |
| 104 | |
| 105 | - **`WebAiPortalChatMixInFormatter`**: `EditorRangeSnap` stores **`LineRange`**; JSON parsing normalizes boundaries through `min/max` when ints are “reversed” in the wire response. |
| 106 | |
| 107 | ### Priority 3 - parametric args - **done** |
| 108 | |
| 109 | - **`ParametricLineRangeArgsBuilder`**: columns via **`ColumnNumber.TryCreate`**, path via **`EditorDocumentPath`**. |
| 110 | |
| 111 | ### Priority 4 - Roslyn - **done** |
| 112 | |
| 113 | - **`RoslynLinePositionMapper`**: `LinePosition` (0-based) → `(LineNumber, ColumnNumber)`; **`ContextMinimizer`**, **`WorkspaceDiagnosticsCoordinator`**. |
| 114 | |
| 115 | ### File path - **done** |
| 116 | |
| 117 | - **`EditorDocumentPath`** over **`CanonicalFilePath.TryNormalize`**. |
| 118 | |
| 119 | ### Readiness criterion |
| 120 | |
| 121 | - Two call sites with one set of JSON fields (`Select` + `ApplyEdit`) → **`EditorTextSpan`**; tests in **`EditorMcpSpansTests`**. |