fix: retain semantic colors across editable diff updates
This commit is contained in:
@@ -33,7 +33,7 @@ Frontends drive `EditorSession::handle` or `execute`, periodically call `poll()`
|
|||||||
|
|
||||||
`Editor::should_quit()` tells the frontend to exit its input/render loop for either quit or restart; `Editor::should_restart()` distinguishes a requested executable relaunch from an ordinary exit. Normal-mode `core.restart` / `:restart` first closes clean buffers across all tabs, panes, and historical views. Remaining dirty buffers retain unsaved text and undo/redo history, block the exit/relaunch request with a warning, and leave the editor interactive; already closed clean buffers remain closed. `core.force_restart` / `:force_restart` (alias `:restart!`) requests relaunch without saving, discarding unsaved text. Both commands take no arguments and have no default binding. Force restart uses palette confirmation, while configured action bindings execute directly.
|
`Editor::should_quit()` tells the frontend to exit its input/render loop for either quit or restart; `Editor::should_restart()` distinguishes a requested executable relaunch from an ordinary exit. Normal-mode `core.restart` / `:restart` first closes clean buffers across all tabs, panes, and historical views. Remaining dirty buffers retain unsaved text and undo/redo history, block the exit/relaunch request with a warning, and leave the editor interactive; already closed clean buffers remain closed. `core.force_restart` / `:force_restart` (alias `:restart!`) requests relaunch without saving, discarding unsaved text. Both commands take no arguments and have no default binding. Force restart uses palette confirmation, while configured action bindings execute directly.
|
||||||
|
|
||||||
Session polling schedules synchronous `Editor::refresh_files()` metadata scans every 250 ms; the editor method itself is unthrottled and reports whether a redraw is needed. Scans include active and parked live buffers across inactive tabs/panes, but never immutable historical snapshots, and read content only after metadata changes. Valid changed disk text replaces only clean buffers, updates the saved baseline and text revision, clears stale undo/redo, preserves/clamps every associated view, invalidates annotations and stale requests, and refreshes live comparisons. Dirty buffers keep edits; missing, unreadable, or invalid changed files preserve text and report a warning.
|
Session polling schedules synchronous `Editor::refresh_files()` metadata scans every 250 ms; the editor method itself is unthrottled and reports whether a redraw is needed. Scans include active and parked live buffers across inactive tabs/panes, but never immutable historical snapshots, and read content only after metadata changes. Valid changed disk text replaces only clean buffers, updates the saved baseline and text revision, clears stale undo/redo, preserves/clamps every associated view, clears diagnostics and stale requests, remaps retained semantic colors, and refreshes live comparisons. Dirty buffers keep edits; missing, unreadable, or invalid changed files preserve text and report a warning.
|
||||||
|
|
||||||
Every save independently re-reads disk content and compares its LF-normalized text with the saved baseline, rather than trusting polling or modification timestamps. Normal writes refuse external changes/deletion; newly opened nonexistent paths can be created normally. Typed `core.force_save` / `core.force_save_and_quit` expose canonical `:force_save [path]` / `:force_save_and_quit [path]` and aliases `:w!` / `:wq!`. Force bypasses only the external-change check, retaining snapshot read-only, target-symlink, and another-buffer path-ownership protections. Both save-and-quit variants quit only after a successful save and only when all buffers are clean.
|
Every save independently re-reads disk content and compares its LF-normalized text with the saved baseline, rather than trusting polling or modification timestamps. Normal writes refuse external changes/deletion; newly opened nonexistent paths can be created normally. Typed `core.force_save` / `core.force_save_and_quit` expose canonical `:force_save [path]` / `:force_save_and_quit [path]` and aliases `:w!` / `:wq!`. Force bypasses only the external-change check, retaining snapshot read-only, target-symlink, and another-buffer path-ownership protections. Both save-and-quit variants quit only after a successful save and only when all buffers are clean.
|
||||||
Frontends inspect `selections()` for document-ordered, end-exclusive Visual ranges of grapheme coordinates (empty outside Visual), `carets()` for all active cursors in document order, `cursor()` for the primary, and `selection_count()` for the total target count. `visual_linewise()` is true only for the whole-row shape in `Mode::Visual`; it changes neither binding modes nor Ruby mode names. Linewise ranges expand to complete rows. A range ending at the next row's column 0 includes the previous row's newline; an empty final-row range contains no text. `linewise_rows()` exposes inclusive selected row ranges only in linewise Visual, including an empty final row without changing `selections()` cardinality. The TUI highlights the copied text on those rows and one cell for each copied newline, never unused viewport space or the gutter, and labels their mode VISUAL LINE. Selection normalization, clipboard shape and contents, edit planning, and history remain inside the editor module.
|
Frontends inspect `selections()` for document-ordered, end-exclusive Visual ranges of grapheme coordinates (empty outside Visual), `carets()` for all active cursors in document order, `cursor()` for the primary, and `selection_count()` for the total target count. `visual_linewise()` is true only for the whole-row shape in `Mode::Visual`; it changes neither binding modes nor Ruby mode names. Linewise ranges expand to complete rows. A range ending at the next row's column 0 includes the previous row's newline; an empty final-row range contains no text. `linewise_rows()` exposes inclusive selected row ranges only in linewise Visual, including an empty final row without changing `selections()` cardinality. The TUI highlights the copied text on those rows and one cell for each copied newline, never unused viewport space or the gutter, and labels their mode VISUAL LINE. Selection normalization, clipboard shape and contents, edit planning, and history remain inside the editor module.
|
||||||
@@ -55,7 +55,7 @@ Current-line blame is enabled by default. Session loads whole-file incremental a
|
|||||||
|
|
||||||
Frontends inspect `file_picker()` for query, presentation, view, explorer-local INSERT/filter subfocus, selection, and errors, and iterate borrowed `FileRow` metadata (actual path, display label, directory flag, visible depth, expansion, optional repository status and old path). Dock side is a logical preference. They allocate rectangles, add folder suffixes/colors and sanitized rename labels, and render; hierarchy, filtering, parent/child navigation, focus transitions, and selected-file opening remain backend-owned.
|
Frontends inspect `file_picker()` for query, presentation, view, explorer-local INSERT/filter subfocus, selection, and errors, and iterate borrowed `FileRow` metadata (actual path, display label, directory flag, visible depth, expansion, optional repository status and old path). Dock side is a logical preference. They allocate rectangles, add folder suffixes/colors and sanitized rename labels, and render; hierarchy, filtering, parent/child navigation, focus transitions, and selected-file opening remain backend-owned.
|
||||||
Frontends inspect `diagnostics()` and `diagnostic_at_cursor()` for validated active-buffer annotations. Diagnostics are owned by core LSP or a plugin and buffer revision; text changes clear them. Native server retirement removes only that server's contribution, preserving sibling diagnostics. The TUI underlines annotated text, colors affected gutter numbers by severity, and displays the primary caret's diagnostic message without replacing search/selection backgrounds.
|
Frontends inspect `diagnostics()` and `diagnostic_at_cursor()` for validated active-buffer annotations. Diagnostics are owned by core LSP or a plugin and buffer revision; text changes clear them. Native server retirement removes only that server's contribution, preserving sibling diagnostics. The TUI underlines annotated text, colors affected gutter numbers by severity, and displays the primary caret's diagnostic message without replacing search/selection backgrounds.
|
||||||
Frontends borrow `semantic_tokens()` for sorted, single-line grapheme spans from the active buffer. Immutable `Arc` slices are shared by native LSP and buffer storage; painting does not copy tokens. Text/path changes discard overlays, and native updates validate the whole applicable annotation batch before mutation. The TUI merges semantic styling into its existing glyph pass above syntax foregrounds but below diagnostic/search/selection/caret styling.
|
Frontends borrow `semantic_tokens()` for sorted, single-line grapheme spans from the active buffer. Immutable `Arc` slices are shared by native LSP and buffer storage; painting does not copy tokens. Text replacements retain tokens outside the changed range and shift following positions using whole graphemes, including newline changes. Tokens overlapping the changed range are dropped; disjoint edits conservatively invalidate the range between their first and last changes. This applies to typing, undo/redo, completion, plugin replacements, and disk reloads. Path changes discard overlays. Native updates validate the whole applicable annotation batch before mutation. The TUI merges semantic styling into its existing glyph pass above syntax foregrounds but below diagnostic/search/selection/caret styling.
|
||||||
Frontends inspect `hover()` for optional read-only content and logical-line/grapheme scroll offsets. Popup request eligibility, dismissal, and scrolling stay editor/session-owned; the TUI clips the panel inside the full editor area without changing the primary cursor or viewport. Its hover-local syntax cache treats content as Markdown, including named fenced-code injections, and rebuilds only when content changes. The popup shares the document syntax palette and preserves raw Markdown text and logical scroll coordinates.
|
Frontends inspect `hover()` for optional read-only content and logical-line/grapheme scroll offsets. Popup request eligibility, dismissal, and scrolling stay editor/session-owned; the TUI clips the panel inside the full editor area without changing the primary cursor or viewport. Its hover-local syntax cache treats content as Markdown, including named fenced-code injections, and rebuilds only when content changes. The popup shares the document syntax palette and preserves raw Markdown text and logical scroll coordinates.
|
||||||
Frontends borrow `completion()` for the read-only item list and selected index: zero is Original text, and other indices select `items[selected - 1]`. The editor owns the immutable original document/carets, atomic candidate transactions, cycling/dismissal, pending Insert undo, and final-only repeat recording. It changes only the primary completion target and translates secondary carets through all edits. The TUI places a windowed list near the current caret, bounded by the full editor rather than its pane, sanitizes/clips labels and detail by grapheme, and preserves hardware cursor ownership; wheel input over the list is consumed without dispatching edits.
|
Frontends borrow `completion()` for the read-only item list and selected index: zero is Original text, and other indices select `items[selected - 1]`. The editor owns the immutable original document/carets, atomic candidate transactions, cycling/dismissal, pending Insert undo, and final-only repeat recording. It changes only the primary completion target and translates secondary carets through all edits. The TUI places a windowed list near the current caret, bounded by the full editor rather than its pane, sanitizes/clips labels and detail by grapheme, and preserves hardware cursor ownership; wheel input over the list is consumed without dispatching edits.
|
||||||
Frontends borrow `snippet_selections()` for the active argument's end-exclusive grapheme ranges, including mirrors and empty tabstops. The editor caches those ranges after previews, argument edits, and navigation; rendering adds no snippet allocation. Template edits stay primary-only, translate other carets, and fold resolved replacement text and caret placement into the pending completion repeat step. Per-occurrence ancestry distinguishes nested arguments from adjacent empty stops, allowing enclosing replacements to retire only overwritten descendants.
|
Frontends borrow `snippet_selections()` for the active argument's end-exclusive grapheme ranges, including mirrors and empty tabstops. The editor caches those ranges after previews, argument edits, and navigation; rendering adds no snippet allocation. Template edits stay primary-only, translate other carets, and fold resolved replacement text and caret placement into the pending completion repeat step. Per-occurrence ancestry distinguishes nested arguments from adjacent empty stops, allowing enclosing replacements to retire only overwritten descendants.
|
||||||
@@ -119,7 +119,7 @@ Diff snapshots retain a separate immutable document identity, full source text,
|
|||||||
|
|
||||||
Language configurations can own an ordered server stack. The first matching language wins extension overlap; all its matching servers receive document events, while the first handles hover/definition/completion. The Python preset uses ty then Ruff. LSP merges each worker's latest diagnostic snapshot in assignment order before emitting the unchanged core-owned update. Empty publications/retirement remove only their source; revision/path/root changes and close discard all contributions. Worker snapshots retain identity so queued pre-rename diagnostics cannot attach to a new path with the same text revision. Combined diagnostic message bytes remain bounded to 4 MiB; an overflowing publication retires its worker instead of discarding healthy sibling annotations. Workers are stored in config/root order, avoiding per-poll key cloning/sorting. Shutdown flushes the exit notification and waits for peer termination within the existing bounded grace period before process cleanup.
|
Language configurations can own an ordered server stack. The first matching language wins extension overlap; all its matching servers receive document events, while the first handles hover/definition/completion. The Python preset uses ty then Ruff. LSP merges each worker's latest diagnostic snapshot in assignment order before emitting the unchanged core-owned update. Empty publications/retirement remove only their source; revision/path/root changes and close discard all contributions. Worker snapshots retain identity so queued pre-rename diagnostics cannot attach to a new path with the same text revision. Combined diagnostic message bytes remain bounded to 4 MiB; an overflowing publication retires its worker instead of discarding healthy sibling annotations. Workers are stored in config/root order, avoiding per-poll key cloning/sorting. Shutdown flushes the exit notification and waits for peer termination within the existing bounded grace period before process cleanup.
|
||||||
|
|
||||||
Semantic tokens belong only to the first assigned server. Initialization advertises standard token types/modifiers, relative encoding, full/range requests, and server refresh support; deltas, overlapping/multiline tokens, and dynamic registration are not advertised. Each buffer has at most one request in flight; changes/refreshes coalesce into the latest snapshot. Full requests are preferred, with whole-document range fallback. A linear UTF-16 decoder validates raw coordinates and legend indices before outward grapheme rounding; overlong ends truncate at EOL, unknown types and invisible spans fall back to syntax. Both wire frames and decoded spans are bounded to 4 MiB. Snapshot identity rejects stale/renamed/closed replies before decoding. Invalid semantic replies clear the overlay and report an error without retiring diagnostics/navigation; primary retirement clears its overlays, and sibling retirement leaves them intact.
|
Semantic tokens belong only to the first assigned server. Initialization advertises standard token types/modifiers, relative encoding, full/range requests, and server refresh support; deltas, overlapping/multiline tokens, and dynamic registration are not advertised. Each buffer has at most one request in flight; changes/refreshes coalesce into the latest snapshot. Text-change notifications discard cached server coordinates without publishing an empty overlay over the editor's remapped colors. Full requests are preferred, with whole-document range fallback. A linear UTF-16 decoder validates raw coordinates and legend indices before outward grapheme rounding; overlong ends truncate at EOL, unknown types and invisible spans fall back to syntax. Both wire frames and decoded spans are bounded to 4 MiB. Snapshot identity rejects stale/renamed/closed replies before decoding. Invalid semantic replies clear the overlay and report an error without retiring diagnostics/navigation; primary retirement clears its overlays, and sibling retirement leaves them intact.
|
||||||
|
|
||||||
Manual completion dispatches the Insert-only `core.lsp.completion` action directly to the session, without the Ex execution path's mode/history transitions. The native worker sends the primary caret as UTF-16 and normalizes results into `CompletionItem { label, detail, primary_edit, edits, snippet }`; each `CompletionEdit` uses original-document, end-exclusive grapheme coordinates. Snippet primary text is expanded and newline-normalized before byte-relative metadata is assigned; additional edits remain plain text. Primary and additional edits form one atomic candidate. Session freshness includes the latest request, buffer/revision, primary caret, Insert source mode, and absence of modal overlays; any intervening input invalidates a pending reply. Decoding/validation errors remain completion-local and preserve healthy diagnostics/semantic tokens. Server commands, indentation adjustment, resolve requests, and automatic triggers are not supported; both frames and aggregate decoded completion text remain bounded.
|
Manual completion dispatches the Insert-only `core.lsp.completion` action directly to the session, without the Ex execution path's mode/history transitions. The native worker sends the primary caret as UTF-16 and normalizes results into `CompletionItem { label, detail, primary_edit, edits, snippet }`; each `CompletionEdit` uses original-document, end-exclusive grapheme coordinates. Snippet primary text is expanded and newline-normalized before byte-relative metadata is assigned; additional edits remain plain text. Primary and additional edits form one atomic candidate. Session freshness includes the latest request, buffer/revision, primary caret, Insert source mode, and absence of modal overlays; any intervening input invalidates a pending reply. Decoding/validation errors remain completion-local and preserve healthy diagnostics/semantic tokens. Server commands, indentation adjustment, resolve requests, and automatic triggers are not supported; both frames and aggregate decoded completion text remain bounded.
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -50,7 +50,7 @@ Rust tests live alongside their modules; native LSP integration checks are in [`
|
|||||||
- Queued-navigation checks cover counted ordered motions through short rows, Normal/Visual ranges, remapped press/repeat keys, rendered caret visibility, edit/layout/prefix and non-key barriers, and the 64-key fairness bound. Burst smoke must verify the final cursor position as well as frame count; batching reduces queued redraws without increasing the OS/terminal repeat rate.
|
- Queued-navigation checks cover counted ordered motions through short rows, Normal/Visual ranges, remapped press/repeat keys, rendered caret visibility, edit/layout/prefix and non-key barriers, and the 64-key fairness bound. Burst smoke must verify the final cursor position as well as frame count; batching reduces queued redraws without increasing the OS/terminal repeat rate.
|
||||||
- Native LSP integration checks launch framed-protocol fixture subprocesses, covering initialization/settings, opt-in startup, synchronization, UTF-16/file-URI conversion, dirty targets, stale requests/diagnostics, and transport failures. Stack checks cover independent initialization, diagnostic merging/clearing, sibling failure isolation in active/parked buffers, same-revision rename/close identity, aggregate limits, first-server position routing, and both shutdowns. Semantic ordering uses protocol/file gates; elapsed deadlines only bound stuck subprocesses.
|
- Native LSP integration checks launch framed-protocol fixture subprocesses, covering initialization/settings, opt-in startup, synchronization, UTF-16/file-URI conversion, dirty targets, stale requests/diagnostics, and transport failures. Stack checks cover independent initialization, diagnostic merging/clearing, sibling failure isolation in active/parked buffers, same-revision rename/close identity, aggregate limits, first-server position routing, and both shutdowns. Semantic ordering uses protocol/file gates; elapsed deadlines only bound stuck subprocesses.
|
||||||
- Hover regression checks cover content forms, sanitization, cancelled/superseded replies, invalid results, scrolling/dismissal, Unicode clipping, and preserved editing state. Rendered checks cover longer/wider documentation visibility, Markdown heading and fenced-code colors, scrolled color coordinates, and replacement content without stale highlighting.
|
- Hover regression checks cover content forms, sanitization, cancelled/superseded replies, invalid results, scrolling/dismissal, Unicode clipping, and preserved editing state. Rendered checks cover longer/wider documentation visibility, Markdown heading and fenced-code colors, scrolled color coordinates, and replacement content without stale highlighting.
|
||||||
- Semantic-token checks cover full/range requests, legend/modifier remapping, UTF-16 boundaries and grapheme rounding, ASCII tab and Unicode column bounds, refresh/change coalescing, stale rename/close replies, null/empty/error fallback, primary/sibling retirement, and rendered cell precedence with syntax, diagnostics, search, selections, and carets.
|
- Semantic-token checks cover full/range requests, legend/modifier remapping, UTF-16 boundaries and grapheme rounding, ASCII tab and Unicode column bounds, refresh/change coalescing, stale rename/close replies, null/empty/error fallback, primary/sibling retirement, and rendered cell precedence with syntax, diagnostics, search, selections, and carets. Retention checks cover typing/undo/redo in editable diffs, column/newline/Unicode remapping, overlapping-token removal, and session polling with delayed/stale server replies. Live PTY smoke holds semantic replies while typing and undoing/redoing, checking that untouched colored text is not repainted in the fallback color.
|
||||||
|
|
||||||
Clipboard checks use isolated providers rather than reading or overwriting the desktop clipboard.
|
Clipboard checks use isolated providers rather than reading or overwriting the desktop clipboard.
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -469,7 +469,7 @@ Missing counterparts use adjacent muted `╱` diagonals (U+2571, box drawings li
|
|||||||
|
|
||||||
In Normal mode, Tab/Shift+Tab cycles focus between Files and the visible text panes, skipping absent panes. Switching Before/After synchronizes the primary cursor by aligned diff row and display column. A missing counterpart uses the nearest visible source row; shorter lines clamp the column while retaining the preferred display column for the return trip. Vertical wheel input scrolls the visible panes without moving the focused text caret; switching sides by vertical wheel also synchronizes the cursor. Over Files it navigates visible tree rows without opening them. Horizontal wheel input in either split synchronizes the Before/After horizontal offsets, bounded by the longer side, without changing focus, cursor, or selections; over Files it is consumed. Added/deleted files retain single-pane scrolling. While a comparison is open, Escape cancels modes, prompts, or filtering, or returns picker focus to the last bound text pane; it leaves the comparison open. Use **`:diff.close`**, find `diff.close` in the palette, or bind **`config.core.diff.close`** to leave the comparison explicitly. The dotted command is intentional; there is no `:diffclose` alias.
|
In Normal mode, Tab/Shift+Tab cycles focus between Files and the visible text panes, skipping absent panes. Switching Before/After synchronizes the primary cursor by aligned diff row and display column. A missing counterpart uses the nearest visible source row; shorter lines clamp the column while retaining the preferred display column for the return trip. Vertical wheel input scrolls the visible panes without moving the focused text caret; switching sides by vertical wheel also synchronizes the cursor. Over Files it navigates visible tree rows without opening them. Horizontal wheel input in either split synchronizes the Before/After horizontal offsets, bounded by the longer side, without changing focus, cursor, or selections; over Files it is consumed. Added/deleted files retain single-pane scrolling. While a comparison is open, Escape cancels modes, prompts, or filtering, or returns picker focus to the last bound text pane; it leaves the comparison open. Use **`:diff.close`**, find `diff.close` in the palette, or bind **`config.core.diff.close`** to leave the comparison explicitly. The dotted command is intentional; there is no `:diffclose` alias.
|
||||||
|
|
||||||
Both diff panes apply available semantic tokens from their own buffer, independently of keyboard focus. Historical snapshots never borrow the working buffer's tokens; live After tokens must match the displayed text revision. Missing or invalidated tokens leave Tree-sitter syntax colors intact. Semantic colors and modifiers preserve the change backgrounds.
|
Both diff panes apply available semantic tokens from their own buffer, independently of keyboard focus. Historical snapshots never borrow the working buffer's tokens. Typing and undo/redo retain semantic colors outside the edited range, shifting their positions to follow the text until a current LSP reply replaces them. Edited ranges fall back to Tree-sitter syntax colors while waiting; semantic roles elsewhere can briefly lag changes. Semantic colors and modifiers preserve the change backgrounds.
|
||||||
|
|
||||||
Binary data, missing final newlines, permission/type changes, symlink targets, and submodule pointers have explicit notices. Symlinks are not followed and submodule contents are not traversed. jj conflicts use jj's file-content projection and carry conflict notices; ordinary files remain readable. Identical-content moves are shown as renames; modified moves appear as an added and removed file.
|
Binary data, missing final newlines, permission/type changes, symlink targets, and submodule pointers have explicit notices. Symlinks are not followed and submodule contents are not traversed. jj conflicts use jj's file-content projection and carry conflict notices; ordinary files remain readable. Identical-content moves are shown as renames; modified moves appear as an added and removed file.
|
||||||
|
|
||||||
@@ -550,4 +550,4 @@ Colors retain the terminal palette: comments dark gray, strings green, keywords
|
|||||||
|
|
||||||
Markdown heading markers (`#` through `######`, or Setext underlines) share the heading text's markup color. List markers (`-`, `+`, `*`, and ordered prefixes such as `1.` or `1)`) also use cyan markup, including nested and blockquoted lists. Plain-text hashes and other punctuation keep their existing styling.
|
Markdown heading markers (`#` through `######`, or Setext underlines) share the heading text's markup color. List markers (`-`, `+`, `*`, and ordered prefixes such as `1.` or `1)`) also use cyan markup, including nested and blockquoted lists. Plain-text hashes and other punctuation keep their existing styling.
|
||||||
|
|
||||||
An enabled supporting LSP server supplies semantic tokens automatically, overriding syntax colors only on recognized spans. Semantic colors also use the terminal palette: namespaces neutral gray, types cyan, variables/parameters light cyan, and properties blue. Readonly variables/parameters/properties keep those role colors and add italics; declarations/definitions are bold, documentation comments italic, and deprecated symbols struck through. Italic emphasis depends on terminal support. Tree-sitter fills gaps and remains available while new tokens are pending after edits. Search, Visual selections, diagnostics, and carets keep their existing precedence. See [native LSP configuration](configuration.md#native-lsp) for provider support and server-stack routing.
|
An enabled supporting LSP server supplies semantic tokens automatically, overriding syntax colors only on recognized spans. Semantic colors also use the terminal palette: namespaces neutral gray, types cyan, variables/parameters light cyan, and properties blue. Readonly variables/parameters/properties keep those role colors and add italics; declarations/definitions are bold, documentation comments italic, and deprecated symbols struck through. Italic emphasis depends on terminal support. Edits retain semantic colors outside the changed range and shift them with the text until updated tokens arrive, avoiding whole-buffer flashes. Tree-sitter fills gaps, including edited ranges whose semantic tokens were discarded. Search, Visual selections, diagnostics, and carets keep their existing precedence. See [native LSP configuration](configuration.md#native-lsp) for provider support and server-stack routing.
|
||||||
|
|||||||
+236
-38
@@ -1861,11 +1861,10 @@ impl Buffer {
|
|||||||
{
|
{
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
self.lines = split_lines(&text);
|
self.replace_lines(split_lines(&text));
|
||||||
self.saved_lines.clone_from(&self.lines);
|
self.saved_lines.clone_from(&self.lines);
|
||||||
self.undo.clear();
|
self.undo.clear();
|
||||||
self.redo.clear();
|
self.redo.clear();
|
||||||
self.text_changed();
|
|
||||||
self.view.reload(&self.lines);
|
self.view.reload(&self.lines);
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
@@ -1893,10 +1892,17 @@ impl Buffer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn text_changed(&mut self) {
|
/// Keep unaffected semantic colors until the server supplies this revision's tokens.
|
||||||
self.text_revision = self.text_revision.wrapping_add(1);
|
fn replace_lines(&mut self, lines: Vec<String>) -> Vec<String> {
|
||||||
self.diagnostics.clear();
|
if self.lines != lines {
|
||||||
self.semantic_tokens = None;
|
if let Some(tokens) = &mut self.semantic_tokens {
|
||||||
|
retain_semantic_tokens(tokens, &self.lines, &lines);
|
||||||
|
}
|
||||||
|
self.text_revision = self.text_revision.wrapping_add(1);
|
||||||
|
self.diagnostics.clear();
|
||||||
|
self.view.search_match = None;
|
||||||
|
}
|
||||||
|
std::mem::replace(&mut self.lines, lines)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2455,8 +2461,9 @@ impl Editor {
|
|||||||
};
|
};
|
||||||
let seed = split_lines(projection.seed());
|
let seed = split_lines(projection.seed());
|
||||||
if buffer.lines != seed {
|
if buffer.lines != seed {
|
||||||
|
let previous = buffer.replace_lines(seed);
|
||||||
buffer.undo.push(Snapshot {
|
buffer.undo.push(Snapshot {
|
||||||
lines: std::mem::replace(&mut buffer.lines, seed),
|
lines: previous,
|
||||||
targets: if buffer.view.targets.is_empty() {
|
targets: if buffer.view.targets.is_empty() {
|
||||||
vec![Target::caret(Cursor::default())]
|
vec![Target::caret(Cursor::default())]
|
||||||
} else {
|
} else {
|
||||||
@@ -2465,7 +2472,6 @@ impl Editor {
|
|||||||
primary: buffer.view.primary,
|
primary: buffer.view.primary,
|
||||||
});
|
});
|
||||||
buffer.redo.clear();
|
buffer.redo.clear();
|
||||||
buffer.text_changed();
|
|
||||||
buffer.view.clamp(&buffer.lines);
|
buffer.view.clamp(&buffer.lines);
|
||||||
for pane in self.panes.values_mut() {
|
for pane in self.panes.values_mut() {
|
||||||
if let Some(view) = pane.views.get_mut(&draft_buffer) {
|
if let Some(view) = pane.views.get_mut(&draft_buffer) {
|
||||||
@@ -2526,8 +2532,7 @@ impl Editor {
|
|||||||
} {
|
} {
|
||||||
if let Some(saved) = saved_text {
|
if let Some(saved) = saved_text {
|
||||||
if !was_text && !buffer.dirty() {
|
if !was_text && !buffer.dirty() {
|
||||||
buffer.lines.clone_from(&saved);
|
buffer.replace_lines(saved.clone());
|
||||||
buffer.text_changed();
|
|
||||||
buffer.view.clamp(&buffer.lines);
|
buffer.view.clamp(&buffer.lines);
|
||||||
}
|
}
|
||||||
buffer.saved_lines = saved;
|
buffer.saved_lines = saved;
|
||||||
@@ -2535,12 +2540,11 @@ impl Editor {
|
|||||||
buffer.path = Some(target_path);
|
buffer.path = Some(target_path);
|
||||||
buffer.resolver_readonly = false;
|
buffer.resolver_readonly = false;
|
||||||
} else if !buffer.dirty() {
|
} else if !buffer.dirty() {
|
||||||
buffer.lines = vec!["Non-text resolution saved; reopen the file to inspect its current type.".into()];
|
buffer.replace_lines(vec!["Non-text resolution saved; reopen the file to inspect its current type.".into()]);
|
||||||
buffer.saved_lines.clone_from(&buffer.lines);
|
buffer.saved_lines.clone_from(&buffer.lines);
|
||||||
buffer.path = None;
|
buffer.path = None;
|
||||||
buffer.identity = None;
|
buffer.identity = None;
|
||||||
buffer.resolver_readonly = true;
|
buffer.resolver_readonly = true;
|
||||||
buffer.text_changed();
|
|
||||||
buffer.view.clamp(&buffer.lines);
|
buffer.view.clamp(&buffer.lines);
|
||||||
}
|
}
|
||||||
buffer.disk_stamp = buffer
|
buffer.disk_stamp = buffer
|
||||||
@@ -2832,9 +2836,8 @@ impl Editor {
|
|||||||
self.parked.get_mut(&alternative_buffer).unwrap()
|
self.parked.get_mut(&alternative_buffer).unwrap()
|
||||||
};
|
};
|
||||||
if buffer.lines != alternative {
|
if buffer.lines != alternative {
|
||||||
buffer.lines.clone_from(&alternative);
|
buffer.replace_lines(alternative.clone());
|
||||||
buffer.saved_lines = alternative;
|
buffer.saved_lines = alternative;
|
||||||
buffer.text_changed();
|
|
||||||
buffer.view.clamp(&buffer.lines);
|
buffer.view.clamp(&buffer.lines);
|
||||||
for pane in self.panes.values_mut() {
|
for pane in self.panes.values_mut() {
|
||||||
if let Some(view) = pane.views.get_mut(&alternative_buffer) {
|
if let Some(view) = pane.views.get_mut(&alternative_buffer) {
|
||||||
@@ -5913,10 +5916,9 @@ impl Editor {
|
|||||||
if state.popup.selected == 0 {
|
if state.popup.selected == 0 {
|
||||||
self.snippet = None;
|
self.snippet = None;
|
||||||
if self.active.lines != state.original.lines {
|
if self.active.lines != state.original.lines {
|
||||||
self.active.text_changed();
|
self.active.replace_lines(state.original.lines.clone());
|
||||||
self.active.view.search_match = None;
|
self.active.view.search_match = None;
|
||||||
}
|
}
|
||||||
self.active.lines.clone_from(&state.original.lines);
|
|
||||||
self.clamp_other_views();
|
self.clamp_other_views();
|
||||||
self.active.view.targets.clone_from(&state.original.targets);
|
self.active.view.targets.clone_from(&state.original.targets);
|
||||||
self.active.view.primary = state.original.primary;
|
self.active.view.primary = state.original.primary;
|
||||||
@@ -9515,11 +9517,7 @@ impl Editor {
|
|||||||
match effect {
|
match effect {
|
||||||
Effect::Insert(text) => self.insert_text(&normalize_newlines(&text)),
|
Effect::Insert(text) => self.insert_text(&normalize_newlines(&text)),
|
||||||
Effect::Replace(text) => {
|
Effect::Replace(text) => {
|
||||||
let lines = split_lines(&text);
|
self.active.replace_lines(split_lines(&text));
|
||||||
if lines != self.active.lines {
|
|
||||||
self.active.text_changed();
|
|
||||||
}
|
|
||||||
self.active.lines = lines;
|
|
||||||
self.clamp_other_views();
|
self.clamp_other_views();
|
||||||
self.active.view.search_match = None;
|
self.active.view.search_match = None;
|
||||||
self.normalize_targets();
|
self.normalize_targets();
|
||||||
@@ -10991,7 +10989,7 @@ impl Editor {
|
|||||||
let search_match = self.active.view.search_match;
|
let search_match = self.active.view.search_match;
|
||||||
let revision = self.active.text_revision;
|
let revision = self.active.text_revision;
|
||||||
let diagnostics = std::mem::take(&mut self.active.diagnostics);
|
let diagnostics = std::mem::take(&mut self.active.diagnostics);
|
||||||
let semantic_tokens = self.active.semantic_tokens.take();
|
let semantic_tokens = self.active.semantic_tokens.clone();
|
||||||
let mut restored = false;
|
let mut restored = false;
|
||||||
let result = self
|
let result = self
|
||||||
.replay_edit(&recipe)
|
.replay_edit(&recipe)
|
||||||
@@ -11288,10 +11286,7 @@ impl Editor {
|
|||||||
if self.mutation_blocked() {
|
if self.mutation_blocked() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if self.active.lines != snapshot.lines {
|
self.active.replace_lines(snapshot.lines);
|
||||||
self.active.text_changed();
|
|
||||||
}
|
|
||||||
self.active.lines = snapshot.lines;
|
|
||||||
self.clamp_other_views();
|
self.clamp_other_views();
|
||||||
self.active.view.targets = snapshot.targets;
|
self.active.view.targets = snapshot.targets;
|
||||||
self.active.view.primary = snapshot.primary;
|
self.active.view.primary = snapshot.primary;
|
||||||
@@ -11666,18 +11661,11 @@ impl Editor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !text
|
|
||||||
.split('\n')
|
|
||||||
.eq(self.active.lines.iter().map(String::as_str))
|
|
||||||
{
|
|
||||||
self.active.text_changed();
|
|
||||||
self.active.view.search_match = None;
|
|
||||||
}
|
|
||||||
if let Some(original) = original {
|
if let Some(original) = original {
|
||||||
self.active.view.targets.clone_from(&original.targets);
|
self.active.view.targets.clone_from(&original.targets);
|
||||||
self.active.view.primary = original.primary;
|
self.active.view.primary = original.primary;
|
||||||
}
|
}
|
||||||
self.active.lines = new_lines;
|
self.active.replace_lines(new_lines);
|
||||||
let offsets = new_offsets;
|
let offsets = new_offsets;
|
||||||
for (target, (byte, round_up)) in self.active.view.targets.iter_mut().zip(destinations) {
|
for (target, (byte, round_up)) in self.active.view.targets.iter_mut().zip(destinations) {
|
||||||
target.active = offset_cursor(&self.active.lines, &offsets, byte, round_up);
|
target.active = offset_cursor(&self.active.lines, &offsets, byte, round_up);
|
||||||
@@ -12055,6 +12043,102 @@ fn grapheme_cursor(lines: &[String], mut index: usize) -> Option<Cursor> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn document_graphemes(lines: &[String]) -> impl DoubleEndedIterator<Item = &str> {
|
||||||
|
lines.iter().enumerate().flat_map(move |(row, line)| {
|
||||||
|
line.graphemes(true)
|
||||||
|
.chain((row + 1 < lines.len()).then_some("\n"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn retain_semantic_tokens(tokens: &mut Arc<[SemanticToken]>, before: &[String], after: &[String]) {
|
||||||
|
// Skip whole unchanged lines before doing Unicode segmentation at the edit boundaries.
|
||||||
|
let first = before
|
||||||
|
.iter()
|
||||||
|
.zip(after)
|
||||||
|
.take_while(|(left, right)| left == right)
|
||||||
|
.count()
|
||||||
|
.min(before.len() - 1)
|
||||||
|
.min(after.len() - 1);
|
||||||
|
let suffix = before[first..]
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.zip(after[first..].iter().rev())
|
||||||
|
.take_while(|(left, right)| left == right)
|
||||||
|
.count();
|
||||||
|
// Include one common suffix line so its preceding newline remains part of the comparison.
|
||||||
|
let before = &before[first..before.len() - suffix.saturating_sub(1)];
|
||||||
|
let after = &after[first..after.len() - suffix.saturating_sub(1)];
|
||||||
|
let mut old = document_graphemes(before).peekable();
|
||||||
|
let mut new = document_graphemes(after).peekable();
|
||||||
|
let mut start = TextPosition {
|
||||||
|
row: first,
|
||||||
|
column: 0,
|
||||||
|
};
|
||||||
|
while let (Some(left), Some(right)) = (old.peek(), new.peek()) {
|
||||||
|
if left != right {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if *left == "\n" {
|
||||||
|
start.row += 1;
|
||||||
|
start.column = 0;
|
||||||
|
} else {
|
||||||
|
start.column += 1;
|
||||||
|
}
|
||||||
|
old.next();
|
||||||
|
new.next();
|
||||||
|
}
|
||||||
|
let end = |lines: &[String]| TextPosition {
|
||||||
|
row: first + lines.len() - 1,
|
||||||
|
column: grapheme_count(lines.last().unwrap()),
|
||||||
|
};
|
||||||
|
let mut old_end = end(before);
|
||||||
|
let mut new_end = end(after);
|
||||||
|
for (left, right) in old.rev().zip(new.rev()) {
|
||||||
|
if left != right {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
for (position, lines) in [(&mut old_end, before), (&mut new_end, after)] {
|
||||||
|
if left == "\n" {
|
||||||
|
position.row -= 1;
|
||||||
|
position.column = grapheme_count(&lines[position.row - first]);
|
||||||
|
} else {
|
||||||
|
position.column -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tokens
|
||||||
|
.iter()
|
||||||
|
.all(|token| token.end <= start || (token.start >= old_end && old_end == new_end))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let shift = |position: TextPosition| TextPosition {
|
||||||
|
row: new_end.row + (position.row - old_end.row),
|
||||||
|
column: if position.row == old_end.row {
|
||||||
|
new_end.column + (position.column - old_end.column)
|
||||||
|
} else {
|
||||||
|
position.column
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// ponytail: one changed range; retain between disjoint edits with per-edit mapping if needed.
|
||||||
|
*tokens = tokens
|
||||||
|
.iter()
|
||||||
|
.filter_map(|token| {
|
||||||
|
if token.end <= start {
|
||||||
|
Some(*token)
|
||||||
|
} else if token.start >= old_end {
|
||||||
|
Some(SemanticToken {
|
||||||
|
start: shift(token.start),
|
||||||
|
end: shift(token.end),
|
||||||
|
..*token
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
|
||||||
fn translate_edit_offset<'a, 'b>(
|
fn translate_edit_offset<'a, 'b>(
|
||||||
position: usize,
|
position: usize,
|
||||||
after: bool,
|
after: bool,
|
||||||
@@ -16171,8 +16255,9 @@ mod tests {
|
|||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
live.lines[1] = "dirty after".into();
|
let mut lines = live.lines.clone();
|
||||||
live.text_changed();
|
lines[1] = "dirty after".into();
|
||||||
|
live.replace_lines(lines);
|
||||||
let live_id = live.id;
|
let live_id = live.id;
|
||||||
editor.next_buffer_id += 1;
|
editor.next_buffer_id += 1;
|
||||||
editor.parked.insert(live_id, live);
|
editor.parked.insert(live_id, live);
|
||||||
@@ -17709,8 +17794,11 @@ mod tests {
|
|||||||
editor
|
editor
|
||||||
.navigate_to(b, TextPosition { row: 0, column: 0 })
|
.navigate_to(b, TextPosition { row: 0, column: 0 })
|
||||||
.unwrap();
|
.unwrap();
|
||||||
editor.parked.get_mut(&1).unwrap().lines = vec!["中👩💻".into()];
|
editor
|
||||||
editor.parked.get_mut(&1).unwrap().text_changed();
|
.parked
|
||||||
|
.get_mut(&1)
|
||||||
|
.unwrap()
|
||||||
|
.replace_lines(vec!["中👩💻".into()]);
|
||||||
editor.handle(Input::Ctrl('o'));
|
editor.handle(Input::Ctrl('o'));
|
||||||
assert_eq!(editor.cursor(), Cursor { row: 0, column: 1 });
|
assert_eq!(editor.cursor(), Cursor { row: 0, column: 1 });
|
||||||
assert!(editor.dirty());
|
assert!(editor.dirty());
|
||||||
@@ -18595,6 +18683,116 @@ mod tests {
|
|||||||
assert!(editor.semantic_tokens().is_empty());
|
assert!(editor.semantic_tokens().is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn semantic_colors_follow_untouched_text_and_drop_overlapping_edits() {
|
||||||
|
use crate::plugin::SemanticTokenKind;
|
||||||
|
let position = |(row, column)| TextPosition { row, column };
|
||||||
|
for (before, after, start, end, expected) in [
|
||||||
|
(
|
||||||
|
"a value z",
|
||||||
|
"aa value z",
|
||||||
|
(0, 2),
|
||||||
|
(0, 7),
|
||||||
|
Some(((0, 3), (0, 8))),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"a value z",
|
||||||
|
"value z",
|
||||||
|
(0, 2),
|
||||||
|
(0, 7),
|
||||||
|
Some(((0, 0), (0, 5))),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"a value z",
|
||||||
|
"a \nvalue z",
|
||||||
|
(0, 2),
|
||||||
|
(0, 7),
|
||||||
|
Some(((1, 0), (1, 5))),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"a\nvalue z",
|
||||||
|
"avalue z",
|
||||||
|
(1, 0),
|
||||||
|
(1, 5),
|
||||||
|
Some(((0, 1), (0, 6))),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"e\u{301}value",
|
||||||
|
"e\u{301}界value",
|
||||||
|
(0, 1),
|
||||||
|
(0, 6),
|
||||||
|
Some(((0, 2), (0, 7))),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"x\nvalue",
|
||||||
|
"x\n👩🚀value",
|
||||||
|
(1, 0),
|
||||||
|
(1, 5),
|
||||||
|
Some(((1, 1), (1, 6))),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"head\nvalue\ntail\nlast",
|
||||||
|
"head\n界\nvalue\ntail\nlast",
|
||||||
|
(1, 0),
|
||||||
|
(1, 5),
|
||||||
|
Some(((2, 0), (2, 5))),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"head\n界\nvalue\ntail",
|
||||||
|
"head\nvalue\ntail",
|
||||||
|
(2, 0),
|
||||||
|
(2, 5),
|
||||||
|
Some(((1, 0), (1, 5))),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"value z",
|
||||||
|
"value \nz",
|
||||||
|
(0, 0),
|
||||||
|
(0, 5),
|
||||||
|
Some(((0, 0), (0, 5))),
|
||||||
|
),
|
||||||
|
("value", "value!", (0, 0), (0, 5), Some(((0, 0), (0, 5)))),
|
||||||
|
("value", "valXue", (0, 0), (0, 5), None),
|
||||||
|
("value", "", (0, 0), (0, 5), None),
|
||||||
|
("value", "VALUE", (0, 0), (0, 5), None),
|
||||||
|
("evalue", "e\u{301}value", (0, 0), (0, 1), None),
|
||||||
|
] {
|
||||||
|
let mut editor = editor();
|
||||||
|
editor.apply(vec![Effect::Replace(before.into())]).unwrap();
|
||||||
|
let revision = editor.text_revision();
|
||||||
|
let token = SemanticToken {
|
||||||
|
start: position(start),
|
||||||
|
end: position(end),
|
||||||
|
kind: SemanticTokenKind::Function,
|
||||||
|
modifiers: 5,
|
||||||
|
};
|
||||||
|
let original: Arc<[SemanticToken]> = Arc::from([token]);
|
||||||
|
editor
|
||||||
|
.replace_semantic_tokens(editor.buffer_id(), revision, Some(original.clone()))
|
||||||
|
.unwrap();
|
||||||
|
editor.apply(vec![Effect::Replace(after.into())]).unwrap();
|
||||||
|
let expected: Vec<_> = expected
|
||||||
|
.map(|(start, end)| SemanticToken {
|
||||||
|
start: position(start),
|
||||||
|
end: position(end),
|
||||||
|
..token
|
||||||
|
})
|
||||||
|
.into_iter()
|
||||||
|
.collect();
|
||||||
|
assert_eq!(
|
||||||
|
editor.semantic_tokens(),
|
||||||
|
expected,
|
||||||
|
"{before:?} -> {after:?}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!editor
|
||||||
|
.replace_semantic_tokens(editor.buffer_id(), revision, Some(original))
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(editor.semantic_tokens(), expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn semantic_validation_rejects_malformed_live_tokens_but_ignores_stale_and_unloaded() {
|
fn semantic_validation_rejects_malformed_live_tokens_but_ignores_stale_and_unloaded() {
|
||||||
use crate::plugin::SemanticTokenKind;
|
use crate::plugin::SemanticTokenKind;
|
||||||
|
|||||||
+5
-2
@@ -238,8 +238,11 @@ impl Lsp {
|
|||||||
if !Arc::ptr_eq(&cached.snapshot, &snapshot) {
|
if !Arc::ptr_eq(&cached.snapshot, &snapshot) {
|
||||||
cached.snapshot = Arc::clone(&snapshot);
|
cached.snapshot = Arc::clone(&snapshot);
|
||||||
cached.diagnostics.clear();
|
cached.diagnostics.clear();
|
||||||
cached.semantic_tokens = None;
|
if cached.semantic_tokens.take().is_some() {
|
||||||
self.changed_semantics.insert(snapshot.id);
|
// Discard queued old coordinates, not explicit clears such as retirement.
|
||||||
|
// The editor keeps remapped colors until a server reply replaces them.
|
||||||
|
self.changed_semantics.remove(&snapshot.id);
|
||||||
|
}
|
||||||
self.changed_diagnostics.insert(snapshot.id);
|
self.changed_diagnostics.insert(snapshot.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-2
@@ -13669,6 +13669,34 @@ end
|
|||||||
}
|
}
|
||||||
editor.handle(Input::Char('\t'));
|
editor.handle(Input::Char('\t'));
|
||||||
}
|
}
|
||||||
|
if writable {
|
||||||
|
while editor.diff_buffer_focus() != Some(Focus::After)
|
||||||
|
|| editor.diff().unwrap().focus != Focus::After
|
||||||
|
{
|
||||||
|
editor.handle(Input::Char('\t'));
|
||||||
|
}
|
||||||
|
assert!(editor.diff_editing());
|
||||||
|
editor.handle(Input::Char('A'));
|
||||||
|
assert_eq!(editor.mode(), Mode::Insert);
|
||||||
|
for input in " // typing".chars().map(Input::Char).chain([
|
||||||
|
Input::Escape,
|
||||||
|
Input::Char('u'),
|
||||||
|
Input::Char('U'),
|
||||||
|
]) {
|
||||||
|
editor.handle(input.clone());
|
||||||
|
terminal
|
||||||
|
.draw(|frame| views.draw(frame, &mut editor))
|
||||||
|
.unwrap();
|
||||||
|
let regions = views.panels.diff.as_ref().unwrap().regions;
|
||||||
|
for (index, color) in [(1, Color::LightCyan), (2, Color::LightBlue)] {
|
||||||
|
let pane = regions[index].0;
|
||||||
|
let cell = &terminal.backend().buffer()[(pane.x + 8, pane.y + 1)];
|
||||||
|
assert_eq!(cell.symbol(), "v");
|
||||||
|
assert_eq!(cell.fg, color, "semantic color flashed after {input:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert_eq!(editor.lines(), ["let value = 2; // typing"]);
|
||||||
|
}
|
||||||
editor
|
editor
|
||||||
.replace_semantic_tokens(editor.buffer_id(), editor.text_revision(), None)
|
.replace_semantic_tokens(editor.buffer_id(), editor.text_revision(), None)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -15084,8 +15112,9 @@ end
|
|||||||
assert_eq!(buffer[(0, 0)].fg, Color::Reset);
|
assert_eq!(buffer[(0, 0)].fg, Color::Reset);
|
||||||
assert_eq!(editor.lines(), &[source]);
|
assert_eq!(editor.lines(), &[source]);
|
||||||
}
|
}
|
||||||
editor.handle(Input::Char('x'));
|
editor
|
||||||
assert!(editor.semantic_tokens().is_empty());
|
.replace_semantic_tokens(editor.buffer_id(), editor.text_revision(), None)
|
||||||
|
.unwrap();
|
||||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(20, 4)).unwrap();
|
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(20, 4)).unwrap();
|
||||||
let (mut top, mut left) = (0, 0);
|
let (mut top, mut left) = (0, 0);
|
||||||
terminal
|
terminal
|
||||||
|
|||||||
@@ -2357,6 +2357,50 @@ fn lsp_semantics_coalesce_changed_text_and_refresh_without_decoding_stale_payloa
|
|||||||
&& frame.get("result").is_some()));
|
&& frame.get("result").is_some()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lsp_semantic_colors_survive_edits_and_polling_until_a_current_reply() {
|
||||||
|
let fixture = Fixture::new();
|
||||||
|
let mut session = fixture.open("controlled-semantic");
|
||||||
|
until(&mut session, &fixture, |_| {
|
||||||
|
fixture.method_count("textDocument/semanticTokens/full") == 1
|
||||||
|
});
|
||||||
|
semantic_reply(
|
||||||
|
&mut session,
|
||||||
|
&fixture,
|
||||||
|
1,
|
||||||
|
json!({"result": {"data": [0, 0, 1, 4, 0]}}),
|
||||||
|
);
|
||||||
|
let original = session.editor().semantic_tokens().to_vec();
|
||||||
|
assert_eq!(original[0].kind, SemanticTokenKind::Type);
|
||||||
|
session.handle(Input::Char('A'));
|
||||||
|
session.handle(Input::Char('!'));
|
||||||
|
until(&mut session, &fixture, |_| {
|
||||||
|
fixture.method_count("textDocument/semanticTokens/full") == 2
|
||||||
|
});
|
||||||
|
assert_eq!(session.editor().semantic_tokens(), original);
|
||||||
|
session.handle(Input::Char('?'));
|
||||||
|
semantic_reply(
|
||||||
|
&mut session,
|
||||||
|
&fixture,
|
||||||
|
2,
|
||||||
|
json!({"result": {"data": ["stale malformed data"]}}),
|
||||||
|
);
|
||||||
|
until(&mut session, &fixture, |_| {
|
||||||
|
fixture.method_count("textDocument/semanticTokens/full") == 3
|
||||||
|
});
|
||||||
|
assert_eq!(session.editor().semantic_tokens(), original);
|
||||||
|
semantic_reply(
|
||||||
|
&mut session,
|
||||||
|
&fixture,
|
||||||
|
3,
|
||||||
|
json!({"result": {"data": [0, 1, 2, 2, 0]}}),
|
||||||
|
);
|
||||||
|
let tokens = session.editor().semantic_tokens();
|
||||||
|
assert_eq!(tokens[0].kind, SemanticTokenKind::Function);
|
||||||
|
assert_eq!(tokens[0].start, TextPosition { row: 0, column: 1 });
|
||||||
|
assert_eq!(tokens[0].end, TextPosition { row: 0, column: 2 });
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn lsp_semantics_discard_queued_same_revision_rename_and_close_outputs() {
|
fn lsp_semantics_discard_queued_same_revision_rename_and_close_outputs() {
|
||||||
let fixture = Fixture::new();
|
let fixture = Fixture::new();
|
||||||
|
|||||||
Reference in New Issue
Block a user