ADR 0111: LineNumber и LineRange как доменные типы редактора
Связанные ADR
| ADR |
Роль |
| 0081 |
Семантика параметрики по строкам; 0111 — VO LR/LN в домене IDE |
| 0109 |
Каталог, tail_signature; :ln — метаданные каталога |
| 0110 |
Roslyn по диапазону; будущий мост к аргументам MCP |
Резюме
- Value objects
LineNumber / LineRange (1-based, Start ≤ End).
- Граница к JSON/командам —
int в args; ParsedLineRange для парсинга.
Отклонённые альтернативы
- Только алиасы
:ln в TOML — не заменяют проверки и самодокументацию API в C#.
record с int без инвариантов — дублирование проверок в каждом потребителе.
- Полный переход команд на typed DTO вместо JSON — вне scope v1; граница остаётся на сериализации.
- Спец-токены «до конца файла» (
7:end, 7:* и т.п.) — не входят в v1: отдельная мини-грамматика, взаимодействие с длиной файла на parse vs build, риск коллизий; оставлено на отдельное решение при явной потребности.
Последствия (текущие)
- Новые сценарии только по строкам в ветке parametric melody → предпочтительно через LR/LN.
- Проверка «строки есть в файле» остаётся в
ParametricLineRangeArgsBuilder, не в порядке двух чисел во вводе и не в LN/LR как таковых.
Решение
- Ввести value objects в пространстве имён
CascadeIDE.Models.Editor:
- LN (
LineNumber) — 1-based номер строки; инвариант Value >= LineNumber.MinimumOneBasedInclusive (1); создание через TryCreate(int, out LineNumber); операторы сравнения (в т.ч. для CA1036 / IComparable).
- LR (
LineRange) — пара Start / End типа LN с инвариантом Start <= End (инклюзивный диапазон в терминах редактора и IDE-команд); создание через TryCreate(LineNumber, LineNumber, out LineRange).
ParametricIntentMelody.ParsedLineRange хранит диапазон как LineRange Lines, а не два «голых» int.
- Парсер хвоста melody (
TryExtractLineRangeFromRemainder в ParametricIntentMelody):
- Одно целое без второго слота — одна строка (
<line> по смыслу совпадает с <line>:<line> как LR).
- Два целых — границы одного инклюзивного диапазона; порядок ввода не важен: после разбора применяется
min..max (например 7:3 и 3:7 → один LR).
- У
LineRange.TryCreate для явного кода контракт по-прежнему «второй аргумент не раньше первого»; «перевёрнутый» ввод обрабатывается до вызова TryCreate, за счёт нормализации в парсере.
- На границе к JSON (
ParametricLineRangeArgsBuilder) в анонимные DTO по-прежнему уходят int через .Value у LN — wire-формат IdeCommands.Select / IdeCommands.ApplyEdit не меняется.
IntentMelodyTailSemantics.MinEditorLineNumber согласован с LineNumber.MinimumOneBasedInclusive (одна константа-источник для «минимальной строки 1-based»).
Реализация v1 (источники в коде)
| Артефакт |
Назначение |
Models/Editor/LineNumber.cs |
LN, константа минимума, TryCreate, равенство и сравнение |
Models/Editor/LineRange.cs |
LR, TryCreate при End >= Start |
Services/ParametricIntentMelody.cs |
ParsedLineRange, TryParseLineRangeTail, TryExtractLineRangeFromRemainder, делегирование в ParametricLineRangeArgsBuilder |
Services/ParametricLineRangeArgsBuilder.cs |
Сборка JSON-args из ParsedLineRange.Lines + проверка вылета за длину файла |
Services/IntentMelodyTailSemantics.cs |
MinEditorLineNumber → ссылка на минимум LN |
CascadeIDE.Tests/EditorLineNumberRangeTests.cs |
Юнит-тесты LN/LR |
CascadeIDE.Tests/ParametricIntentMelodyTests.cs |
Парсинг, одна строка, min..max, сборка args |
IntentMelody/intent-melody-aliases.toml и копия в publish-gh-release/IntentMelody/ |
Подсказки палитры для els / eld (диапазон и одна строка) |
Каталог по-прежнему описывает два числовых слота в tail_signature (<start:ln>:<end:ln>); сокращение «одно число» — соглашение парсера приложения, не отдельная строка каталога.
Реализация v2 (дорожная карта после v1 — закрыта в коде)
| Артефакт |
Назначение |
Models/Editor/ColumnNumber.cs |
CN — 1-based колонка (TryCreate, сравнение) |
Models/Editor/EditorDocumentPath.cs |
Обёртка пути документа: CanonicalFilePath.TryNormalize, сравнение без регистра |
Models/Editor/EditorMcpSpans.cs |
EditorTextSpan.TryParse, EditorContentLineRangeMcpArgs.TryParse, EditorGoToPositionMcpArgs.TryParse |
Services/RoslynLinePositionMapper.cs |
Microsoft.CodeAnalysis.Text.LinePosition (0-based) → (LineNumber, ColumnNumber) для UI/MCP |
Services/ContextMinimizer.cs, Services/WorkspaceDiagnosticsCoordinator.cs |
Используют маппер вместо дублирования +1 |
ViewModels/IdeMcpCommandExecutor.Handlers.Editor/*.cs |
Select / ApplyEdit / GoToPosition / GetEditorContentRange через общий разбор VO |
Services/ParametricLineRangeArgsBuilder.cs |
Границы колонок через ColumnNumber + канонический EditorDocumentPath |
Features/WebAiPortal/Application/WebAiPortalChatMixInFormatter.cs |
Снимок диапазона редактора: LineRange вместо «голых» int строк |
CascadeIDE.Tests/EditorMcpSpansTests.cs, расширение EditorLineNumberRangeTests |
Отказы и границы парсера MCP |
Связанные ADR
| ADR |
Роль |
| 0081 |
Семантика параметрики по строкам; 0111 — VO LR/LN в домене IDE |
| 0109 |
Каталог, tail_signature; :ln — метаданные каталога |
| 0110 |
Roslyn по диапазону; будущий мост к аргументам MCP |
Отклонённые альтернативы
- Только алиасы
:ln в TOML — не заменяют проверки и самодокументацию API в C#.
record с int без инвариантов — дублирование проверок в каждом потребителе.
- Полный переход команд на typed DTO вместо JSON — вне scope v1; граница остаётся на сериализации.
- Спец-токены «до конца файла» (
7:end, 7:* и т.п.) — не входят в v1: отдельная мини-грамматика, взаимодействие с длиной файла на parse vs build, риск коллизий; оставлено на отдельное решение при явной потребности.
Последствия (текущие)
- Новые сценарии только по строкам в ветке parametric melody → предпочтительно через LR/LN.
- Проверка «строки есть в файле» остаётся в
ParametricLineRangeArgsBuilder, не в порядке двух чисел во вводе и не в LN/LR как таковых.
Дорожная карта (после v1) — выполнено (v2)
Цель итераций v2 — тот же паттерн: инварианты в типах до границы JSON/MCP, без смены wire-контрактов IdeCommands.
Приоритет 1 — граница MCP → редактор — сделано
EditorTextSpan + ColumnNumber, TryParse из IReadOnlyDictionary<string, JsonElement> для Select / ApplyEdit; EditorGoToPositionMcpArgs для go_to_position; EditorContentLineRangeMcpArgs для get_editor_content_range (при отсутствии ключей — 1..1 как раньше; инвертированный явный диапазон — отказ с сообщением).
Приоритет 2 — веб-портал — сделано
WebAiPortalChatMixInFormatter: EditorRangeSnap хранит LineRange; разбор JSON нормализует границы через min/max при «перевёрнутых» int в wire-ответе.
Приоритет 3 — parametric args — сделано
ParametricLineRangeArgsBuilder: колонки через ColumnNumber.TryCreate, путь через EditorDocumentPath.
Приоритет 4 — Roslyn — сделано
RoslynLinePositionMapper: LinePosition (0-based) → (LineNumber, ColumnNumber); ContextMinimizer, WorkspaceDiagnosticsCoordinator.
Путь к файлу — сделано
EditorDocumentPath поверх CanonicalFilePath.TryNormalize.
Критерий готовности
- Два call site с одним набором полей JSON (
Select + ApplyEdit) → EditorTextSpan; тесты в EditorMcpSpansTests.