feat: redesign status chrome and floating prompts

This commit is contained in:
Valdemar Lorenzen
2026-10-12 12:21:31 +02:00
parent 8c6a050171
commit 52915ffacf
12 changed files with 1333 additions and 373 deletions
+2 -1
View File
@@ -41,6 +41,7 @@ Editor-owned jump history is a bounded 100-location `VecDeque` of buffer IDs and
Frontends inspect `buffer_id()`, `buffer_count()`, `has_buffer(id)`, and the ascending-ID `buffers()` iterator of borrowed path/dirty/active metadata. Existing text, cursor, selection, search, and mode methods describe only the active buffer.
Frontends inspect `tab_id()` for stable identity, `tab_index()` for display order, `tab_count()`, and `tab_buffer(index)` for borrowed title metadata. `pane_layout()` describes only the active tab; globally unique pane IDs keep inactive tabs' viewports alive. Documents remain unique across all tabs, while independent pane/buffer views are parked and restored without copying text.
Frontends borrow `top_bar_config()` and `bottom_bar_config()` for validated ordered left/center/right segment groups and effective segment styles, and `label(name)` for session-wide custom values. The TUI owns terminal-cell widths, full-row center anchoring, clipping, and accent styling. Label effects carry only a name and plain text; they are global presentation updates, not buffer/revision-bound results or render callbacks.
Frontends borrow `lsp_label()` / `lsp_names()` for the focused buffer's cached ready-server metadata. Initialization-only worker events trigger status redraws independently of diagnostics; session synchronization clears or replaces names when focus changes or a server retires. `notification_visible()` gates persistent operation/diagnostic popups independently of stored message text, so Escape dismissal neither alters text nor collapses carets. Display-only home abbreviation does not change document identity.
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). Dock side is a logical preference. They allocate rectangles, add folder suffixes/colors, 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.
@@ -59,7 +60,7 @@ Redraws stage Ratatui's buffer diff, cursor placement, and DEC synchronized-upda
`vendor/crossterm` retains Crossterm 0.29.0's upstream source and MIT license, with a small Unix decoder patch exposing `Event::ColorSchemeChanged` for `CSI ? 997 ; 1/2 n`. The report signals a palette refresh, not a choice of hard-coded light/dark colors. Valid fragmented reports and malformed color reports leave following keys intact. Cargo patches both direct and Ratatui dependency paths to this one copy; remove the patch/workspace member once upstream supports these notifications.
The TUI uses Ratatui constraint-based layout for a flexible buffer viewport, optional one-row top and bottom bars, and a separate prompt/message area. Both bars derive visibility from their configured content before allocating rows: empty content takes no row, and tabs-only content is hidden with a single tab in either position. Both bars render across ordinary, split, diff, and selector views. Each bar renders configured ordered left/center/right groups, with center anchored to the full row rather than the remaining gap. Overflow keeps the active tab title visible wherever tabs appear. Padding is inside segment styling, while leading/trailing edge glyphs are separate terminal-cell-width decorations. Scrolling, clipping, highlights, and cursor placement use the allocated regions, not terminal-wide offsets. An active command/search prompt takes priority in minimal height; zero-sized terminals render nothing. Layout and terminal-cell text mapping remain frontend-owned.
The TUI allocates a flexible buffer viewport and optional one-row top/bottom bars, with no reserved prompt/message footer. Both bars derive visibility from configured content: empty content takes no row, and tabs-only content is hidden with a single tab in either position. Both render across ordinary, split, diff, and selector views. Ordered left/center/right groups use full-row centering, active-tab collision priority, internal padding, and independent styled edges. Floating Command/Search input owns the hardware cursor; non-focusable operation/diagnostic notifications anchor bottom-right inside the chrome-free workspace and hide obscured document carets. Higher-priority modal overlays suppress them without changing input state. Wheel hit regions clear each frame and consume reports over these popups. Zero-sized terminals render nothing; minimal-height input never reserves another row. Layout and terminal-cell text mapping remain frontend-owned.
Mouse wheel input uses the last rendered document/panel rectangles for hit-testing. Document offsets and explorer scroll state remain frontend-owned; three-row wheel steps never become document cursor motions. Manual document views persist until logical caret-reveal, caret/selection, mode, revision, or buffer changes; offscreen primary and browsing-selection cursors are not placed. Hover wheel input reuses the existing editor-owned popup scrolling.
+11 -7
View File
@@ -50,13 +50,13 @@ Plugin paths resolve relative to the config file, not the shell's working direct
Use the two setters independently; a combined `config.bar = { ... }` assignment is not supported.
Defaults put `left: [:tabs]` and `right: [:battery]` in the top bar; the bottom has `left: [:mode, :pending, :buffer, :diagnostics, :path, :selections, :count, :language]` and `right: [:position]`; other groups are empty. Set the bars independently, as in this swapped layout with status information above and tabs plus live battery charge below:
Defaults put `left: [:tabs]` and `right: [:battery]` in the top bar; the bottom has `left: [:mode, :pending, :buffer, :diagnostics, :path, :selections, :count]` and `right: [:language, :lsp, :position]`; other groups are empty. Set the bars independently, as in this swapped layout with status information above and tabs plus live battery charge below:
```ruby
Vamp.configure do |config|
config.bar.top = {
left: [:mode, :path, :language, :diagnostics, :selections, :count, :pending, :buffer],
center: [], right: [:position],
left: [:mode, :path, :diagnostics, :selections, :count, :pending, :buffer],
center: [], right: [:language, :lsp, :position],
mode: { padding: 1, leading_edge: "", trailing_edge: "▓▒░" },
position: { padding: 1, leading_edge: "░▒▓", trailing_edge: "" }
}
@@ -71,12 +71,16 @@ Vamp.configure do |config|
end
```
Builtins are `:tabs`, `:battery`, `:mode`, `:path`, `:position`, `:language`, `:diagnostics`, `:selections`, `:count`, `:pending`, and `:buffer`, in either bar. They provide tab titles, battery charge, mode, file path/dirty state, caret position, detected language, diagnostics, selection count, numeric prefix, pending key sequence, and buffer identity/count.
Builtins are `:tabs`, `:battery`, `:mode`, `:path`, `:position`, `:language`, `:lsp`, `:diagnostics`, `:selections`, `:count`, `:pending`, and `:buffer`, in either bar. They provide tab titles, battery charge, mode, file path/dirty state, caret position, detected language, ready LSP server names, diagnostics, selection count, numeric prefix, pending key sequence, and buffer identity/count.
A bar with no visible content consumes no row. For example, `config.bar.top = { left: [], center: [], right: [] }` removes the top bar. A bar whose only visible content is `:tabs` is hidden with a single tab, whether above or below the workspace. Narrow layouts first omit inactive tabs, then optional segments from group ends to avoid overlap; the active tab drops decorations before its title is clipped. The command/search prompt and message area remain separate from both bars.
A bar with no visible content consumes no row. For example, `config.bar.top = { left: [], center: [], right: [] }` removes the top bar. A bar whose only visible content is `:tabs` is hidden with a single tab, whether above or below the workspace. Narrow layouts first omit inactive tabs, then optional segments from group ends to avoid overlap; the active tab drops decorations before its title is clipped. Commands and buffer searches use floating input; operation feedback uses a bottom-right popup. Neither reserves a footer row.
Each segment's `title` is a literal template supporting only `{index}`, `{name}`, `{path}`, and `{modified}`: the one-based tab index, filename, file path, and dirty marker (`" [+]"` when dirty, `""` when clean). Tabs use each tab's buffer metadata; other segments use the active buffer. Tabs default to `"{index} {name}{modified}"`; an empty title on another segment uses its builtin content or named label value. Templates are not Ruby code and never invoke a render callback.
`:path` and `{path}` abbreviate the actual home-directory prefix to `~/` (`~` for the home directory itself). This is display-only: save paths, buffer identity, and LSP file URIs retain the original path. Sibling directories with similar names are not abbreviated.
`:lsp` displays only ready servers assigned to the focused buffer, in configured stack order joined with ` + `. Disabled, initializing, failed, or unassigned servers do not appear. Names come from the initialization reply's `serverInfo.name`, falling back to the configured executable basename; no arbitrary named label can make an inactive server appear active.
`padding` adds spaces inside the segment's styling, on both sides of its title, and accepts integers from `0` through `64` (default `1`). `leading_edge` and `trailing_edge` are separate styled glyph strings, not padding; `""` removes an edge. Tabs default to `"░▒▓"` / `"▓▒░"` edges; mode has only the trailing `"▓▒░"` edge, position and battery only the leading `"░▒▓"` edge, and other segments have none. `first_leading_edge` optionally overrides the first occurrence's leading glyphs: for tabs this is the actual first tab, not the first tab remaining after clipping; other segments have one occurrence. Tabs default this override to `""`, suppressing the first tab's left fade. Unicode widths are measured in terminal cells. Edge strings must be single-line and contain no control characters. Unknown style keys, invalid templates, and wrong types reject configuration.
Any other segment name is a custom named value, such as `:build_status`. Set initial strings with `config.labels`; an empty value hides the custom segment, including its title override, padding, and edges. Names match `[A-Za-z_][A-Za-z0-9_]*`. Labels are plain text: control characters are replaced rather than interpreted as terminal escapes. The same named value can appear in either bar.
@@ -170,7 +174,7 @@ Vamp.configure do |config|
end
```
The last valid declaration for a key/mode wins. Invalid keys, incompatible action/mode pairs, ambiguous sequence prefixes, or unknown command names leave that binding unchanged and appear in the status message; other valid bindings still load. An invalid leader key reports a notice and retains Space, while valid bindings still install. Ruby syntax errors, unknown methods, and type errors—including non-string or empty leader values—disable the entire configuration and fall back to defaults. One plugin's load failure does not prevent other plugins loading. Disabling a plugin removes its single-key and sequence bindings, without restoring any default keys they replaced.
The last valid declaration for a key/mode wins. Invalid keys, incompatible action/mode pairs, ambiguous sequence prefixes, or unknown command names leave that binding unchanged and appear in the operation notification; other valid bindings still load. An invalid leader key reports a notice and retains Space, while valid bindings still install. Ruby syntax errors, unknown methods, and type errors—including non-string or empty leader values—disable the entire configuration and fall back to defaults. One plugin's load failure does not prevent other plugins loading. Disabling a plugin removes its single-key and sequence bindings, without restoring any default keys they replaced.
Bindings are resolved inside the editor module, not the terminal adapter. Bracketed paste remains literal and bypasses keybindings. If configuration disables your keys for leaving Insert/Command/Search/Visual mode or entering commands, restart with `--no-config` to restore defaults.
@@ -286,7 +290,7 @@ For programmatic bindings, `config.core.lsp` exposes immutable Normal-mode Actio
Interactive LSP commands use dotted names (`:lsp.hover`, `:lsp.definition`, `:lsp.diagnostics`). The existing whitespace separator keeps the whole dotted name separate from arguments. Current LSP commands take no arguments.
Diagnostics appear as severity-colored gutter numbers and text underlines; the primary caret's diagnostic appears in the message row. `:lsp.diagnostics` reports the current document's server state. Older versioned diagnostics are discarded; unversioned diagnostics can only be associated with the arrival-time document revision. Editing clears previous annotations immediately. Server startup/transport failures are reported and clear that server's diagnostics; other servers and native editing remain available. There are no automatic restarts.
Diagnostics appear as severity-colored gutter numbers and text underlines; the primary caret's diagnostic can also appear in the bottom-right notification popup. `:lsp.diagnostics` reports the current document's server state. Older versioned diagnostics are discarded; unversioned diagnostics can only be associated with the arrival-time document revision. Editing clears previous annotations immediately. Server startup/transport failures are reported and clear that server's diagnostics; other servers and native editing remain available. There are no automatic restarts.
Insert mode hides all diagnostic gutter colors, underlines, counts, and caret messages. Incoming results are still stored; leaving Insert reveals only the latest results matching the current text revision. Editing invalidates earlier results, so stale annotations never reappear on exit. Document synchronization stays live for completion and semantic highlighting; this display policy does not stop server-side diagnostic analysis.
+1
View File
@@ -43,6 +43,7 @@ Rust tests live alongside their modules; native LSP integration checks are in [`
- Session and Ruby checks exercise real Ruby execution, command/capability ownership, effect validation, configuration fallback, plugin retirement, crash/timeout recovery, and inherited-pipe cleanup.
- For chrome changes, exercise anchor/template validation, padding/edge bounds, all builtin/custom segments in either bar, named startup values, and atomic live label batches. Rendering regressions should cover swapped bars across ordinary/split/diff/selector views, full-row center anchoring, terminal-cell glyph widths, empty-content row removal, tabs-only singleton hiding in either position, active-tab preservation, padding inside styled edges, and separate prompt geometry. Labels are session-wide presentation state: test cross-tab/read-only updates and unchanged-value redraw behavior without collecting real battery data or invoking render callbacks.
- Battery checks inject samples and refresh gates, use temporary Linux sysfs fixtures, and validate native value types/bounds without depending on the machine's charge or clock. Unit sessions do not automatically read hardware. Live smoke should compare the default indicator with OS telemetry and observe a production refresh; no-battery, explicit overrides, and disabled anchors must not fabricate values.
- Status redesign checks cover component-aware home abbreviation without save-path changes; floating command/search submit/cancel, Unicode/tiny/offset clipping and wheel isolation; footer-row reclamation; notification Escape/modal/prefix priority and covered-caret visibility; and ready-server metadata using real framed subprocess fixtures, including stacks, initialization gates, failure, and focus changes.
- Unix terminal-hook checks use isolated child processes and real PTYs to keep process-global hooks out of the parallel runner. They cover raw/alternate-screen/mouse-capture cleanup on entry failure, normal return, I/O errors, and panics during frame construction, including original panic-payload propagation.
- Redraw checks inspect actual Crossterm output during repeated scrolling: supporting terminals receive synchronized frames, terminals ignoring synchronization never paint with a visible hardware cursor, and the final caret remains visible. A protocol-state regression verifies that the terminal is not left in synchronized-output mode while the next frame is computed.
- Wheel checks render document/panel views and cover three-row scrolling, unchanged carets/selections/text, first/last-page clamping, offscreen cursor hiding, no-op/custom-bound navigation reveal, manual persistence through idle/resize/palette updates, and floating/unfocused-docked explorer selection and focus preservation. Queued-input checks verify that the next frame shows ordered wheel steps without crossing a key or resize boundary.
+6 -2
View File
@@ -142,6 +142,10 @@ Submitting an empty search repeats the last query. Esc cancels without moving th
## Commands, saving, and pasted input
`:` opens compact floating command input; `/` opens the same kind of input for buffer text search. Enter submits and Escape cancels using the existing modal rules. Long Unicode input scrolls horizontally without changing buffer text. Input is bounded by the workspace, above configured bars, and temporarily hides operation notifications; there is no permanent prompt/footer row.
The latest reported operation result appears in a non-focusable popup at the workspace's bottom-right, just above the bottom bar when visible. It remains until dismissed or replaced, with no timer. In Normal mode, the first Escape dismisses it without collapsing carets; a subsequent Escape retains ordinary Normal behavior. Existing modal windows, focused explorers/diff selectors, and pending prefixes retain Escape priority. New operation messages reopen the popup; routine successful buffer switches do not duplicate the buffer/path information already shown in the bar.
Command-capable core actions derive canonical Ex names by removing `core.`: `core.quit` gives `:quit`, `core.save` gives `:save`, and `core.diff.close` gives `:diff.close`. Native editing and Insert-only completion/snippet actions remain action bindings, not additional Ex commands.
Commands: `:save [path]`, `:quit`, `:force_quit`, `:save_and_quit [path]`, `:plugins`, and `:plugin-command [arguments]`. Short names remain aliases of these canonical commands, with identical argument and safety rules:
@@ -220,9 +224,9 @@ Use `:tab.new` to open the current buffer in a new, focused tab with one pane. E
By default, multiple tabs show a single top row with numbered filenames and dirty markers, with padding inside each tab's styling. The active tab has an accent-colored background fading into the terminal background through `░▒▓` / `▓▒░`, except the first tab has no left fade; inactive tabs remain unfilled. Narrow terminals keep the active title visible. New/close actions have no default keybinding; for example, `config.bind "leader t", config.core.tab.new`.
The [top and bottom bars](configuration.md#top-and-bottom-bars) accept the same builtin and custom segments, anchored left, truly centered on the full row, or anchored right. Either bar can show tabs or status information, with independent title templates, internal padding, and styled edge glyphs. Defaults keep tabs above and status below; the command/search prompt stays separate. Empty content consumes no row, and a bar whose only visible content is tabs is hidden with a single tab in either position. Custom named values such as a battery label are session-wide and can update through plugins without editing buffers; empty values hide their segments. Both bars share collision handling: narrow layouts first omit inactive tabs, then optional segments from group ends to avoid overlap; the active tab drops decorations before its title is clipped.
The [top and bottom bars](configuration.md#top-and-bottom-bars) accept the same builtin and custom segments, anchored left, truly centered on the full row, or anchored right. Either bar can show tabs or status information, with independent title templates, internal padding, and styled edge glyphs. Defaults keep tabs and battery above, with home-abbreviated paths on the bottom left and language/ready LSP names/position on the bottom right. Floating input and operation notifications overlay the workspace without reserving a footer. Empty content consumes no row, and a bar whose only visible content is tabs is hidden with a single tab in either position. Custom named values are session-wide and can update through plugins without editing buffers; empty values hide their segments. Both bars share collision handling: narrow layouts first omit inactive tabs, then optional segments from group ends to avoid overlap; the active tab drops decorations before its title is clipped.
At very small heights, both bars yield to the separate active prompt and a document row. If only one bar row fits while both have content, the top bar receives it.
At very small heights, both bars yield to a document row; floating input still accepts text on a one-row terminal. If only one bar row fits while both have content, the top bar receives it.
Closing a tab refuses dirty buffers whose final displayed owner would disappear, including a comparison's working and return buffers. Buffers displayed in another tab remain open with their edits, even on force-close. Other hidden buffers remain loaded. Closing the last tab quits only if every open buffer is clean; force-close quits without saving.
+2 -2
View File
@@ -9,8 +9,8 @@ Vamp.configure do |config|
config.file_picker_view = "tree"
# Status above, tabs and live battery below; either bar accepts all segments.
config.bar.top = {
left: [:mode, :path, :language, :diagnostics, :selections, :count, :pending, :buffer],
center: [], right: [:position],
left: [:mode, :path, :diagnostics, :selections, :count, :pending, :buffer],
center: [], right: [:language, :lsp, :position],
mode: { padding: 1, leading_edge: "", trailing_edge: "▓▒░" },
position: { padding: 1, leading_edge: "░▒▓", trailing_edge: "" }
}
+26 -3
View File
@@ -10,6 +10,7 @@ const DEFAULT_BOTTOM_SEGMENTS: &[&str] = &[
"selections",
"count",
"language",
"lsp",
"position",
];
@@ -57,12 +58,12 @@ impl ChromeLine {
pub fn bottom() -> Self {
Self {
left: DEFAULT_BOTTOM_SEGMENTS[..DEFAULT_BOTTOM_SEGMENTS.len() - 1]
left: DEFAULT_BOTTOM_SEGMENTS[..DEFAULT_BOTTOM_SEGMENTS.len() - 3]
.iter()
.map(|name| (*name).into())
.collect(),
center: Vec::new(),
right: vec!["position".into()],
right: vec!["language".into(), "lsp".into(), "position".into()],
segments: DEFAULT_BOTTOM_SEGMENTS
.iter()
.map(|name| ((*name).into(), default_segment(name)))
@@ -267,6 +268,27 @@ mod tests {
bottom: ChromeLine,
}
#[test]
fn bottom_keeps_metadata_on_the_right_without_filled_styles() {
let line = ChromeLine::bottom();
assert_eq!(
line.left,
[
"mode",
"pending",
"buffer",
"diagnostics",
"path",
"selections",
"count"
]
);
assert_eq!(line.right, ["language", "lsp", "position"]);
for name in ["language", "lsp"] {
assert_eq!(line.segment(name), &SegmentConfig::default());
}
}
#[test]
fn omitted_options_inherit_and_explicit_groups_replace_defaults() {
let lines: Lines = serde_json::from_value(serde_json::json!({
@@ -277,7 +299,7 @@ mod tests {
assert_eq!(lines.top.segment("tabs").leading_edge, "░▒▓");
assert_eq!(lines.top.segment("battery").padding, 0);
assert!(lines.bottom.left.is_empty());
assert_eq!(lines.bottom.right, ["position"]);
assert_eq!(lines.bottom.right, ["language", "lsp", "position"]);
assert_eq!(lines.bottom.segment("workspace"), &SegmentConfig::default());
}
@@ -293,6 +315,7 @@ mod tests {
"selections",
"count",
"language",
"lsp",
"position",
"battery",
"workspace",
+289 -17
View File
@@ -1351,11 +1351,14 @@ pub struct Editor {
owned_system_clipboard: Option<(String, Vec<String>, bool)>,
last_edit: Option<EditRecipe>,
message: String,
notification_visible: bool,
top_bar: ChromeLine,
bottom_bar: ChromeLine,
labels: BTreeMap<String, String>,
battery_charge: Option<u8>,
battery_label: String,
lsp_names: Vec<String>,
lsp_label: String,
quit: bool,
capabilities: Vec<String>,
features: HashMap<String, Feature>,
@@ -1441,11 +1444,14 @@ impl Editor {
owned_system_clipboard: None,
last_edit: None,
message: String::new(),
notification_visible: true,
top_bar: ChromeLine::top(),
bottom_bar: ChromeLine::bottom(),
labels: BTreeMap::new(),
battery_charge: None,
battery_label: String::new(),
lsp_names: Vec::new(),
lsp_label: String::new(),
quit: false,
capabilities,
features: HashMap::new(),
@@ -1773,6 +1779,7 @@ impl Editor {
self.line_pending.take(),
self.seek_pending.take(),
std::mem::take(&mut self.message),
std::mem::replace(&mut self.notification_visible, true),
)
});
self.select_tab(target_tab);
@@ -1794,6 +1801,7 @@ impl Editor {
self.line_pending,
self.seek_pending,
self.message,
self.notification_visible,
) = interaction;
}
match result {
@@ -3199,7 +3207,6 @@ impl Editor {
popup: Completion { items, selected: 1 },
original,
});
self.message.clear();
Ok(())
}
@@ -3835,6 +3842,9 @@ impl Editor {
pub fn message(&self) -> &str {
&self.message
}
pub fn notification_visible(&self) -> bool {
self.notification_visible
}
pub fn top_bar_config(&self) -> &ChromeLine {
&self.top_bar
}
@@ -3847,6 +3857,12 @@ impl Editor {
pub fn battery_label(&self) -> &str {
&self.battery_label
}
pub fn lsp_names(&self) -> &[String] {
&self.lsp_names
}
pub fn lsp_label(&self) -> &str {
&self.lsp_label
}
pub fn path(&self) -> Option<&Path> {
self.active.path.as_deref()
}
@@ -3884,6 +3900,7 @@ impl Editor {
} else {
message
};
self.notification_visible = true;
}
pub fn set_chrome_config(
@@ -3928,6 +3945,20 @@ impl Editor {
Ok(true)
}
pub(crate) fn set_lsp_names(&mut self, mut names: Vec<String>) -> bool {
for name in &mut names {
if name.chars().any(char::is_control) {
*name = sanitize_label(std::mem::take(name));
}
}
if self.lsp_names == names {
return false;
}
self.lsp_label = names.join(" + ");
self.lsp_names = names;
true
}
pub(crate) fn set_battery_charge(&mut self, charge: Option<u8>) -> bool {
let charge = charge.filter(|charge| *charge <= 100);
if self.battery_charge == charge {
@@ -4775,6 +4806,14 @@ impl Editor {
if input == Input::Escape && self.count.take().is_some() {
return None;
}
if input == Input::Escape
&& self.active.view.mode == Mode::Normal
&& self.notification_visible
&& (!self.message.is_empty() || self.diagnostic_at_cursor().is_some())
{
self.notification_visible = false;
return None;
}
if matches!(self.active.view.mode, Mode::Normal | Mode::Visual)
&& let Input::Char(ch @ '0'..='9') = input
{
@@ -5124,7 +5163,6 @@ impl Editor {
self.active.view.linewise = linewise;
self.active.view.mode = Mode::Visual;
self.normalize_targets();
self.message.clear();
}
}
Action::SelectLines => self.split_rows(false, false),
@@ -5255,7 +5293,6 @@ impl Editor {
}
self.active.view.visual_motions = None;
self.normalize_targets();
self.message.clear();
self.record_jump(source);
}
@@ -5360,21 +5397,12 @@ impl Editor {
self.parked.insert(previous.id, previous);
self.load_view();
self.reveal_caret();
self.show_buffer();
}
fn show_buffer(&mut self) {
let path = self
.path()
.map_or_else(|| Cow::Borrowed("[No Name]"), Path::to_string_lossy);
self.set_message(format!("Buffer {}: {path}", self.active.id));
}
fn switch_buffer(&mut self, id: usize) {
if id == self.active.id {
self.reveal_caret();
self.park_source();
self.show_buffer();
} else if let Some(buffer) = self.parked.remove(&id) {
self.activate(buffer);
} else {
@@ -6424,7 +6452,6 @@ impl Editor {
}
self.active.view.visual_motions = None;
self.normalize_targets();
self.message.clear();
}
fn line_selections(&mut self, count: usize) -> Vec<Selection> {
@@ -6489,7 +6516,6 @@ impl Editor {
},
);
}
self.message.clear();
self.reveal_caret();
}
@@ -6518,7 +6544,6 @@ impl Editor {
if changing {
self.start_insert(before.unwrap(), recipe.unwrap().start);
self.edit_ranges(action, &ranges);
self.message.clear();
return;
}
self.edit_ranges(action, &ranges);
@@ -6770,7 +6795,6 @@ impl Editor {
});
}
self.record(before);
self.message.clear();
}
if !system {
self.clipboard = fragments;
@@ -7079,7 +7103,6 @@ impl Editor {
}
self.active.view.visual_motions = None;
self.record(before);
self.message.clear();
}
Err(error) => {
if !restored {
@@ -8612,6 +8635,206 @@ mod tests {
}
}
#[test]
fn notification_escape_preserves_carets_then_ordinary_escape_collapses_them() {
let mut editor = editor();
editor
.apply(vec![Effect::Replace("abcd\nabcd".into())])
.unwrap();
set_carets(&mut editor, &[(0, 1), (1, 1)], 1);
assert!(editor.notification_visible());
editor.set_message("Written");
editor.handle(Input::Right);
let targets = editor.active.view.targets.clone();
let revision = editor.text_revision();
let history = (editor.active.undo.len(), editor.active.redo.len());
editor.handle(Input::Escape);
assert!(!editor.notification_visible());
assert_eq!(editor.message(), "Written");
assert_eq!(editor.active.view.targets, targets);
assert_eq!(editor.active.view.primary, 1);
assert_eq!(editor.text_revision(), revision);
assert_eq!(
(editor.active.undo.len(), editor.active.redo.len()),
history
);
editor.handle(Input::Escape);
assert_eq!(editor.selection_count(), 1);
editor.set_message("Written");
assert!(editor.notification_visible());
type_text(&mut editor, "iX");
editor.handle(Input::Escape);
assert!(editor.notification_visible());
assert_eq!(editor.message(), "Written");
editor.handle(Input::Char('v'));
editor.handle(Input::Escape);
assert!(editor.notification_visible());
for prompt in [':', '/'] {
editor.handle(Input::Char(prompt));
editor.handle(Input::Char('x'));
editor.handle(Input::Escape);
assert_eq!(editor.mode(), Mode::Normal);
assert!(editor.notification_visible());
}
}
#[test]
fn notification_escape_waits_for_prefixes_and_focused_overlays() {
let mut editor = editor();
editor
.apply(vec![Effect::Replace("abcd\nabcd".into())])
.unwrap();
set_carets(&mut editor, &[(0, 1), (1, 1)], 1);
editor.set_message("Written");
let targets = editor.active.view.targets.clone();
for prefix in ["f", "3g", "2d", "4", " b"] {
type_text(&mut editor, prefix);
editor.handle(Input::Escape);
assert!(editor.notification_visible(), "{prefix}");
assert_eq!(editor.active.view.targets, targets, "{prefix}");
assert_eq!(editor.count(), None);
}
editor.show_startup_diagnostic("startup".into());
editor.handle(Input::Escape);
assert!(editor.startup_diagnostic().is_none());
assert!(editor.notification_visible());
editor.show_hover("hover".into()).unwrap();
editor.handle(Input::Escape);
assert!(editor.hover().is_none());
assert!(editor.notification_visible());
editor.handle(Input::Ctrl('p'));
editor.handle(Input::Escape);
assert!(editor.palette().is_none());
assert!(editor.notification_visible());
editor.open_file_picker(FilePresentation::Docked);
editor.handle(Input::Char('i'));
assert!(editor.file_picker().unwrap().searching);
editor.handle(Input::Escape);
assert!(editor.file_picker().unwrap().focused);
assert!(editor.notification_visible());
editor.handle(Input::Escape);
assert!(!editor.file_picker().unwrap().focused);
assert!(editor.notification_visible());
assert_eq!(editor.active.view.targets, targets);
editor.handle(Input::Escape);
assert!(!editor.notification_visible());
assert_eq!(editor.active.view.targets, targets);
}
#[test]
fn empty_notification_does_not_swallow_escape_but_cursor_diagnostic_does() {
let mut editor = editor();
editor
.apply(vec![Effect::Replace("abcd\nabcd".into())])
.unwrap();
set_carets(&mut editor, &[(0, 1), (1, 1)], 1);
editor.handle(Input::Escape);
assert_eq!(editor.selection_count(), 1);
assert!(editor.notification_visible());
editor
.replace_diagnostics(
"fixture",
editor.buffer_id(),
editor.text_revision(),
vec![annotation((1, 0), (1, 3), "diagnostic")],
)
.unwrap();
set_carets(&mut editor, &[(0, 1), (1, 1)], 1);
editor.handle(Input::Escape);
assert!(!editor.notification_visible());
assert_eq!(editor.selection_count(), 2);
assert_eq!(editor.diagnostic_at_cursor().unwrap().message, "diagnostic");
editor.handle(Input::Escape);
assert_eq!(editor.selection_count(), 1);
}
#[test]
fn hidden_pane_interactions_cannot_reopen_or_dismiss_focused_notifications() {
let mut editor = editor();
let hidden = editor.pane_id();
editor.execute("pane.split_vertical");
editor.set_message("focused");
editor.handle(Input::Escape);
assert!(!editor.notification_visible());
editor.with_pane(hidden, |pane| pane.set_message("background"));
assert_eq!(editor.message(), "focused");
assert!(!editor.notification_visible());
editor.with_render_pane(hidden, |pane| {
assert!(pane.message().is_empty());
});
assert!(!editor.notification_visible());
editor.set_message("focused");
editor.with_pane(hidden, |pane| {
pane.set_message("background");
pane.handle(Input::Escape);
});
assert!(editor.notification_visible());
assert_eq!(editor.message(), "focused");
editor.execute("tab.new");
editor.set_message("new tab");
editor.handle(Input::Escape);
editor.with_pane(hidden, |pane| pane.set_message("hidden tab"));
assert_eq!(editor.message(), "new tab");
assert!(!editor.notification_visible());
}
#[test]
fn buffer_activation_preserves_last_operation_and_dismissal() {
let directory = Directory::new();
let path = directory.path("opened.txt");
fs::write(&path, "text").unwrap();
let mut editor = editor();
editor.set_message("Written");
editor.execute(&format!("edit {}", path.display()));
assert_eq!(editor.message(), "Written");
editor.handle(Input::Escape);
for command in ["buffer 1", "bnext", "bprev", "buffer 1"] {
editor.execute(command);
assert_eq!(editor.message(), "Written");
assert!(!editor.notification_visible());
}
editor.execute("buffer 999");
assert!(editor.message().contains("No buffer"));
assert!(editor.notification_visible());
}
#[test]
fn native_lsp_cache_is_independent_presentation_metadata() {
let mut editor = editor();
editor
.apply(vec![Effect::Replace("document".into())])
.unwrap();
type_text(&mut editor, "vl");
editor.set_message("Written");
editor.notification_visible = false;
editor
.set_label("lsp".into(), "configured label".into())
.unwrap();
let targets = editor.active.view.targets.clone();
let revision = editor.text_revision();
let history = (editor.active.undo.len(), editor.active.redo.len());
assert!(editor.lsp_names().is_empty());
assert_eq!(editor.lsp_label(), "");
assert!(editor.set_lsp_names(vec!["rust-analyzer".into(), "界 server".into()]));
assert_eq!(editor.lsp_names(), ["rust-analyzer", "界 server"]);
assert_eq!(editor.lsp_label(), "rust-analyzer + 界 server");
assert!(!editor.set_lsp_names(vec!["rust-analyzer".into(), "界 server".into()]));
assert!(editor.set_lsp_names(vec!["unsafe\nname".into()]));
assert_eq!(editor.lsp_label(), "unsafe�name");
assert!(editor.set_lsp_names(Vec::new()));
assert_eq!(editor.lsp_label(), "");
assert_eq!(editor.label("lsp"), "configured label");
assert_eq!(editor.message(), "Written");
assert!(!editor.notification_visible());
assert_eq!(editor.mode(), Mode::Visual);
assert_eq!(editor.active.view.targets, targets);
assert_eq!(editor.text_revision(), revision);
assert_eq!(
(editor.active.undo.len(), editor.active.redo.len()),
history
);
}
#[test]
fn pane_focus_remaps_do_not_steal_printable_picker_filter_input() {
let directory = Directory::new();
@@ -9571,6 +9794,38 @@ mod tests {
assert!(editor.diff_interacting());
}
#[test]
fn diff_documents_dismiss_notifications_but_files_and_selectors_keep_escape() {
let directory = Directory::new();
let mut editor = editor();
historical_diff(
&mut editor,
&directory.0,
vec!["before".into(), "second".into()],
vec!["after".into(), "second".into()],
);
set_carets(&mut editor, &[(0, 1), (1, 1)], 1);
editor.set_message("operation");
editor.handle(Input::Escape);
assert!(!editor.notification_visible());
assert_eq!(editor.selection_count(), 2);
assert!(editor.diff_interacting());
editor.handle(Input::Escape);
assert_eq!(editor.selection_count(), 1);
editor.set_message("operation");
editor.diff.as_mut().unwrap().focus = Focus::Files;
editor.handle(Input::Escape);
assert!(editor.notification_visible());
assert_ne!(editor.diff().unwrap().focus, Focus::Files);
editor.execute("diff.close");
editor.execute("diff");
editor.handle(Input::Char('q'));
assert_eq!(editor.diff().unwrap().query, "q");
editor.handle(Input::Escape);
assert_eq!(editor.diff().unwrap().query, "");
assert!(editor.notification_visible());
}
#[test]
fn readonly_native_panes_allow_selection_yank_search_and_multicarets() {
for focus in [Focus::Before, Focus::After] {
@@ -11451,6 +11706,7 @@ mod tests {
);
editor.handle(Input::Escape);
assert!(editor.hover().is_none());
assert!(editor.notification_visible());
assert_eq!(editor.active.view.targets, targets);
assert_eq!(editor.active.view.primary, primary);
assert_eq!(editor.context().text, text);
@@ -11461,6 +11717,9 @@ mod tests {
);
assert_eq!(editor.mode(), Mode::Normal);
editor.handle(Input::Escape);
assert!(!editor.notification_visible());
assert_eq!(editor.active.view.targets, targets);
editor.handle(Input::Escape);
assert_eq!(editor.selection_count(), 1);
editor.show_hover("documentation".into()).unwrap();
editor.handle(Input::Char('i'));
@@ -14211,6 +14470,11 @@ mod tests {
assert_eq!(editor.count(), None);
assert_eq!(editor.sequence_pending(), None);
editor.handle(Input::Escape);
if !visual {
assert!(!editor.notification_visible());
assert_eq!(editor.selection_count(), 2);
editor.handle(Input::Escape);
}
assert_eq!(editor.mode(), Mode::Normal);
assert_eq!(editor.selection_count(), if visual { 2 } else { 1 });
}
@@ -16901,6 +17165,10 @@ mod tests {
assert_eq!(editor.context().text, "ab\nxyzq");
editor.handle(Input::Char('u'));
editor.handle(Input::Escape);
assert!(!editor.notification_visible());
assert_eq!(editor.selection_count(), 2);
editor.handle(Input::Escape);
assert_eq!(editor.selection_count(), 1);
let before = editor.snapshot();
let undo = editor.active.undo.len();
let redo = editor.active.redo.len();
@@ -17979,6 +18247,8 @@ mod tests {
fn plugin_effects_validate_before_any_mutation_and_group_undo() {
let mut editor = editor();
editor.set_message("unchanged");
editor.handle(Input::Escape);
assert!(!editor.notification_visible());
editor
.set_label("battery".into(), "unchanged".into())
.unwrap();
@@ -17996,6 +18266,7 @@ mod tests {
);
assert_eq!(editor.context().text, "");
assert_eq!(editor.message(), "unchanged");
assert!(!editor.notification_visible());
assert_eq!(editor.label("battery"), "unchanged");
assert!(!editor.dirty());
assert!(
@@ -18024,6 +18295,7 @@ mod tests {
.unwrap();
assert_eq!(editor.context().text, "one\ntwo");
assert_eq!(editor.message(), "ok");
assert!(editor.notification_visible());
editor.handle(Input::Char('u'));
assert_eq!(editor.context().text, "");
assert_eq!(editor.label("battery"), "updated");
+31
View File
@@ -60,6 +60,7 @@ struct Contribution {
}
enum Output {
Ready(String),
Update(PluginUpdate),
Diagnostics {
snapshot: Arc<DocumentSnapshot>,
@@ -77,6 +78,7 @@ struct Worker {
failure: Receiver<String>,
cancelled: Arc<AtomicBool>,
retired: bool,
name: Option<String>,
thread: Option<thread::JoinHandle<()>>,
}
@@ -145,6 +147,20 @@ impl Lsp {
!self.configs.is_empty()
}
/// Ready servers assigned to this document, in configured stack order.
pub fn active_server_names(&self, buffer_id: usize) -> impl Iterator<Item = &str> {
self.documents
.get(&buffer_id)
.into_iter()
.flat_map(|doc| &doc.servers)
.filter_map(|key| {
self.servers
.get(key)
.filter(|worker| !worker.retired)
.and_then(|worker| worker.name.as_deref())
})
}
pub fn notify_document(&mut self, event: &DocumentEvent) -> Result<(), String> {
if !self.is_enabled() {
return Ok(());
@@ -334,6 +350,7 @@ impl Lsp {
break;
};
match output {
Output::Ready(name) => worker.name = Some(name),
Output::Update(update) => self.updates.push(update),
Output::Diagnostics { snapshot, items } => {
let Some(doc) = self.documents.get_mut(&snapshot.id).filter(|doc| {
@@ -485,6 +502,7 @@ impl Lsp {
failure,
cancelled,
retired: false,
name: None,
thread,
},
);
@@ -552,6 +570,7 @@ impl Lsp {
return;
}
worker.retired = true;
worker.name = None;
worker.cancelled.store(true, Ordering::Release);
for (id, doc) in documents {
if doc.servers.first() == Some(key) {
@@ -1232,6 +1251,18 @@ impl Runtime {
Some(json!({"settings": self.config.settings})),
)?;
self.ready = true;
let name = result["serverInfo"]["name"]
.as_str()
.filter(|name| !name.chars().any(char::is_control))
.map(str::trim)
.filter(|name| !name.is_empty())
.unwrap_or_else(|| {
Path::new(&self.config.command[0])
.file_name()
.and_then(|name| name.to_str())
.unwrap_or(&self.config.command[0])
});
self.send(Output::Ready(clean(name)))?;
let documents = std::mem::take(&mut self.documents);
for (id, mut doc) in documents {
self.open(&mut doc)?;
-1
View File
@@ -1220,7 +1220,6 @@ end
&crate::chrome::SegmentConfig::default()
);
assert!(loaded.bottom_bar.left.is_empty());
assert_eq!(loaded.bottom_bar.right, ["position"]);
assert_eq!(loaded.labels["battery"], "bat 38%");
assert_eq!(loaded.labels["workspace"], "界 e\u{301} 👩‍🚀\t\n\u{1b}");
}
+21 -5
View File
@@ -251,9 +251,7 @@ impl EditorSession {
));
}
}
if notices.is_empty() {
editor.set_message("Ready • --no-config uses default keybindings");
} else {
if !notices.is_empty() {
editor.set_message(notices.join(" | "));
}
let mut session = Self {
@@ -512,7 +510,7 @@ impl EditorSession {
}
if !self.lsp.is_enabled() && !self.plugins.values().any(RubyPlugin::is_async) {
self.documents.clear();
return false;
return self.refresh_lsp_names();
}
let mut events = Vec::new();
if !self.editor.should_quit() {
@@ -604,7 +602,25 @@ impl EditorSession {
for (owner, error) in failures {
self.retire(&owner, &error);
}
changed
changed | self.refresh_lsp_names()
}
fn refresh_lsp_names(&mut self) -> bool {
let buffer_id = self.editor.buffer_id();
if self.lsp.active_server_names(buffer_id).eq(self
.editor
.lsp_names()
.iter()
.map(String::as_str))
{
return false;
}
self.editor.set_lsp_names(
self.lsp
.active_server_names(buffer_id)
.map(str::to_owned)
.collect(),
)
}
fn source_is_current(&self, source: InvocationSource) -> bool {
+799 -331
View File
File diff suppressed because it is too large Load Diff
+145 -4
View File
@@ -91,7 +91,20 @@ loop do
'tokenModifiers' => ['custom', 'readonly', 'declaration', 'async', 'defaultLibrary']},
'range' => true, 'full' => mode.include?('range') ? false : {'delta' => true}}
end
result(initialize_id, {'capabilities' => capabilities})
response = {'capabilities' => capabilities}
response['serverInfo'] = {'name' => "Fixture λ #{File.basename(log, '.jsonl')}"}
case mode
when 'name-absent' then response.delete('serverInfo')
when 'name-empty' then response['serverInfo']['name'] = ''
when 'name-blank' then response['serverInfo']['name'] = ' '
when 'name-controls' then response['serverInfo']['name'] = "bad\e\nname"
when 'name-edge-controls' then response['serverInfo']['name'] = "\nFixture\n"
when 'name-type' then response['serverInfo']['name'] = 42
when 'initialize-error'
emit({'id' => initialize_id, 'error' => {'code' => -32603, 'message' => 'fixture initialization failed'}})
when 'initialize-invalid' then response['capabilities'] = nil
end
result(initialize_id, response) unless mode == 'initialize-error'
initialized_sent = true
end
if !held.empty? && File.exist?(release) && ['stale', 'hover-stale'].include?(mode)
@@ -300,7 +313,7 @@ impl Fixture {
fn open(&self, mode: &str) -> EditorSession {
let server = self.root.join("server.rb");
fs::write(&server, SERVER).unwrap();
let command = if mode == "spawn" {
let mut command = if mode == "spawn" {
json!([self.root.join("unavailable-language-server")])
} else {
json!([
@@ -314,6 +327,12 @@ impl Fixture {
self.alternate
])
};
if mode == "name-absent" {
command
.as_array_mut()
.unwrap()
.insert(0, json!("/usr/bin/env"));
}
let options = json!({
"extensions": if mode == "irrelevant" { vec!["myl"] } else { vec!["rs"] },
"command": command,
@@ -492,6 +511,106 @@ fn edit_target(session: &mut EditorSession, fixture: &Fixture) -> usize {
id
}
#[test]
fn lsp_ready_names_wait_for_initialization_and_follow_only_the_focused_buffer() {
let fixture = Fixture::new();
let mut session = fixture.open("controlled-gated");
assert!(session.editor().lsp_names().is_empty());
assert_eq!(session.editor().lsp_label(), "");
until(&mut session, &fixture, |_| {
fixture.method_count("initialize") == 1
});
assert!(session.editor().lsp_names().is_empty());
let unserved = fixture.root.join("notes.txt");
fs::write(&unserved, "unserved\n").unwrap();
session.execute(&format!("edit {}", unserved.display()));
let unserved_id = session.editor().buffer_id();
fixture.release();
until(&mut session, &fixture, |_| {
fixture.method_count("textDocument/didOpen") == 1
});
session.poll();
assert!(session.editor().lsp_names().is_empty());
assert_eq!(session.editor().lsp_label(), "");
session.execute("buffer 1");
assert_eq!(session.editor().lsp_names(), ["Fixture λ protocol"]);
assert_eq!(session.editor().lsp_label(), "Fixture λ protocol");
let revision = session.editor().text_revision();
let cursor = session.editor().cursor();
session.poll();
assert_eq!(session.editor().text_revision(), revision);
assert_eq!(session.editor().cursor(), cursor);
assert_eq!(session.editor().mode(), Mode::Normal);
session.execute(&format!("buffer {unserved_id}"));
assert!(session.editor().lsp_names().is_empty());
assert_eq!(session.editor().lsp_label(), "");
session.execute("buffer 1");
assert_eq!(session.editor().lsp_names(), ["Fixture λ protocol"]);
fixture.control(0, 1, json!({"action": "die"}));
until(&mut session, &fixture, |session| {
session.editor().message().contains("server stopped")
});
assert!(session.editor().lsp_names().is_empty());
assert_eq!(session.editor().lsp_label(), "");
}
#[test]
fn lsp_readiness_alone_requests_a_redraw_without_document_or_message_changes() {
let fixture = Fixture::new();
let mut session = fixture.open("controlled-gated");
until(&mut session, &fixture, |_| {
fixture.method_count("initialize") == 1
});
assert!(session.editor().lsp_names().is_empty());
let message = session.editor().message().to_owned();
let revision = session.editor().text_revision();
let cursor = session.editor().cursor();
fixture.release();
let deadline = Instant::now() + Duration::from_secs(5);
loop {
let changed = session.poll();
if !session.editor().lsp_names().is_empty() {
assert!(changed, "readiness must redraw even without plugin effects");
break;
}
assert!(
Instant::now() < deadline,
"fixture server did not become ready"
);
thread::sleep(Duration::from_millis(2));
}
assert_eq!(session.editor().lsp_names(), ["Fixture λ protocol"]);
assert_eq!(session.editor().message(), message);
assert_eq!(session.editor().text_revision(), revision);
assert_eq!(session.editor().cursor(), cursor);
assert_eq!(session.editor().mode(), Mode::Normal);
assert_eq!(session.editor().diagnostics().count(), 0);
}
#[test]
fn lsp_ready_names_fall_back_to_executable_only_for_missing_or_invalid_server_names() {
for mode in [
"name-absent",
"name-empty",
"name-blank",
"name-controls",
"name-edge-controls",
"name-type",
] {
let fixture = Fixture::new();
let mut session = fixture.open(mode);
until(&mut session, &fixture, |session| {
!session.editor().lsp_names().is_empty()
});
let expected = if mode == "name-absent" { "env" } else { "ruby" };
assert_eq!(session.editor().lsp_names(), [expected], "{mode}");
assert_eq!(session.editor().lsp_label(), expected, "{mode}");
}
}
#[test]
fn lsp_framing_lifecycle_diagnostics_and_unicode_dirty_definition() {
let fixture = Fixture::new();
@@ -885,6 +1004,8 @@ fn lsp_server_failures_and_unsupported_results_leave_native_editor_available() {
"spawn",
"malformed",
"dead",
"initialize-error",
"initialize-invalid",
"unsupported",
"remote",
"bounds",
@@ -892,7 +1013,10 @@ fn lsp_server_failures_and_unsupported_results_leave_native_editor_available() {
] {
let fixture = Fixture::new();
let mut session = fixture.open(mode);
if matches!(mode, "spawn" | "malformed" | "dead") {
if matches!(
mode,
"spawn" | "malformed" | "dead" | "initialize-error" | "initialize-invalid"
) {
until(&mut session, &fixture, |session| {
session.editor().message().contains(if mode == "spawn" {
"failed to start"
@@ -901,6 +1025,8 @@ fn lsp_server_failures_and_unsupported_results_leave_native_editor_available() {
})
});
assert_eq!(session.editor().diagnostics().count(), 0);
assert!(session.editor().lsp_names().is_empty(), "{mode}");
assert_eq!(session.editor().lsp_label(), "", "{mode}");
} else {
until(&mut session, &fixture, |session| {
session.editor().diagnostics().count() == 2
@@ -1285,11 +1411,13 @@ fn publish(
fn lsp_python_stack_merges_in_assignment_order_and_routes_positions_only_to_ty() {
let fixture = Fixture::new();
let mut session = fixture.open_stack(["controlled-gated", "controlled"], true);
until(&mut session, &fixture, |_| {
until(&mut session, &fixture, |session| {
fixture.worker_count(0, "initialize") == 1
&& fixture.worker_count(1, "textDocument/didOpen") == 1
&& session.editor().lsp_names() == ["Fixture λ protocol-1"]
});
assert_eq!(fixture.worker_count(0, "textDocument/didOpen"), 0);
assert_eq!(session.editor().lsp_label(), "Fixture λ protocol-1");
session.handle(Input::Char('A'));
session.handle(Input::Char('!'));
session.handle(Input::Escape);
@@ -1298,9 +1426,14 @@ fn lsp_python_stack_merges_in_assignment_order_and_routes_positions_only_to_ty()
fixture.release();
until(&mut session, &fixture, |session| {
session.editor().hover().is_some()
&& session.editor().lsp_names() == ["Fixture λ protocol", "Fixture λ protocol-1"]
});
assert_eq!(fixture.worker_count(0, "textDocument/hover"), 1);
assert_eq!(fixture.worker_count(1, "textDocument/hover"), 0);
assert_eq!(
session.editor().lsp_label(),
"Fixture λ protocol + Fixture λ protocol-1"
);
for worker in 0..2 {
let frames = fixture.worker_frames(worker);
let initialize = frames
@@ -1551,8 +1684,16 @@ fn lsp_python_stack_failures_retire_only_the_offender_in_active_and_parked_buffe
});
}
assert_eq!(diagnostic_messages(&session), ["healthy target"]);
let name = if survivor == 0 {
"Fixture λ protocol"
} else {
"Fixture λ protocol-1"
};
assert_eq!(session.editor().lsp_names(), [name]);
assert_eq!(session.editor().lsp_label(), name);
session.execute("buffer 1");
assert_eq!(diagnostic_messages(&session), ["healthy source"]);
assert_eq!(session.editor().lsp_names(), [name]);
session.handle(Input::Char('A'));
session.handle(Input::Char('!'));
session.handle(Input::Escape);