diff --git a/Cargo.lock b/Cargo.lock index 01bfb08b0a..e28f2d640e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1275,11 +1275,10 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.83" +version = "1.0.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +checksum = "0f8e7c90afad890484a21653d08b6e209ae34770fb5ee298f9c699fcc1e5c856" dependencies = [ - "jobserver", "libc", ] @@ -1880,6 +1879,30 @@ dependencies = [ "zed-actions", ] +[[package]] +name = "command_palette2" +version = "0.1.0" +dependencies = [ + "anyhow", + "collections", + "ctor", + "editor2", + "env_logger 0.9.3", + "fuzzy2", + "gpui2", + "language2", + "picker2", + "project2", + "serde", + "serde_json", + "settings2", + "theme2", + "ui2", + "util", + "workspace2", + "zed_actions2", +] + [[package]] name = "component_test" version = "0.1.0" @@ -2781,6 +2804,7 @@ dependencies = [ "tree-sitter-html", "tree-sitter-rust", "tree-sitter-typescript", + "ui2", "unindent", "util", "workspace2", @@ -4370,15 +4394,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" -[[package]] -name = "jobserver" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" -dependencies = [ - "libc", -] - [[package]] name = "journal" version = "0.1.0" @@ -4433,6 +4448,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json_comments" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dbbfed4e59ba9750e15ba154fdfd9329cee16ff3df539c2666b70f58cc32105" + [[package]] name = "jwt" version = "0.16.0" @@ -6129,6 +6150,7 @@ dependencies = [ "serde_json", "settings2", "theme2", + "ui2", "util", ] @@ -9186,6 +9208,7 @@ dependencies = [ "convert_case 0.6.0", "gpui2", "indexmap 1.9.3", + "json_comments", "log", "rust-embed", "serde", @@ -11394,6 +11417,7 @@ dependencies = [ "cli", "client2", "collections", + "command_palette2", "copilot2", "ctor", "db2", @@ -11481,6 +11505,15 @@ dependencies = [ "util", "uuid 1.4.1", "workspace2", + "zed_actions2", +] + +[[package]] +name = "zed_actions2" +version = "0.1.0" +dependencies = [ + "gpui2", + "serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 4dd2737e5d..2df17864d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ members = [ "crates/collab_ui", "crates/collections", "crates/command_palette", + "crates/command_palette2", "crates/component_test", "crates/context_menu", "crates/copilot", @@ -111,7 +112,8 @@ members = [ "crates/xtask", "crates/zed", "crates/zed2", - "crates/zed-actions" + "crates/zed-actions", + "crates/zed_actions2" ] default-members = ["crates/zed"] resolver = "2" diff --git a/assets/keymaps/jetbrains.json b/assets/keymaps/jetbrains.json index ab093a8deb..b2ed144a3f 100644 --- a/assets/keymaps/jetbrains.json +++ b/assets/keymaps/jetbrains.json @@ -10,6 +10,7 @@ "bindings": { "ctrl->": "zed::IncreaseBufferFontSize", "ctrl-<": "zed::DecreaseBufferFontSize", + "ctrl-shift-j": "editor::JoinLines", "cmd-d": "editor::DuplicateLine", "cmd-backspace": "editor::DeleteLine", "cmd-pagedown": "editor::MovePageDown", @@ -18,7 +19,7 @@ "cmd-alt-enter": "editor::NewlineAbove", "shift-enter": "editor::NewlineBelow", "cmd--": "editor::Fold", - "cmd-=": "editor::UnfoldLines", + "cmd-+": "editor::UnfoldLines", "alt-shift-g": "editor::SplitSelectionIntoLines", "ctrl-g": [ "editor::SelectNext", diff --git a/assets/settings/default.json b/assets/settings/default.json index 9a6c7587d6..42f3b31286 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -209,7 +209,7 @@ "ensure_final_newline_on_save": true, // Whether or not to perform a buffer format before saving "format_on_save": "on", - // How to perform a buffer format. This setting can take two values: + // How to perform a buffer format. This setting can take 4 values: // // 1. Format code using the current language server: // "formatter": "language_server" diff --git a/assets/themes/src/vscode/synthwave-84/synthwave.json b/assets/themes/src/vscode/synthwave-84/synthwave.json index 8e356d587c..9b23270d8c 100644 --- a/assets/themes/src/vscode/synthwave-84/synthwave.json +++ b/assets/themes/src/vscode/synthwave-84/synthwave.json @@ -285,7 +285,7 @@ "name": "Inherited class", "scope": "entity.other.inherited-class", "settings": { - "foreground": "#D50D50" + "foreground": "#D50" } }, { @@ -452,7 +452,7 @@ "entity.other.attribute-name.pseudo-class" ], "settings": { - "foreground": "#D50D50" + "foreground": "#D50" } }, { @@ -495,7 +495,7 @@ "name": "Markup link", "scope": "markup.underline.link", "settings": { - "foreground": "#D50D50" + "foreground": "#D50" } }, { diff --git a/crates/command_palette2/Cargo.toml b/crates/command_palette2/Cargo.toml new file mode 100644 index 0000000000..bcc0099c20 --- /dev/null +++ b/crates/command_palette2/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "command_palette2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/command_palette.rs" +doctest = false + +[dependencies] +collections = { path = "../collections" } +editor = { package = "editor2", path = "../editor2" } +fuzzy = { package = "fuzzy2", path = "../fuzzy2" } +gpui = { package = "gpui2", path = "../gpui2" } +picker = { package = "picker2", path = "../picker2" } +project = { package = "project2", path = "../project2" } +settings = { package = "settings2", path = "../settings2" } +ui = { package = "ui2", path = "../ui2" } +util = { path = "../util" } +theme = { package = "theme2", path = "../theme2" } +workspace = { package="workspace2", path = "../workspace2" } +zed_actions = { package = "zed_actions2", path = "../zed_actions2" } +anyhow.workspace = true +serde.workspace = true +[dev-dependencies] +gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } +editor = { package = "editor2", path = "../editor2", features = ["test-support"] } +language = { package="language2", path = "../language2", features = ["test-support"] } +project = { package="project2", path = "../project2", features = ["test-support"] } +serde_json.workspace = true +workspace = { package="workspace2", path = "../workspace2", features = ["test-support"] } +ctor.workspace = true +env_logger.workspace = true diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs new file mode 100644 index 0000000000..c7a6c9ee83 --- /dev/null +++ b/crates/command_palette2/src/command_palette.rs @@ -0,0 +1,541 @@ +use collections::{CommandPaletteFilter, HashMap}; +use fuzzy::{StringMatch, StringMatchCandidate}; +use gpui::{ + actions, div, Action, AppContext, Component, Div, EventEmitter, FocusHandle, Keystroke, + ParentElement, Render, StatelessInteractive, Styled, View, ViewContext, VisualContext, + WeakView, WindowContext, +}; +use picker::{Picker, PickerDelegate}; +use std::{ + cmp::{self, Reverse}, + sync::Arc, +}; +use theme::ActiveTheme; +use ui::{v_stack, HighlightedLabel, StyledExt}; +use util::{ + channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL}, + ResultExt, +}; +use workspace::{Modal, ModalEvent, Workspace}; +use zed_actions::OpenZedURL; + +actions!(Toggle); + +pub fn init(cx: &mut AppContext) { + cx.set_global(HitCounts::default()); + cx.observe_new_views(CommandPalette::register).detach(); +} + +pub struct CommandPalette { + picker: View>, +} + +impl CommandPalette { + fn register(workspace: &mut Workspace, _: &mut ViewContext) { + workspace.register_action(|workspace, _: &Toggle, cx| { + let Some(previous_focus_handle) = cx.focused() else { + return; + }; + workspace.toggle_modal(cx, move |cx| CommandPalette::new(previous_focus_handle, cx)); + }); + } + + fn new(previous_focus_handle: FocusHandle, cx: &mut ViewContext) -> Self { + let filter = cx.try_global::(); + + let commands = cx + .available_actions() + .into_iter() + .filter_map(|action| { + let name = action.name(); + let namespace = name.split("::").next().unwrap_or("malformed action name"); + if filter.is_some_and(|f| f.filtered_namespaces.contains(namespace)) { + return None; + } + + Some(Command { + name: humanize_action_name(&name), + action, + keystrokes: vec![], // todo!() + }) + }) + .collect(); + + let delegate = + CommandPaletteDelegate::new(cx.view().downgrade(), commands, previous_focus_handle); + + let picker = cx.build_view(|cx| Picker::new(delegate, cx)); + Self { picker } + } +} + +impl EventEmitter for CommandPalette {} +impl Modal for CommandPalette { + fn focus(&self, cx: &mut WindowContext) { + self.picker.update(cx, |picker, cx| picker.focus(cx)); + } +} + +impl Render for CommandPalette { + type Element = Div; + + fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { + v_stack().w_96().child(self.picker.clone()) + } +} + +pub type CommandPaletteInterceptor = + Box Option>; + +pub struct CommandInterceptResult { + pub action: Box, + pub string: String, + pub positions: Vec, +} + +pub struct CommandPaletteDelegate { + command_palette: WeakView, + commands: Vec, + matches: Vec, + selected_ix: usize, + previous_focus_handle: FocusHandle, +} + +struct Command { + name: String, + action: Box, + keystrokes: Vec, +} + +impl Clone for Command { + fn clone(&self) -> Self { + Self { + name: self.name.clone(), + action: self.action.boxed_clone(), + keystrokes: self.keystrokes.clone(), + } + } +} +/// Hit count for each command in the palette. +/// We only account for commands triggered directly via command palette and not by e.g. keystrokes because +/// if an user already knows a keystroke for a command, they are unlikely to use a command palette to look for it. +#[derive(Default)] +struct HitCounts(HashMap); + +impl CommandPaletteDelegate { + fn new( + command_palette: WeakView, + commands: Vec, + previous_focus_handle: FocusHandle, + ) -> Self { + Self { + command_palette, + matches: commands + .iter() + .enumerate() + .map(|(i, command)| StringMatch { + candidate_id: i, + string: command.name.clone(), + positions: Vec::new(), + score: 0.0, + }) + .collect(), + commands, + selected_ix: 0, + previous_focus_handle, + } + } +} + +impl PickerDelegate for CommandPaletteDelegate { + type ListItem = Div>; + + fn placeholder_text(&self) -> Arc { + "Execute a command...".into() + } + + fn match_count(&self) -> usize { + self.matches.len() + } + + fn selected_index(&self) -> usize { + self.selected_ix + } + + fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext>) { + self.selected_ix = ix; + } + + fn update_matches( + &mut self, + query: String, + cx: &mut ViewContext>, + ) -> gpui::Task<()> { + let mut commands = self.commands.clone(); + + cx.spawn(move |picker, mut cx| async move { + cx.read_global::(|hit_counts, _| { + commands.sort_by_key(|action| { + ( + Reverse(hit_counts.0.get(&action.name).cloned()), + action.name.clone(), + ) + }); + }) + .ok(); + + let candidates = commands + .iter() + .enumerate() + .map(|(ix, command)| StringMatchCandidate { + id: ix, + string: command.name.to_string(), + char_bag: command.name.chars().collect(), + }) + .collect::>(); + let mut matches = if query.is_empty() { + candidates + .into_iter() + .enumerate() + .map(|(index, candidate)| StringMatch { + candidate_id: index, + string: candidate.string, + positions: Vec::new(), + score: 0.0, + }) + .collect() + } else { + fuzzy::match_strings( + &candidates, + &query, + true, + 10000, + &Default::default(), + cx.background_executor().clone(), + ) + .await + }; + + let mut intercept_result = cx + .try_read_global(|interceptor: &CommandPaletteInterceptor, cx| { + (interceptor)(&query, cx) + }) + .flatten(); + + if *RELEASE_CHANNEL == ReleaseChannel::Dev { + if parse_zed_link(&query).is_some() { + intercept_result = Some(CommandInterceptResult { + action: OpenZedURL { url: query.clone() }.boxed_clone(), + string: query.clone(), + positions: vec![], + }) + } + } + if let Some(CommandInterceptResult { + action, + string, + positions, + }) = intercept_result + { + if let Some(idx) = matches + .iter() + .position(|m| commands[m.candidate_id].action.type_id() == action.type_id()) + { + matches.remove(idx); + } + commands.push(Command { + name: string.clone(), + action, + keystrokes: vec![], + }); + matches.insert( + 0, + StringMatch { + candidate_id: commands.len() - 1, + string, + positions, + score: 0.0, + }, + ) + } + picker + .update(&mut cx, |picker, _| { + let delegate = &mut picker.delegate; + delegate.commands = commands; + delegate.matches = matches; + if delegate.matches.is_empty() { + delegate.selected_ix = 0; + } else { + delegate.selected_ix = + cmp::min(delegate.selected_ix, delegate.matches.len() - 1); + } + }) + .log_err(); + }) + } + + fn dismissed(&mut self, cx: &mut ViewContext>) { + self.command_palette + .update(cx, |_, cx| cx.emit(ModalEvent::Dismissed)) + .log_err(); + } + + fn confirm(&mut self, _: bool, cx: &mut ViewContext>) { + if self.matches.is_empty() { + self.dismissed(cx); + return; + } + let action_ix = self.matches[self.selected_ix].candidate_id; + let command = self.commands.swap_remove(action_ix); + cx.update_global(|hit_counts: &mut HitCounts, _| { + *hit_counts.0.entry(command.name).or_default() += 1; + }); + let action = command.action; + cx.focus(&self.previous_focus_handle); + cx.dispatch_action(action); + self.dismissed(cx); + } + + fn render_match( + &self, + ix: usize, + selected: bool, + cx: &mut ViewContext>, + ) -> Self::ListItem { + let colors = cx.theme().colors(); + let Some(r#match) = self.matches.get(ix) else { + return div(); + }; + let Some(command) = self.commands.get(r#match.candidate_id) else { + return div(); + }; + + div() + .px_1() + .text_color(colors.text) + .text_ui() + .bg(colors.ghost_element_background) + .rounded_md() + .when(selected, |this| this.bg(colors.ghost_element_selected)) + .hover(|this| this.bg(colors.ghost_element_hover)) + .child(HighlightedLabel::new( + command.name.clone(), + r#match.positions.clone(), + )) + } + + // fn render_match( + // &self, + // ix: usize, + // mouse_state: &mut MouseState, + // selected: bool, + // cx: &gpui::AppContext, + // ) -> AnyElement> { + // let mat = &self.matches[ix]; + // let command = &self.actions[mat.candidate_id]; + // let theme = theme::current(cx); + // let style = theme.picker.item.in_state(selected).style_for(mouse_state); + // let key_style = &theme.command_palette.key.in_state(selected); + // let keystroke_spacing = theme.command_palette.keystroke_spacing; + + // Flex::row() + // .with_child( + // Label::new(mat.string.clone(), style.label.clone()) + // .with_highlights(mat.positions.clone()), + // ) + // .with_children(command.keystrokes.iter().map(|keystroke| { + // Flex::row() + // .with_children( + // [ + // (keystroke.ctrl, "^"), + // (keystroke.alt, "⌥"), + // (keystroke.cmd, "⌘"), + // (keystroke.shift, "⇧"), + // ] + // .into_iter() + // .filter_map(|(modifier, label)| { + // if modifier { + // Some( + // Label::new(label, key_style.label.clone()) + // .contained() + // .with_style(key_style.container), + // ) + // } else { + // None + // } + // }), + // ) + // .with_child( + // Label::new(keystroke.key.clone(), key_style.label.clone()) + // .contained() + // .with_style(key_style.container), + // ) + // .contained() + // .with_margin_left(keystroke_spacing) + // .flex_float() + // })) + // .contained() + // .with_style(style.container) + // .into_any() + // } +} + +fn humanize_action_name(name: &str) -> String { + let capacity = name.len() + name.chars().filter(|c| c.is_uppercase()).count(); + let mut result = String::with_capacity(capacity); + for char in name.chars() { + if char == ':' { + if result.ends_with(':') { + result.push(' '); + } else { + result.push(':'); + } + } else if char == '_' { + result.push(' '); + } else if char.is_uppercase() { + if !result.ends_with(' ') { + result.push(' '); + } + result.extend(char.to_lowercase()); + } else { + result.push(char); + } + } + result +} + +impl std::fmt::Debug for Command { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Command") + .field("name", &self.name) + .field("keystrokes", &self.keystrokes) + .finish() + } +} + +// #[cfg(test)] +// mod tests { +// use std::sync::Arc; + +// use super::*; +// use editor::Editor; +// use gpui::{executor::Deterministic, TestAppContext}; +// use project::Project; +// use workspace::{AppState, Workspace}; + +// #[test] +// fn test_humanize_action_name() { +// assert_eq!( +// humanize_action_name("editor::GoToDefinition"), +// "editor: go to definition" +// ); +// assert_eq!( +// humanize_action_name("editor::Backspace"), +// "editor: backspace" +// ); +// assert_eq!( +// humanize_action_name("go_to_line::Deploy"), +// "go to line: deploy" +// ); +// } + +// #[gpui::test] +// async fn test_command_palette(deterministic: Arc, cx: &mut TestAppContext) { +// let app_state = init_test(cx); + +// let project = Project::test(app_state.fs.clone(), [], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); +// let workspace = window.root(cx); +// let editor = window.add_view(cx, |cx| { +// let mut editor = Editor::single_line(None, cx); +// editor.set_text("abc", cx); +// editor +// }); + +// workspace.update(cx, |workspace, cx| { +// cx.focus(&editor); +// workspace.add_item(Box::new(editor.clone()), cx) +// }); + +// workspace.update(cx, |workspace, cx| { +// toggle_command_palette(workspace, &Toggle, cx); +// }); + +// let palette = workspace.read_with(cx, |workspace, _| { +// workspace.modal::().unwrap() +// }); + +// palette +// .update(cx, |palette, cx| { +// // Fill up palette's command list by running an empty query; +// // we only need it to subsequently assert that the palette is initially +// // sorted by command's name. +// palette.delegate_mut().update_matches("".to_string(), cx) +// }) +// .await; + +// palette.update(cx, |palette, _| { +// let is_sorted = +// |actions: &[Command]| actions.windows(2).all(|pair| pair[0].name <= pair[1].name); +// assert!(is_sorted(&palette.delegate().actions)); +// }); + +// palette +// .update(cx, |palette, cx| { +// palette +// .delegate_mut() +// .update_matches("bcksp".to_string(), cx) +// }) +// .await; + +// palette.update(cx, |palette, cx| { +// assert_eq!(palette.delegate().matches[0].string, "editor: backspace"); +// palette.confirm(&Default::default(), cx); +// }); +// deterministic.run_until_parked(); +// editor.read_with(cx, |editor, cx| { +// assert_eq!(editor.text(cx), "ab"); +// }); + +// // Add namespace filter, and redeploy the palette +// cx.update(|cx| { +// cx.update_default_global::(|filter, _| { +// filter.filtered_namespaces.insert("editor"); +// }) +// }); + +// workspace.update(cx, |workspace, cx| { +// toggle_command_palette(workspace, &Toggle, cx); +// }); + +// // Assert editor command not present +// let palette = workspace.read_with(cx, |workspace, _| { +// workspace.modal::().unwrap() +// }); + +// palette +// .update(cx, |palette, cx| { +// palette +// .delegate_mut() +// .update_matches("bcksp".to_string(), cx) +// }) +// .await; + +// palette.update(cx, |palette, _| { +// assert!(palette.delegate().matches.is_empty()) +// }); +// } + +// fn init_test(cx: &mut TestAppContext) -> Arc { +// cx.update(|cx| { +// let app_state = AppState::test(cx); +// theme::init(cx); +// language::init(cx); +// editor::init(cx); +// workspace::init(app_state.clone(), cx); +// init(cx); +// Project::init_settings(cx); +// app_state +// }) +// } +// } diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index e794771434..4748f63e5d 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -171,10 +171,9 @@ impl ProjectDiagnosticsEditor { .entry(*language_server_id) .or_default() .insert(path.clone()); - let no_multiselections = this.editor.update(cx, |editor, cx| { - editor.selections.all::(cx).len() <= 1 - }); - if no_multiselections && !this.is_dirty(cx) { + if this.editor.read(cx).selections.all::(cx).is_empty() + && !this.is_dirty(cx) + { this.update_excerpts(Some(*language_server_id), cx); } } diff --git a/crates/editor2/Cargo.toml b/crates/editor2/Cargo.toml index b897110966..e45c33d917 100644 --- a/crates/editor2/Cargo.toml +++ b/crates/editor2/Cargo.toml @@ -44,6 +44,7 @@ snippet = { path = "../snippet" } sum_tree = { path = "../sum_tree" } text = { package="text2", path = "../text2" } theme = { package="theme2", path = "../theme2" } +ui = { package = "ui2", path = "../ui2" } util = { path = "../util" } sqlez = { path = "../sqlez" } workspace = { package = "workspace2", path = "../workspace2" } diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 51cd549923..b1f0d26786 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -39,10 +39,12 @@ use futures::FutureExt; use fuzzy::{StringMatch, StringMatchCandidate}; use git::diff_hunk_to_display; use gpui::{ - action, actions, point, px, relative, rems, size, AnyElement, AppContext, BackgroundExecutor, - Bounds, ClipboardItem, Context, DispatchContext, EventEmitter, FocusHandle, FontFeatures, - FontStyle, FontWeight, HighlightStyle, Hsla, InputHandler, Model, Pixels, Render, Subscription, - Task, TextStyle, View, ViewContext, VisualContext, WeakView, WindowContext, + action, actions, div, point, px, relative, rems, size, uniform_list, AnyElement, AppContext, + AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Component, Context, + DispatchContext, EventEmitter, FocusHandle, FontFeatures, FontStyle, FontWeight, + HighlightStyle, Hsla, InputHandler, Model, MouseButton, ParentElement, Pixels, Render, + StatelessInteractive, Styled, Subscription, Task, TextStyle, UniformListScrollHandle, View, + ViewContext, VisualContext, WeakView, WindowContext, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HoverState}; @@ -67,7 +69,7 @@ pub use multi_buffer::{ }; use ordered_float::OrderedFloat; use parking_lot::{Mutex, RwLock}; -use project::{FormatTrigger, Location, Project}; +use project::{FormatTrigger, Location, Project, ProjectTransaction}; use rand::prelude::*; use rpc::proto::*; use scroll::{ @@ -95,6 +97,7 @@ use text::{OffsetUtf16, Rope}; use theme::{ ActiveTheme, DiagnosticStyle, PlayerColor, SyntaxTheme, Theme, ThemeColors, ThemeSettings, }; +use ui::{IconButton, StyledExt}; use util::{post_inc, RangeExt, ResultExt, TryFutureExt}; use workspace::{ item::ItemEvent, searchable::SearchEvent, ItemNavHistory, SplitDirection, ViewId, Workspace, @@ -384,26 +387,6 @@ actions!( UnfoldLines, ); -// impl_actions!( -// editor, -// [ -// SelectNext, -// SelectPrevious, -// SelectAllMatches, -// SelectToBeginningOfLine, -// SelectToEndOfLine, -// ToggleCodeActions, -// MovePageUp, -// MovePageDown, -// ConfirmCompletion, -// ConfirmCodeAction, -// ToggleComments, -// FoldAt, -// UnfoldAt, -// GutterHover -// ] -// ); - enum DocumentHighlightRead {} enum DocumentHighlightWrite {} enum InputComposition {} @@ -919,15 +902,14 @@ impl ContextMenu { fn render( &self, cursor_position: DisplayPoint, - style: EditorStyle, + style: &EditorStyle, workspace: Option>, cx: &mut ViewContext, ) -> (DisplayPoint, AnyElement) { - todo!() - // match self { - // ContextMenu::Completions(menu) => (cursor_position, menu.render(style, workspace, cx)), - // ContextMenu::CodeActions(menu) => menu.render(cursor_position, style, cx), - // } + match self { + ContextMenu::Completions(menu) => (cursor_position, menu.render(style, workspace, cx)), + ContextMenu::CodeActions(menu) => menu.render(cursor_position, style, cx), + } } } @@ -940,29 +922,13 @@ struct CompletionsMenu { match_candidates: Arc<[StringMatchCandidate]>, matches: Arc<[StringMatch]>, selected_item: usize, - list: UniformListState, -} - -// todo!(this is fake) -#[derive(Clone, Default)] -struct UniformListState; - -// todo!(this is fake) -impl UniformListState { - pub fn scroll_to(&mut self, target: ScrollTarget) {} -} - -// todo!(this is somewhat fake) -#[derive(Debug)] -pub enum ScrollTarget { - Show(usize), - Center(usize), + scroll_handle: UniformListScrollHandle, } impl CompletionsMenu { fn select_first(&mut self, project: Option<&Model>, cx: &mut ViewContext) { self.selected_item = 0; - self.list.scroll_to(ScrollTarget::Show(self.selected_item)); + self.scroll_handle.scroll_to_item(self.selected_item); self.attempt_resolve_selected_completion_documentation(project, cx); cx.notify(); } @@ -973,7 +939,7 @@ impl CompletionsMenu { } else { self.selected_item = self.matches.len() - 1; } - self.list.scroll_to(ScrollTarget::Show(self.selected_item)); + self.scroll_handle.scroll_to_item(self.selected_item); self.attempt_resolve_selected_completion_documentation(project, cx); cx.notify(); } @@ -984,14 +950,14 @@ impl CompletionsMenu { } else { self.selected_item = 0; } - self.list.scroll_to(ScrollTarget::Show(self.selected_item)); + self.scroll_handle.scroll_to_item(self.selected_item); self.attempt_resolve_selected_completion_documentation(project, cx); cx.notify(); } fn select_last(&mut self, project: Option<&Model>, cx: &mut ViewContext) { self.selected_item = self.matches.len() - 1; - self.list.scroll_to(ScrollTarget::Show(self.selected_item)); + self.scroll_handle.scroll_to_item(self.selected_item); self.attempt_resolve_selected_completion_documentation(project, cx); cx.notify(); } @@ -1252,13 +1218,13 @@ impl CompletionsMenu { fn render( &self, - style: EditorStyle, + style: &EditorStyle, workspace: Option>, cx: &mut ViewContext, - ) { + ) -> AnyElement { todo!("old implementation below") } - // ) -> AnyElement { + // enum CompletionTag {} // let settings = EditorSettings>(cx); @@ -1527,14 +1493,14 @@ struct CodeActionsMenu { actions: Arc<[CodeAction]>, buffer: Model, selected_item: usize, - list: UniformListState, + scroll_handle: UniformListScrollHandle, deployed_from_indicator: bool, } impl CodeActionsMenu { fn select_first(&mut self, cx: &mut ViewContext) { self.selected_item = 0; - self.list.scroll_to(ScrollTarget::Show(self.selected_item)); + self.scroll_handle.scroll_to_item(self.selected_item); cx.notify() } @@ -1544,7 +1510,7 @@ impl CodeActionsMenu { } else { self.selected_item = self.actions.len() - 1; } - self.list.scroll_to(ScrollTarget::Show(self.selected_item)); + self.scroll_handle.scroll_to_item(self.selected_item); cx.notify(); } @@ -1554,13 +1520,13 @@ impl CodeActionsMenu { } else { self.selected_item = 0; } - self.list.scroll_to(ScrollTarget::Show(self.selected_item)); + self.scroll_handle.scroll_to_item(self.selected_item); cx.notify(); } fn select_last(&mut self, cx: &mut ViewContext) { self.selected_item = self.actions.len() - 1; - self.list.scroll_to(ScrollTarget::Show(self.selected_item)); + self.scroll_handle.scroll_to_item(self.selected_item); cx.notify() } @@ -1571,83 +1537,70 @@ impl CodeActionsMenu { fn render( &self, mut cursor_position: DisplayPoint, - style: EditorStyle, + style: &EditorStyle, cx: &mut ViewContext, ) -> (DisplayPoint, AnyElement) { - todo!("old version below") + let actions = self.actions.clone(); + let selected_item = self.selected_item; + let element = uniform_list( + "code_actions_menu", + self.actions.len(), + move |editor, range, cx| { + actions[range.clone()] + .iter() + .enumerate() + .map(|(ix, action)| { + let item_ix = range.start + ix; + let selected = selected_item == item_ix; + let colors = cx.theme().colors(); + div() + .px_2() + .text_ui() + .text_color(colors.text) + .when(selected, |style| { + style + .bg(colors.element_active) + .text_color(colors.text_accent) + }) + .hover(|style| { + style + .bg(colors.element_hover) + .text_color(colors.text_accent) + }) + .on_mouse_down(MouseButton::Left, move |editor: &mut Editor, _, cx| { + cx.stop_propagation(); + editor + .confirm_code_action( + &ConfirmCodeAction { + item_ix: Some(item_ix), + }, + cx, + ) + .map(|task| task.detach_and_log_err(cx)); + }) + .child(action.lsp_action.title.clone()) + }) + .collect() + }, + ) + .elevation_1(cx) + .px_2() + .py_1() + .with_width_from_item( + self.actions + .iter() + .enumerate() + .max_by_key(|(_, action)| action.lsp_action.title.chars().count()) + .map(|(ix, _)| ix), + ) + .render(); + + if self.deployed_from_indicator { + *cursor_position.column_mut() = 0; + } + + (cursor_position, element) } - // enum ActionTag {} - - // let container_style = style.autocomplete.container; - // let actions = self.actions.clone(); - // let selected_item = self.selected_item; - // let element = UniformList::new( - // self.list.clone(), - // actions.len(), - // cx, - // move |_, range, items, cx| { - // let start_ix = range.start; - // for (ix, action) in actions[range].iter().enumerate() { - // let item_ix = start_ix + ix; - // items.push( - // MouseEventHandler::new::(item_ix, cx, |state, _| { - // let item_style = if item_ix == selected_item { - // style.autocomplete.selected_item - // } else if state.hovered() { - // style.autocomplete.hovered_item - // } else { - // style.autocomplete.item - // }; - - // Text::new(action.lsp_action.title.clone(), style.text.clone()) - // .with_soft_wrap(false) - // .contained() - // .with_style(item_style) - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_down(MouseButton::Left, move |_, this, cx| { - // let workspace = this - // .workspace - // .as_ref() - // .and_then(|(workspace, _)| workspace.upgrade(cx)); - // cx.window_context().defer(move |cx| { - // if let Some(workspace) = workspace { - // workspace.update(cx, |workspace, cx| { - // if let Some(task) = Editor::confirm_code_action( - // workspace, - // &ConfirmCodeAction { - // item_ix: Some(item_ix), - // }, - // cx, - // ) { - // task.detach_and_log_err(cx); - // } - // }); - // } - // }); - // }) - // .into_any(), - // ); - // } - // }, - // ) - // .with_width_from_item( - // self.actions - // .iter() - // .enumerate() - // .max_by_key(|(_, action)| action.lsp_action.title.chars().count()) - // .map(|(ix, _)| ix), - // ) - // .contained() - // .with_style(container_style) - // .into_any(); - - // if self.deployed_from_indicator { - // *cursor_position.column_mut() = 0; - // } - - // (cursor_position, element) - // } } pub struct CopilotState { @@ -3660,7 +3613,7 @@ impl Editor { completions: Arc::new(RwLock::new(completions.into())), matches: Vec::new().into(), selected_item: 0, - list: Default::default(), + scroll_handle: UniformListScrollHandle::new(), }; menu.filter(query.as_deref(), cx.background_executor().clone()) .await; @@ -3846,156 +3799,161 @@ impl Editor { // })) // } - // pub fn toggle_code_actions(&mut self, action: &ToggleCodeActions, cx: &mut ViewContext) { - // let mut context_menu = self.context_menu.write(); - // if matches!(context_menu.as_ref(), Some(ContextMenu::CodeActions(_))) { - // *context_menu = None; - // cx.notify(); - // return; - // } - // drop(context_menu); + pub fn toggle_code_actions(&mut self, action: &ToggleCodeActions, cx: &mut ViewContext) { + let mut context_menu = self.context_menu.write(); + if matches!(context_menu.as_ref(), Some(ContextMenu::CodeActions(_))) { + *context_menu = None; + cx.notify(); + return; + } + drop(context_menu); - // let deployed_from_indicator = action.deployed_from_indicator; - // let mut task = self.code_actions_task.take(); - // cx.spawn(|this, mut cx| async move { - // while let Some(prev_task) = task { - // prev_task.await; - // task = this.update(&mut cx, |this, _| this.code_actions_task.take())?; - // } + let deployed_from_indicator = action.deployed_from_indicator; + let mut task = self.code_actions_task.take(); + cx.spawn(|this, mut cx| async move { + while let Some(prev_task) = task { + prev_task.await; + task = this.update(&mut cx, |this, _| this.code_actions_task.take())?; + } - // this.update(&mut cx, |this, cx| { - // if this.focused { - // if let Some((buffer, actions)) = this.available_code_actions.clone() { - // this.completion_tasks.clear(); - // this.discard_copilot_suggestion(cx); - // *this.context_menu.write() = - // Some(ContextMenu::CodeActions(CodeActionsMenu { - // buffer, - // actions, - // selected_item: Default::default(), - // list: Default::default(), - // deployed_from_indicator, - // })); - // } - // } - // })?; + this.update(&mut cx, |this, cx| { + if this.focus_handle.is_focused(cx) { + if let Some((buffer, actions)) = this.available_code_actions.clone() { + this.completion_tasks.clear(); + this.discard_copilot_suggestion(cx); + *this.context_menu.write() = + Some(ContextMenu::CodeActions(CodeActionsMenu { + buffer, + actions, + selected_item: Default::default(), + scroll_handle: UniformListScrollHandle::default(), + deployed_from_indicator, + })); + cx.notify(); + } + } + })?; - // Ok::<_, anyhow::Error>(()) - // }) - // .detach_and_log_err(cx); - // } + Ok::<_, anyhow::Error>(()) + }) + .detach_and_log_err(cx); + } - // pub fn confirm_code_action( - // workspace: &mut Workspace, - // action: &ConfirmCodeAction, - // cx: &mut ViewContext, - // ) -> Option>> { - // let editor = workspace.active_item(cx)?.act_as::(cx)?; - // let actions_menu = if let ContextMenu::CodeActions(menu) = - // editor.update(cx, |editor, cx| editor.hide_context_menu(cx))? - // { - // menu - // } else { - // return None; - // }; - // let action_ix = action.item_ix.unwrap_or(actions_menu.selected_item); - // let action = actions_menu.actions.get(action_ix)?.clone(); - // let title = action.lsp_action.title.clone(); - // let buffer = actions_menu.buffer; + pub fn confirm_code_action( + &mut self, + action: &ConfirmCodeAction, + cx: &mut ViewContext, + ) -> Option>> { + let actions_menu = if let ContextMenu::CodeActions(menu) = self.hide_context_menu(cx)? { + menu + } else { + return None; + }; + let action_ix = action.item_ix.unwrap_or(actions_menu.selected_item); + let action = actions_menu.actions.get(action_ix)?.clone(); + let title = action.lsp_action.title.clone(); + let buffer = actions_menu.buffer; + let workspace = self.workspace()?; - // let apply_code_actions = workspace.project().clone().update(cx, |project, cx| { - // project.apply_code_action(buffer, action, true, cx) - // }); - // let editor = editor.downgrade(); - // Some(cx.spawn(|workspace, cx| async move { - // let project_transaction = apply_code_actions.await?; - // Self::open_project_transaction(&editor, workspace, project_transaction, title, cx).await - // })) - // } + let apply_code_actions = workspace + .read(cx) + .project() + .clone() + .update(cx, |project, cx| { + project.apply_code_action(buffer, action, true, cx) + }); + let workspace = workspace.downgrade(); + Some(cx.spawn(|editor, cx| async move { + let project_transaction = apply_code_actions.await?; + Self::open_project_transaction(&editor, workspace, project_transaction, title, cx).await + })) + } - // async fn open_project_transaction( - // this: &WeakViewHandle Result<()> { - // let replica_id = this.read_with(&cx, |this, cx| this.replica_id(cx))?; + async fn open_project_transaction( + this: &WeakView, + workspace: WeakView, + transaction: ProjectTransaction, + title: String, + mut cx: AsyncWindowContext, + ) -> Result<()> { + let replica_id = this.update(&mut cx, |this, cx| this.replica_id(cx))?; - // let mut entries = transaction.0.into_iter().collect::>(); - // entries.sort_unstable_by_key(|(buffer, _)| { - // buffer.read_with(&cx, |buffer, _| buffer.file().map(|f| f.path().clone())) - // }); + let mut entries = transaction.0.into_iter().collect::>(); + cx.update(|_, cx| { + entries.sort_unstable_by_key(|(buffer, _)| { + buffer.read(cx).file().map(|f| f.path().clone()) + }); + })?; - // // If the project transaction's edits are all contained within this editor, then - // // avoid opening a new editor to display them. + // If the project transaction's edits are all contained within this editor, then + // avoid opening a new editor to display them. - // if let Some((buffer, transaction)) = entries.first() { - // if entries.len() == 1 { - // let excerpt = this.read_with(&cx, |editor, cx| { - // editor - // .buffer() - // .read(cx) - // .excerpt_containing(editor.selections.newest_anchor().head(), cx) - // })?; - // if let Some((_, excerpted_buffer, excerpt_range)) = excerpt { - // if excerpted_buffer == *buffer { - // let all_edits_within_excerpt = buffer.read_with(&cx, |buffer, _| { - // let excerpt_range = excerpt_range.to_offset(buffer); - // buffer - // .edited_ranges_for_transaction::(transaction) - // .all(|range| { - // excerpt_range.start <= range.start - // && excerpt_range.end >= range.end - // }) - // }); + if let Some((buffer, transaction)) = entries.first() { + if entries.len() == 1 { + let excerpt = this.update(&mut cx, |editor, cx| { + editor + .buffer() + .read(cx) + .excerpt_containing(editor.selections.newest_anchor().head(), cx) + })?; + if let Some((_, excerpted_buffer, excerpt_range)) = excerpt { + if excerpted_buffer == *buffer { + let all_edits_within_excerpt = buffer.read_with(&cx, |buffer, _| { + let excerpt_range = excerpt_range.to_offset(buffer); + buffer + .edited_ranges_for_transaction::(transaction) + .all(|range| { + excerpt_range.start <= range.start + && excerpt_range.end >= range.end + }) + })?; - // if all_edits_within_excerpt { - // return Ok(()); - // } - // } - // } - // } - // } else { - // return Ok(()); - // } + if all_edits_within_excerpt { + return Ok(()); + } + } + } + } + } else { + return Ok(()); + } - // let mut ranges_to_highlight = Vec::new(); - // let excerpt_buffer = cx.build_model(|cx| { - // let mut multibuffer = MultiBuffer::new(replica_id).with_title(title); - // for (buffer_handle, transaction) in &entries { - // let buffer = buffer_handle.read(cx); - // ranges_to_highlight.extend( - // multibuffer.push_excerpts_with_context_lines( - // buffer_handle.clone(), - // buffer - // .edited_ranges_for_transaction::(transaction) - // .collect(), - // 1, - // cx, - // ), - // ); - // } - // multibuffer.push_transaction(entries.iter().map(|(b, t)| (b, t)), cx); - // multibuffer - // }); + let mut ranges_to_highlight = Vec::new(); + let excerpt_buffer = cx.build_model(|cx| { + let mut multibuffer = MultiBuffer::new(replica_id).with_title(title); + for (buffer_handle, transaction) in &entries { + let buffer = buffer_handle.read(cx); + ranges_to_highlight.extend( + multibuffer.push_excerpts_with_context_lines( + buffer_handle.clone(), + buffer + .edited_ranges_for_transaction::(transaction) + .collect(), + 1, + cx, + ), + ); + } + multibuffer.push_transaction(entries.iter().map(|(b, t)| (b, t)), cx); + multibuffer + })?; - // workspace.update(&mut cx, |workspace, cx| { - // let project = workspace.project().clone(); - // let editor = - // cx.add_view(|cx| Editor::for_multibuffer(excerpt_buffer, Some(project), cx)); - // workspace.add_item(Box::new(editor.clone()), cx); - // editor.update(cx, |editor, cx| { - // editor.highlight_background::( - // ranges_to_highlight, - // |theme| theme.editor.highlighted_line_background, - // cx, - // ); - // }); - // })?; + workspace.update(&mut cx, |workspace, cx| { + let project = workspace.project().clone(); + let editor = + cx.build_view(|cx| Editor::for_multibuffer(excerpt_buffer, Some(project), cx)); + workspace.add_item(Box::new(editor.clone()), cx); + editor.update(cx, |editor, cx| { + editor.highlight_background::( + ranges_to_highlight, + |theme| theme.editor_highlighted_line_background, + cx, + ); + }); + })?; - // Ok(()) - // } + Ok(()) + } fn refresh_code_actions(&mut self, cx: &mut ViewContext) -> Option<()> { let project = self.project.clone()?; @@ -4390,41 +4348,29 @@ impl Editor { self.discard_copilot_suggestion(cx); } - // pub fn render_code_actions_indicator( - // &self, - // style: &EditorStyle, - // is_active: bool, - // cx: &mut ViewContext, - // ) -> Option> { - // if self.available_code_actions.is_some() { - // enum CodeActions {} - // Some( - // MouseEventHandler::new::(0, cx, |state, _| { - // Svg::new("icons/bolt.svg").with_color( - // style - // .code_actions - // .indicator - // .in_state(is_active) - // .style_for(state) - // .color, - // ) - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .with_padding(Padding::uniform(3.)) - // .on_down(MouseButton::Left, |_, this, cx| { - // this.toggle_code_actions( - // &ToggleCodeActions { - // deployed_from_indicator: true, - // }, - // cx, - // ); - // }) - // .into_any(), - // ) - // } else { - // None - // } - // } + pub fn render_code_actions_indicator( + &self, + style: &EditorStyle, + is_active: bool, + cx: &mut ViewContext, + ) -> Option> { + if self.available_code_actions.is_some() { + Some( + IconButton::new("code_actions_indicator", ui::Icon::Bolt) + .on_click(|editor: &mut Editor, cx| { + editor.toggle_code_actions( + &ToggleCodeActions { + deployed_from_indicator: true, + }, + cx, + ); + }) + .render(), + ) + } else { + None + } + } // pub fn render_fold_indicators( // &self, @@ -4491,29 +4437,27 @@ impl Editor { // } pub fn context_menu_visible(&self) -> bool { - false - // todo!("context menu") - // self.context_menu - // .read() - // .as_ref() - // .map_or(false, |menu| menu.visible()) + self.context_menu + .read() + .as_ref() + .map_or(false, |menu| menu.visible()) } - // pub fn render_context_menu( - // &self, - // cursor_position: DisplayPoint, - // style: EditorStyle, - // cx: &mut ViewContext, - // ) -> Option<(DisplayPoint, AnyElement)> { - // self.context_menu.read().as_ref().map(|menu| { - // menu.render( - // cursor_position, - // style, - // self.workspace.as_ref().map(|(w, _)| w.clone()), - // cx, - // ) - // }) - // } + pub fn render_context_menu( + &self, + cursor_position: DisplayPoint, + style: &EditorStyle, + cx: &mut ViewContext, + ) -> Option<(DisplayPoint, AnyElement)> { + self.context_menu.read().as_ref().map(|menu| { + menu.render( + cursor_position, + style, + self.workspace.as_ref().map(|(w, _)| w.clone()), + cx, + ) + }) + } fn hide_context_menu(&mut self, cx: &mut ViewContext) -> Option { cx.notify(); @@ -5954,29 +5898,29 @@ impl Editor { }); } - // pub fn context_menu_first(&mut self, _: &ContextMenuFirst, cx: &mut ViewContext) { - // if let Some(context_menu) = self.context_menu.write().as_mut() { - // context_menu.select_first(self.project.as_ref(), cx); - // } - // } + pub fn context_menu_first(&mut self, _: &ContextMenuFirst, cx: &mut ViewContext) { + if let Some(context_menu) = self.context_menu.write().as_mut() { + context_menu.select_first(self.project.as_ref(), cx); + } + } - // pub fn context_menu_prev(&mut self, _: &ContextMenuPrev, cx: &mut ViewContext) { - // if let Some(context_menu) = self.context_menu.write().as_mut() { - // context_menu.select_prev(self.project.as_ref(), cx); - // } - // } + pub fn context_menu_prev(&mut self, _: &ContextMenuPrev, cx: &mut ViewContext) { + if let Some(context_menu) = self.context_menu.write().as_mut() { + context_menu.select_prev(self.project.as_ref(), cx); + } + } - // pub fn context_menu_next(&mut self, _: &ContextMenuNext, cx: &mut ViewContext) { - // if let Some(context_menu) = self.context_menu.write().as_mut() { - // context_menu.select_next(self.project.as_ref(), cx); - // } - // } + pub fn context_menu_next(&mut self, _: &ContextMenuNext, cx: &mut ViewContext) { + if let Some(context_menu) = self.context_menu.write().as_mut() { + context_menu.select_next(self.project.as_ref(), cx); + } + } - // pub fn context_menu_last(&mut self, _: &ContextMenuLast, cx: &mut ViewContext) { - // if let Some(context_menu) = self.context_menu.write().as_mut() { - // context_menu.select_last(self.project.as_ref(), cx); - // } - // } + pub fn context_menu_last(&mut self, _: &ContextMenuLast, cx: &mut ViewContext) { + if let Some(context_menu) = self.context_menu.write().as_mut() { + context_menu.select_last(self.project.as_ref(), cx); + } + } pub fn move_to_previous_word_start( &mut self, diff --git a/crates/editor2/src/editor_tests.rs b/crates/editor2/src/editor_tests.rs index 5b5a40ba8e..0ba0158045 100644 --- a/crates/editor2/src/editor_tests.rs +++ b/crates/editor2/src/editor_tests.rs @@ -1,3 +1,7 @@ +use gpui::TestAppContext; +use language::language_settings::{AllLanguageSettings, AllLanguageSettingsContent}; +use settings::SettingsStore; + // use super::*; // use crate::{ // scroll::scroll_amount::ScrollAmount, @@ -8152,16 +8156,16 @@ // }); // } -// pub(crate) fn update_test_language_settings( -// cx: &mut TestAppContext, -// f: impl Fn(&mut AllLanguageSettingsContent), -// ) { -// cx.update(|cx| { -// cx.update_global::(|store, cx| { -// store.update_user_settings::(cx, f); -// }); -// }); -// } +pub(crate) fn update_test_language_settings( + cx: &mut TestAppContext, + f: impl Fn(&mut AllLanguageSettingsContent), +) { + cx.update(|cx| { + cx.update_global::(|store, cx| { + store.update_user_settings::(cx, f); + }); + }); +} // pub(crate) fn update_test_project_settings( // cx: &mut TestAppContext, diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 3e77a66936..67fcbaa4ba 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -15,7 +15,7 @@ use crate::{ use anyhow::Result; use collections::{BTreeMap, HashMap}; use gpui::{ - black, hsla, point, px, relative, size, transparent_black, Action, AnyElement, + black, hsla, point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace, BorrowAppContext, BorrowWindow, Bounds, ContentMask, Corners, DispatchContext, DispatchPhase, Edges, Element, ElementId, ElementInputHandler, Entity, FocusHandle, GlobalElementId, Hsla, InputHandler, KeyDownEvent, KeyListener, KeyMatch, Line, LineLayout, Modifiers, MouseButton, @@ -447,7 +447,7 @@ impl EditorElement { fn paint_gutter( &mut self, bounds: Bounds, - layout: &LayoutState, + layout: &mut LayoutState, editor: &mut Editor, cx: &mut ViewContext, ) { @@ -495,14 +495,21 @@ impl EditorElement { // } // } - // todo!("code actions indicator") - // if let Some((row, indicator)) = layout.code_actions_indicator.as_mut() { - // let mut x = 0.; - // let mut y = *row as f32 * line_height - scroll_top; - // x += ((layout.gutter_padding + layout.gutter_margin) - indicator.size().x) / 2.; - // y += (line_height - indicator.size().y) / 2.; - // indicator.paint(bounds.origin + point(x, y), visible_bounds, editor, cx); - // } + if let Some(indicator) = layout.code_actions_indicator.as_mut() { + let available_space = size( + AvailableSpace::MinContent, + AvailableSpace::Definite(line_height), + ); + let indicator_size = indicator.element.measure(available_space, editor, cx); + let mut x = Pixels::ZERO; + let mut y = indicator.row as f32 * line_height - scroll_top; + // Center indicator. + x += ((layout.gutter_padding + layout.gutter_margin) - indicator_size.width) / 2.; + y += (line_height - indicator_size.height) / 2.; + indicator + .element + .draw(bounds.origin + point(x, y), available_space, editor, cx); + } } fn paint_diff_hunks( @@ -596,7 +603,7 @@ impl EditorElement { fn paint_text( &mut self, bounds: Bounds, - layout: &LayoutState, + layout: &mut LayoutState, editor: &mut Editor, cx: &mut ViewContext, ) { @@ -787,48 +794,46 @@ impl EditorElement { ) } - cx.stack(0, |cx| { + cx.with_z_index(0, |cx| { for cursor in cursors { cursor.paint(content_origin, cx); } }); - // cx.scene().push_layer(Some(bounds)); - // cx.scene().pop_layer(); + if let Some((position, context_menu)) = layout.context_menu.as_mut() { + cx.with_z_index(1, |cx| { + let line_height = self.style.text.line_height_in_pixels(cx.rem_size()); + let available_space = size( + AvailableSpace::MinContent, + AvailableSpace::Definite( + (12. * line_height).min((bounds.size.height - line_height) / 2.), + ), + ); + let context_menu_size = context_menu.measure(available_space, editor, cx); - // if let Some((position, context_menu)) = layout.context_menu.as_mut() { - // cx.scene().push_stacking_context(None, None); - // let cursor_row_layout = - // &layout.position_map.line_layouts[(position.row() - start_row) as usize].line; - // let x = cursor_row_layout.x_for_index(position.column() as usize) - scroll_left; - // let y = (position.row() + 1) as f32 * layout.position_map.line_height - scroll_top; - // let mut list_origin = content_origin + point(x, y); - // let list_width = context_menu.size().x; - // let list_height = context_menu.size().y; + let cursor_row_layout = &layout.position_map.line_layouts + [(position.row() - start_row) as usize] + .line; + let x = cursor_row_layout.x_for_index(position.column() as usize) - scroll_left; + let y = + (position.row() + 1) as f32 * layout.position_map.line_height - scroll_top; + let mut list_origin = content_origin + point(x, y); + let list_width = context_menu_size.width; + let list_height = context_menu_size.height; - // // Snap the right edge of the list to the right edge of the window if - // // its horizontal bounds overflow. - // if list_origin.x + list_width > cx.window_size().x { - // list_origin.set_x((cx.window_size().x - list_width).max(0.)); - // } + // Snap the right edge of the list to the right edge of the window if + // its horizontal bounds overflow. + if list_origin.x + list_width > cx.viewport_size().width { + list_origin.x = (cx.viewport_size().width - list_width).max(Pixels::ZERO); + } - // if list_origin.y + list_height > bounds.max_y { - // list_origin - // .set_y(list_origin.y - layout.position_map.line_height - list_height); - // } + if list_origin.y + list_height > bounds.lower_right().y { + list_origin.y -= layout.position_map.line_height - list_height; + } - // context_menu.paint( - // list_origin, - // Bounds::::from_points( - // gpui::Point::::zero(), - // point(f32::MAX, f32::MAX), - // ), // Let content bleed outside of editor - // editor, - // cx, - // ); - - // cx.scene().pop_stacking_context(); - // } + context_menu.draw(list_origin, available_space, editor, cx); + }) + } // if let Some((position, hover_popovers)) = layout.hover_popovers.as_mut() { // cx.scene().push_stacking_context(None, None); @@ -1774,26 +1779,28 @@ impl EditorElement { snapshot = editor.snapshot(cx); } - // todo!("context menu") - // let mut context_menu = None; - // let mut code_actions_indicator = None; - // if let Some(newest_selection_head) = newest_selection_head { - // if (start_row..end_row).contains(&newest_selection_head.row()) { - // if editor.context_menu_visible() { - // context_menu = - // editor.render_context_menu(newest_selection_head, style.clone(), cx); - // } + let mut context_menu = None; + let mut code_actions_indicator = None; + if let Some(newest_selection_head) = newest_selection_head { + if (start_row..end_row).contains(&newest_selection_head.row()) { + if editor.context_menu_visible() { + context_menu = + editor.render_context_menu(newest_selection_head, &self.style, cx); + } - // let active = matches!( - // editor.context_menu.read().as_ref(), - // Some(crate::ContextMenu::CodeActions(_)) - // ); + let active = matches!( + editor.context_menu.read().as_ref(), + Some(crate::ContextMenu::CodeActions(_)) + ); - // code_actions_indicator = editor - // .render_code_actions_indicator(&style, active, cx) - // .map(|indicator| (newest_selection_head.row(), indicator)); - // } - // } + code_actions_indicator = editor + .render_code_actions_indicator(&style, active, cx) + .map(|element| CodeActionsIndicator { + row: newest_selection_head.row(), + element, + }); + } + } let visible_rows = start_row..start_row + line_layouts.len() as u32; // todo!("hover") @@ -1831,18 +1838,6 @@ impl EditorElement { // ); // } - // todo!("code actions") - // if let Some((_, indicator)) = code_actions_indicator.as_mut() { - // indicator.layout( - // SizeConstraint::strict_along( - // Axis::Vertical, - // line_height * style.code_actions.vertical_scale, - // ), - // editor, - // cx, - // ); - // } - // todo!("fold indicators") // for fold_indicator in fold_indicators.iter_mut() { // if let Some(indicator) = fold_indicator.as_mut() { @@ -1941,8 +1936,8 @@ impl EditorElement { display_hunks, // blocks, selections, - // context_menu, - // code_actions_indicator, + context_menu, + code_actions_indicator, // fold_indicators, tab_invisible, space_invisible, @@ -2493,7 +2488,7 @@ impl Element for EditorElement { element_state: &mut Self::ElementState, cx: &mut gpui::ViewContext, ) { - let layout = self.compute_layout(editor, cx, bounds); + let mut layout = self.compute_layout(editor, cx, bounds); let gutter_bounds = Bounds { origin: bounds.origin, size: layout.gutter_size, @@ -2503,21 +2498,24 @@ impl Element for EditorElement { size: layout.text_size, }; - cx.with_content_mask(ContentMask { bounds }, |cx| { - self.paint_mouse_listeners( - bounds, - gutter_bounds, - text_bounds, - &layout.position_map, - cx, - ); - self.paint_background(gutter_bounds, text_bounds, &layout, cx); - if layout.gutter_size.width > Pixels::ZERO { - self.paint_gutter(gutter_bounds, &layout, editor, cx); - } - self.paint_text(text_bounds, &layout, editor, cx); - let input_handler = ElementInputHandler::new(bounds, cx); - cx.handle_input(&editor.focus_handle, input_handler); + // We call with_z_index to establish a new stacking context. + cx.with_z_index(0, |cx| { + cx.with_content_mask(ContentMask { bounds }, |cx| { + self.paint_mouse_listeners( + bounds, + gutter_bounds, + text_bounds, + &layout.position_map, + cx, + ); + self.paint_background(gutter_bounds, text_bounds, &layout, cx); + if layout.gutter_size.width > Pixels::ZERO { + self.paint_gutter(gutter_bounds, &mut layout, editor, cx); + } + self.paint_text(text_bounds, &mut layout, editor, cx); + let input_handler = ElementInputHandler::new(bounds, cx); + cx.handle_input(&editor.focus_handle, input_handler); + }); }); } } @@ -3143,14 +3141,19 @@ pub struct LayoutState { show_scrollbars: bool, is_singleton: bool, max_row: u32, - // context_menu: Option<(DisplayPoint, AnyElement)>, - // code_actions_indicator: Option<(u32, AnyElement)>, + context_menu: Option<(DisplayPoint, AnyElement)>, + code_actions_indicator: Option, // hover_popovers: Option<(DisplayPoint, Vec>)>, // fold_indicators: Vec>>, tab_invisible: Line, space_invisible: Line, } +struct CodeActionsIndicator { + row: u32, + element: AnyElement, +} + struct PositionMap { size: Size, line_height: Pixels, @@ -4123,7 +4126,7 @@ fn build_key_listeners( build_action_listener(Editor::unfold_at), build_action_listener(Editor::fold_selected_ranges), build_action_listener(Editor::show_completions), - // build_action_listener(Editor::toggle_code_actions), todo!() + build_action_listener(Editor::toggle_code_actions), // build_action_listener(Editor::open_excerpts), todo!() build_action_listener(Editor::toggle_soft_wrap), build_action_listener(Editor::toggle_inlay_hints), @@ -4139,13 +4142,21 @@ fn build_key_listeners( build_action_listener(Editor::restart_language_server), build_action_listener(Editor::show_character_palette), // build_action_listener(Editor::confirm_completion), todo!() - // build_action_listener(Editor::confirm_code_action), todo!() + build_action_listener(|editor, action, cx| { + editor + .confirm_code_action(action, cx) + .map(|task| task.detach_and_log_err(cx)); + }), // build_action_listener(Editor::rename), todo!() // build_action_listener(Editor::confirm_rename), todo!() // build_action_listener(Editor::find_all_references), todo!() build_action_listener(Editor::next_copilot_suggestion), build_action_listener(Editor::previous_copilot_suggestion), build_action_listener(Editor::copilot_suggest), + build_action_listener(Editor::context_menu_first), + build_action_listener(Editor::context_menu_prev), + build_action_listener(Editor::context_menu_next), + build_action_listener(Editor::context_menu_last), build_key_listener( move |editor, key_down: &KeyDownEvent, dispatch_context, phase, cx| { if phase == DispatchPhase::Bubble { diff --git a/crates/editor2/src/inlay_hint_cache.rs b/crates/editor2/src/inlay_hint_cache.rs index addd3bf3ac..af9febf376 100644 --- a/crates/editor2/src/inlay_hint_cache.rs +++ b/crates/editor2/src/inlay_hint_cache.rs @@ -553,18 +553,17 @@ impl InlayHintCache { let mut resolved_hint = resolved_hint_task.await.context("hint resolve task")?; editor.update(&mut cx, |editor, _| { - todo!() - // if let Some(excerpt_hints) = - // editor.inlay_hint_cache.hints.get(&excerpt_id) - // { - // let mut guard = excerpt_hints.write(); - // if let Some(cached_hint) = guard.hints_by_id.get_mut(&id) { - // if cached_hint.resolve_state == ResolveState::Resolving { - // resolved_hint.resolve_state = ResolveState::Resolved; - // *cached_hint = resolved_hint; - // } - // } - // } + if let Some(excerpt_hints) = + editor.inlay_hint_cache.hints.get(&excerpt_id) + { + let mut guard = excerpt_hints.write(); + if let Some(cached_hint) = guard.hints_by_id.get_mut(&id) { + if cached_hint.resolve_state == ResolveState::Resolving { + resolved_hint.resolve_state = ResolveState::Resolved; + *cached_hint = resolved_hint; + } + } + } })?; } @@ -585,91 +584,89 @@ fn spawn_new_update_tasks( update_cache_version: usize, cx: &mut ViewContext<'_, Editor>, ) { - todo!("old version below"); + let visible_hints = Arc::new(editor.visible_inlay_hints(cx)); + for (excerpt_id, (excerpt_buffer, new_task_buffer_version, excerpt_visible_range)) in + excerpts_to_query + { + if excerpt_visible_range.is_empty() { + continue; + } + let buffer = excerpt_buffer.read(cx); + let buffer_id = buffer.remote_id(); + let buffer_snapshot = buffer.snapshot(); + if buffer_snapshot + .version() + .changed_since(&new_task_buffer_version) + { + continue; + } + + let cached_excerpt_hints = editor.inlay_hint_cache.hints.get(&excerpt_id).cloned(); + if let Some(cached_excerpt_hints) = &cached_excerpt_hints { + let cached_excerpt_hints = cached_excerpt_hints.read(); + let cached_buffer_version = &cached_excerpt_hints.buffer_version; + if cached_excerpt_hints.version > update_cache_version + || cached_buffer_version.changed_since(&new_task_buffer_version) + { + continue; + } + }; + + let (multi_buffer_snapshot, Some(query_ranges)) = + editor.buffer.update(cx, |multi_buffer, cx| { + ( + multi_buffer.snapshot(cx), + determine_query_ranges( + multi_buffer, + excerpt_id, + &excerpt_buffer, + excerpt_visible_range, + cx, + ), + ) + }) + else { + return; + }; + let query = ExcerptQuery { + buffer_id, + excerpt_id, + cache_version: update_cache_version, + invalidate, + reason, + }; + + let new_update_task = |query_ranges| { + new_update_task( + query, + query_ranges, + multi_buffer_snapshot, + buffer_snapshot.clone(), + Arc::clone(&visible_hints), + cached_excerpt_hints, + Arc::clone(&editor.inlay_hint_cache.lsp_request_limiter), + cx, + ) + }; + + match editor.inlay_hint_cache.update_tasks.entry(excerpt_id) { + hash_map::Entry::Occupied(mut o) => { + o.get_mut().update_cached_tasks( + &buffer_snapshot, + query_ranges, + invalidate, + new_update_task, + ); + } + hash_map::Entry::Vacant(v) => { + v.insert(TasksForRanges::new( + query_ranges.clone(), + new_update_task(query_ranges), + )); + } + } + } } -// let visible_hints = Arc::new(editor.visible_inlay_hints(cx)); -// for (excerpt_id, (excerpt_buffer, new_task_buffer_version, excerpt_visible_range)) in -// excerpts_to_query -// { -// if excerpt_visible_range.is_empty() { -// continue; -// } -// let buffer = excerpt_buffer.read(cx); -// let buffer_id = buffer.remote_id(); -// let buffer_snapshot = buffer.snapshot(); -// if buffer_snapshot -// .version() -// .changed_since(&new_task_buffer_version) -// { -// continue; -// } - -// let cached_excerpt_hints = editor.inlay_hint_cache.hints.get(&excerpt_id).cloned(); -// if let Some(cached_excerpt_hints) = &cached_excerpt_hints { -// let cached_excerpt_hints = cached_excerpt_hints.read(); -// let cached_buffer_version = &cached_excerpt_hints.buffer_version; -// if cached_excerpt_hints.version > update_cache_version -// || cached_buffer_version.changed_since(&new_task_buffer_version) -// { -// continue; -// } -// }; - -// let (multi_buffer_snapshot, Some(query_ranges)) = -// editor.buffer.update(cx, |multi_buffer, cx| { -// ( -// multi_buffer.snapshot(cx), -// determine_query_ranges( -// multi_buffer, -// excerpt_id, -// &excerpt_buffer, -// excerpt_visible_range, -// cx, -// ), -// ) -// }) -// else { -// return; -// }; -// let query = ExcerptQuery { -// buffer_id, -// excerpt_id, -// cache_version: update_cache_version, -// invalidate, -// reason, -// }; - -// let new_update_task = |query_ranges| { -// new_update_task( -// query, -// query_ranges, -// multi_buffer_snapshot, -// buffer_snapshot.clone(), -// Arc::clone(&visible_hints), -// cached_excerpt_hints, -// Arc::clone(&editor.inlay_hint_cache.lsp_request_limiter), -// cx, -// ) -// }; - -// match editor.inlay_hint_cache.update_tasks.entry(excerpt_id) { -// hash_map::Entry::Occupied(mut o) => { -// o.get_mut().update_cached_tasks( -// &buffer_snapshot, -// query_ranges, -// invalidate, -// new_update_task, -// ); -// } -// hash_map::Entry::Vacant(v) => { -// v.insert(TasksForRanges::new( -// query_ranges.clone(), -// new_update_task(query_ranges), -// )); -// } -// } -// } -// } #[derive(Debug, Clone)] struct QueryRanges { @@ -765,209 +762,208 @@ fn new_update_task( lsp_request_limiter: Arc, cx: &mut ViewContext<'_, Editor>, ) -> Task<()> { - todo!() - // cx.spawn(|editor, mut cx| async move { - // let closure_cx = cx.clone(); - // let fetch_and_update_hints = |invalidate, range| { - // fetch_and_update_hints( - // editor.clone(), - // multi_buffer_snapshot.clone(), - // buffer_snapshot.clone(), - // Arc::clone(&visible_hints), - // cached_excerpt_hints.as_ref().map(Arc::clone), - // query, - // invalidate, - // range, - // Arc::clone(&lsp_request_limiter), - // closure_cx.clone(), - // ) - // }; - // let visible_range_update_results = future::join_all(query_ranges.visible.into_iter().map( - // |visible_range| async move { - // ( - // visible_range.clone(), - // fetch_and_update_hints(query.invalidate.should_invalidate(), visible_range) - // .await, - // ) - // }, - // )) - // .await; + cx.spawn(|editor, mut cx| async move { + let closure_cx = cx.clone(); + let fetch_and_update_hints = |invalidate, range| { + fetch_and_update_hints( + editor.clone(), + multi_buffer_snapshot.clone(), + buffer_snapshot.clone(), + Arc::clone(&visible_hints), + cached_excerpt_hints.as_ref().map(Arc::clone), + query, + invalidate, + range, + Arc::clone(&lsp_request_limiter), + closure_cx.clone(), + ) + }; + let visible_range_update_results = future::join_all(query_ranges.visible.into_iter().map( + |visible_range| async move { + ( + visible_range.clone(), + fetch_and_update_hints(query.invalidate.should_invalidate(), visible_range) + .await, + ) + }, + )) + .await; - // let hint_delay = cx.background().timer(Duration::from_millis( - // INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS, - // )); + let hint_delay = cx.background_executor().timer(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS, + )); - // let mut query_range_failed = |range: &Range, e: anyhow::Error| { - // log::error!("inlay hint update task for range {range:?} failed: {e:#}"); - // editor - // .update(&mut cx, |editor, _| { - // if let Some(task_ranges) = editor - // .inlay_hint_cache - // .update_tasks - // .get_mut(&query.excerpt_id) - // { - // task_ranges.invalidate_range(&buffer_snapshot, &range); - // } - // }) - // .ok() - // }; + let mut query_range_failed = |range: &Range, e: anyhow::Error| { + log::error!("inlay hint update task for range {range:?} failed: {e:#}"); + editor + .update(&mut cx, |editor, _| { + if let Some(task_ranges) = editor + .inlay_hint_cache + .update_tasks + .get_mut(&query.excerpt_id) + { + task_ranges.invalidate_range(&buffer_snapshot, &range); + } + }) + .ok() + }; - // for (range, result) in visible_range_update_results { - // if let Err(e) = result { - // query_range_failed(&range, e); - // } - // } + for (range, result) in visible_range_update_results { + if let Err(e) = result { + query_range_failed(&range, e); + } + } - // hint_delay.await; - // let invisible_range_update_results = future::join_all( - // query_ranges - // .before_visible - // .into_iter() - // .chain(query_ranges.after_visible.into_iter()) - // .map(|invisible_range| async move { - // ( - // invisible_range.clone(), - // fetch_and_update_hints(false, invisible_range).await, - // ) - // }), - // ) - // .await; - // for (range, result) in invisible_range_update_results { - // if let Err(e) = result { - // query_range_failed(&range, e); - // } - // } - // }) + hint_delay.await; + let invisible_range_update_results = future::join_all( + query_ranges + .before_visible + .into_iter() + .chain(query_ranges.after_visible.into_iter()) + .map(|invisible_range| async move { + ( + invisible_range.clone(), + fetch_and_update_hints(false, invisible_range).await, + ) + }), + ) + .await; + for (range, result) in invisible_range_update_results { + if let Err(e) = result { + query_range_failed(&range, e); + } + } + }) } -// async fn fetch_and_update_hints( -// editor: gpui::WeakView, -// multi_buffer_snapshot: MultiBufferSnapshot, -// buffer_snapshot: BufferSnapshot, -// visible_hints: Arc>, -// cached_excerpt_hints: Option>>, -// query: ExcerptQuery, -// invalidate: bool, -// fetch_range: Range, -// lsp_request_limiter: Arc, -// mut cx: gpui::AsyncAppContext, -// ) -> anyhow::Result<()> { -// let (lsp_request_guard, got_throttled) = if query.invalidate.should_invalidate() { -// (None, false) -// } else { -// match lsp_request_limiter.try_acquire() { -// Some(guard) => (Some(guard), false), -// None => (Some(lsp_request_limiter.acquire().await), true), -// } -// }; -// let fetch_range_to_log = -// fetch_range.start.to_point(&buffer_snapshot)..fetch_range.end.to_point(&buffer_snapshot); -// let inlay_hints_fetch_task = editor -// .update(&mut cx, |editor, cx| { -// if got_throttled { -// let query_not_around_visible_range = match editor.excerpt_visible_offsets(None, cx).remove(&query.excerpt_id) { -// Some((_, _, current_visible_range)) => { -// let visible_offset_length = current_visible_range.len(); -// let double_visible_range = current_visible_range -// .start -// .saturating_sub(visible_offset_length) -// ..current_visible_range -// .end -// .saturating_add(visible_offset_length) -// .min(buffer_snapshot.len()); -// !double_visible_range -// .contains(&fetch_range.start.to_offset(&buffer_snapshot)) -// && !double_visible_range -// .contains(&fetch_range.end.to_offset(&buffer_snapshot)) -// }, -// None => true, -// }; -// if query_not_around_visible_range { -// log::trace!("Fetching inlay hints for range {fetch_range_to_log:?} got throttled and fell off the current visible range, skipping."); -// if let Some(task_ranges) = editor -// .inlay_hint_cache -// .update_tasks -// .get_mut(&query.excerpt_id) -// { -// task_ranges.invalidate_range(&buffer_snapshot, &fetch_range); -// } -// return None; -// } -// } -// editor -// .buffer() -// .read(cx) -// .buffer(query.buffer_id) -// .and_then(|buffer| { -// let project = editor.project.as_ref()?; -// Some(project.update(cx, |project, cx| { -// project.inlay_hints(buffer, fetch_range.clone(), cx) -// })) -// }) -// }) -// .ok() -// .flatten(); -// let new_hints = match inlay_hints_fetch_task { -// Some(fetch_task) => { -// log::debug!( -// "Fetching inlay hints for range {fetch_range_to_log:?}, reason: {query_reason}, invalidate: {invalidate}", -// query_reason = query.reason, -// ); -// log::trace!( -// "Currently visible hints: {visible_hints:?}, cached hints present: {}", -// cached_excerpt_hints.is_some(), -// ); -// fetch_task.await.context("inlay hint fetch task")? -// } -// None => return Ok(()), -// }; -// drop(lsp_request_guard); -// log::debug!( -// "Fetched {} hints for range {fetch_range_to_log:?}", -// new_hints.len() -// ); -// log::trace!("Fetched hints: {new_hints:?}"); +async fn fetch_and_update_hints( + editor: gpui::WeakView, + multi_buffer_snapshot: MultiBufferSnapshot, + buffer_snapshot: BufferSnapshot, + visible_hints: Arc>, + cached_excerpt_hints: Option>>, + query: ExcerptQuery, + invalidate: bool, + fetch_range: Range, + lsp_request_limiter: Arc, + mut cx: gpui::AsyncWindowContext, +) -> anyhow::Result<()> { + let (lsp_request_guard, got_throttled) = if query.invalidate.should_invalidate() { + (None, false) + } else { + match lsp_request_limiter.try_acquire() { + Some(guard) => (Some(guard), false), + None => (Some(lsp_request_limiter.acquire().await), true), + } + }; + let fetch_range_to_log = + fetch_range.start.to_point(&buffer_snapshot)..fetch_range.end.to_point(&buffer_snapshot); + let inlay_hints_fetch_task = editor + .update(&mut cx, |editor, cx| { + if got_throttled { + let query_not_around_visible_range = match editor.excerpt_visible_offsets(None, cx).remove(&query.excerpt_id) { + Some((_, _, current_visible_range)) => { + let visible_offset_length = current_visible_range.len(); + let double_visible_range = current_visible_range + .start + .saturating_sub(visible_offset_length) + ..current_visible_range + .end + .saturating_add(visible_offset_length) + .min(buffer_snapshot.len()); + !double_visible_range + .contains(&fetch_range.start.to_offset(&buffer_snapshot)) + && !double_visible_range + .contains(&fetch_range.end.to_offset(&buffer_snapshot)) + }, + None => true, + }; + if query_not_around_visible_range { + log::trace!("Fetching inlay hints for range {fetch_range_to_log:?} got throttled and fell off the current visible range, skipping."); + if let Some(task_ranges) = editor + .inlay_hint_cache + .update_tasks + .get_mut(&query.excerpt_id) + { + task_ranges.invalidate_range(&buffer_snapshot, &fetch_range); + } + return None; + } + } + editor + .buffer() + .read(cx) + .buffer(query.buffer_id) + .and_then(|buffer| { + let project = editor.project.as_ref()?; + Some(project.update(cx, |project, cx| { + project.inlay_hints(buffer, fetch_range.clone(), cx) + })) + }) + }) + .ok() + .flatten(); + let new_hints = match inlay_hints_fetch_task { + Some(fetch_task) => { + log::debug!( + "Fetching inlay hints for range {fetch_range_to_log:?}, reason: {query_reason}, invalidate: {invalidate}", + query_reason = query.reason, + ); + log::trace!( + "Currently visible hints: {visible_hints:?}, cached hints present: {}", + cached_excerpt_hints.is_some(), + ); + fetch_task.await.context("inlay hint fetch task")? + } + None => return Ok(()), + }; + drop(lsp_request_guard); + log::debug!( + "Fetched {} hints for range {fetch_range_to_log:?}", + new_hints.len() + ); + log::trace!("Fetched hints: {new_hints:?}"); -// let background_task_buffer_snapshot = buffer_snapshot.clone(); -// let backround_fetch_range = fetch_range.clone(); -// let new_update = cx -// .background() -// .spawn(async move { -// calculate_hint_updates( -// query.excerpt_id, -// invalidate, -// backround_fetch_range, -// new_hints, -// &background_task_buffer_snapshot, -// cached_excerpt_hints, -// &visible_hints, -// ) -// }) -// .await; -// if let Some(new_update) = new_update { -// log::debug!( -// "Applying update for range {fetch_range_to_log:?}: remove from editor: {}, remove from cache: {}, add to cache: {}", -// new_update.remove_from_visible.len(), -// new_update.remove_from_cache.len(), -// new_update.add_to_cache.len() -// ); -// log::trace!("New update: {new_update:?}"); -// editor -// .update(&mut cx, |editor, cx| { -// apply_hint_update( -// editor, -// new_update, -// query, -// invalidate, -// buffer_snapshot, -// multi_buffer_snapshot, -// cx, -// ); -// }) -// .ok(); -// } -// Ok(()) -// } + let background_task_buffer_snapshot = buffer_snapshot.clone(); + let backround_fetch_range = fetch_range.clone(); + let new_update = cx + .background_executor() + .spawn(async move { + calculate_hint_updates( + query.excerpt_id, + invalidate, + backround_fetch_range, + new_hints, + &background_task_buffer_snapshot, + cached_excerpt_hints, + &visible_hints, + ) + }) + .await; + if let Some(new_update) = new_update { + log::debug!( + "Applying update for range {fetch_range_to_log:?}: remove from editor: {}, remove from cache: {}, add to cache: {}", + new_update.remove_from_visible.len(), + new_update.remove_from_cache.len(), + new_update.add_to_cache.len() + ); + log::trace!("New update: {new_update:?}"); + editor + .update(&mut cx, |editor, cx| { + apply_hint_update( + editor, + new_update, + query, + invalidate, + buffer_snapshot, + multi_buffer_snapshot, + cx, + ); + }) + .ok(); + } + Ok(()) +} fn calculate_hint_updates( excerpt_id: ExcerptId, @@ -1077,2279 +1073,2196 @@ fn apply_hint_update( multi_buffer_snapshot: MultiBufferSnapshot, cx: &mut ViewContext<'_, Editor>, ) { - todo!("old implementation commented below") + let cached_excerpt_hints = editor + .inlay_hint_cache + .hints + .entry(new_update.excerpt_id) + .or_insert_with(|| { + Arc::new(RwLock::new(CachedExcerptHints { + version: query.cache_version, + buffer_version: buffer_snapshot.version().clone(), + buffer_id: query.buffer_id, + ordered_hints: Vec::new(), + hints_by_id: HashMap::default(), + })) + }); + let mut cached_excerpt_hints = cached_excerpt_hints.write(); + match query.cache_version.cmp(&cached_excerpt_hints.version) { + cmp::Ordering::Less => return, + cmp::Ordering::Greater | cmp::Ordering::Equal => { + cached_excerpt_hints.version = query.cache_version; + } + } + + let mut cached_inlays_changed = !new_update.remove_from_cache.is_empty(); + cached_excerpt_hints + .ordered_hints + .retain(|hint_id| !new_update.remove_from_cache.contains(hint_id)); + cached_excerpt_hints + .hints_by_id + .retain(|hint_id, _| !new_update.remove_from_cache.contains(hint_id)); + let mut splice = InlaySplice { + to_remove: new_update.remove_from_visible, + to_insert: Vec::new(), + }; + for new_hint in new_update.add_to_cache { + let insert_position = match cached_excerpt_hints + .ordered_hints + .binary_search_by(|probe| { + cached_excerpt_hints.hints_by_id[probe] + .position + .cmp(&new_hint.position, &buffer_snapshot) + }) { + Ok(i) => { + let mut insert_position = Some(i); + for id in &cached_excerpt_hints.ordered_hints[i..] { + let cached_hint = &cached_excerpt_hints.hints_by_id[id]; + if new_hint + .position + .cmp(&cached_hint.position, &buffer_snapshot) + .is_gt() + { + break; + } + if cached_hint.text() == new_hint.text() { + insert_position = None; + break; + } + } + insert_position + } + Err(i) => Some(i), + }; + + if let Some(insert_position) = insert_position { + let new_inlay_id = post_inc(&mut editor.next_inlay_id); + if editor + .inlay_hint_cache + .allowed_hint_kinds + .contains(&new_hint.kind) + { + let new_hint_position = + multi_buffer_snapshot.anchor_in_excerpt(query.excerpt_id, new_hint.position); + splice + .to_insert + .push(Inlay::hint(new_inlay_id, new_hint_position, &new_hint)); + } + let new_id = InlayId::Hint(new_inlay_id); + cached_excerpt_hints.hints_by_id.insert(new_id, new_hint); + cached_excerpt_hints + .ordered_hints + .insert(insert_position, new_id); + cached_inlays_changed = true; + } + } + cached_excerpt_hints.buffer_version = buffer_snapshot.version().clone(); + drop(cached_excerpt_hints); + + if invalidate { + let mut outdated_excerpt_caches = HashSet::default(); + for (excerpt_id, excerpt_hints) in &editor.inlay_hint_cache().hints { + let excerpt_hints = excerpt_hints.read(); + if excerpt_hints.buffer_id == query.buffer_id + && excerpt_id != &query.excerpt_id + && buffer_snapshot + .version() + .changed_since(&excerpt_hints.buffer_version) + { + outdated_excerpt_caches.insert(*excerpt_id); + splice + .to_remove + .extend(excerpt_hints.ordered_hints.iter().copied()); + } + } + cached_inlays_changed |= !outdated_excerpt_caches.is_empty(); + editor + .inlay_hint_cache + .hints + .retain(|excerpt_id, _| !outdated_excerpt_caches.contains(excerpt_id)); + } + + let InlaySplice { + to_remove, + to_insert, + } = splice; + let displayed_inlays_changed = !to_remove.is_empty() || !to_insert.is_empty(); + if cached_inlays_changed || displayed_inlays_changed { + editor.inlay_hint_cache.version += 1; + } + if displayed_inlays_changed { + editor.splice_inlay_hints(to_remove, to_insert, cx) + } +} + +#[cfg(test)] +pub mod tests { + use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering}; + + use crate::{ + scroll::{autoscroll::Autoscroll, scroll_amount::ScrollAmount}, + ExcerptRange, + }; + use futures::StreamExt; + use gpui::{Context, TestAppContext, View, WindowHandle}; + use itertools::Itertools; + use language::{ + language_settings::AllLanguageSettingsContent, FakeLspAdapter, Language, LanguageConfig, + }; + use lsp::FakeLanguageServer; + use parking_lot::Mutex; + use project::{FakeFs, Project}; + use serde_json::json; + use settings::SettingsStore; + use text::{Point, ToPoint}; + use workspace::Workspace; + + use crate::editor_tests::update_test_language_settings; + + use super::*; + + #[gpui::test] + async fn test_basic_cache_update_with_duplicate_hints(cx: &mut gpui::TestAppContext) { + let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]); + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), + show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)), + show_other_hints: allowed_hint_kinds.contains(&None), + }) + }); + + let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; + let lsp_request_count = Arc::new(AtomicU32::new(0)); + fake_server + .handle_request::(move |params, _| { + let task_lsp_request_count = Arc::clone(&lsp_request_count); + async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path(file_with_hints).unwrap(), + ); + let current_call_id = + Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst); + let mut new_hints = Vec::with_capacity(2 * current_call_id as usize); + for _ in 0..2 { + let mut i = current_call_id; + loop { + new_hints.push(lsp::InlayHint { + position: lsp::Position::new(0, i), + label: lsp::InlayHintLabel::String(i.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }); + if i == 0 { + break; + } + i -= 1; + } + } + + Ok(Some(new_hints)) + } + }) + .next() + .await; + cx.executor().run_until_parked(); + + let mut edits_made = 1; + editor.update(cx, |editor, cx| { + let expected_hints = vec!["0".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Should get its first hints when opening the editor" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.allowed_hint_kinds, allowed_hint_kinds, + "Cache should use editor settings to get the allowed hint kinds" + ); + assert_eq!( + inlay_cache.version, edits_made, + "The editor update the cache version after every cache/view change" + ); + }); + + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([13..13])); + editor.handle_input("some change", cx); + edits_made += 1; + }); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_hints = vec!["0".to_string(), "1".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Should get new hints after an edit" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.allowed_hint_kinds, allowed_hint_kinds, + "Cache should use editor settings to get the allowed hint kinds" + ); + assert_eq!( + inlay_cache.version, edits_made, + "The editor update the cache version after every cache/view change" + ); + }); + + fake_server + .request::(()) + .await + .expect("inlay refresh request failed"); + edits_made += 1; + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_hints = vec!["0".to_string(), "1".to_string(), "2".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Should get new hints after hint refresh/ request" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.allowed_hint_kinds, allowed_hint_kinds, + "Cache should use editor settings to get the allowed hint kinds" + ); + assert_eq!( + inlay_cache.version, edits_made, + "The editor update the cache version after every cache/view change" + ); + }); + } + + #[gpui::test] + async fn test_cache_update_on_lsp_completion_tasks(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + + let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; + let lsp_request_count = Arc::new(AtomicU32::new(0)); + fake_server + .handle_request::(move |params, _| { + let task_lsp_request_count = Arc::clone(&lsp_request_count); + async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path(file_with_hints).unwrap(), + ); + let current_call_id = + Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst); + Ok(Some(vec![lsp::InlayHint { + position: lsp::Position::new(0, current_call_id), + label: lsp::InlayHintLabel::String(current_call_id.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) + } + }) + .next() + .await; + cx.executor().run_until_parked(); + + let mut edits_made = 1; + editor.update(cx, |editor, cx| { + let expected_hints = vec!["0".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Should get its first hints when opening the editor" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!( + editor.inlay_hint_cache().version, + edits_made, + "The editor update the cache version after every cache/view change" + ); + }); + + let progress_token = "test_progress_token"; + fake_server + .request::(lsp::WorkDoneProgressCreateParams { + token: lsp::ProgressToken::String(progress_token.to_string()), + }) + .await + .expect("work done progress create request failed"); + cx.executor().run_until_parked(); + fake_server.notify::(lsp::ProgressParams { + token: lsp::ProgressToken::String(progress_token.to_string()), + value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin( + lsp::WorkDoneProgressBegin::default(), + )), + }); + cx.executor().run_until_parked(); + + editor.update(cx, |editor, cx| { + let expected_hints = vec!["0".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Should not update hints while the work task is running" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!( + editor.inlay_hint_cache().version, + edits_made, + "Should not update the cache while the work task is running" + ); + }); + + fake_server.notify::(lsp::ProgressParams { + token: lsp::ProgressToken::String(progress_token.to_string()), + value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End( + lsp::WorkDoneProgressEnd::default(), + )), + }); + cx.executor().run_until_parked(); + + edits_made += 1; + editor.update(cx, |editor, cx| { + let expected_hints = vec!["1".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "New hints should be queried after the work task is done" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!( + editor.inlay_hint_cache().version, + edits_made, + "Cache version should udpate once after the work task is done" + ); + }); + } + + #[gpui::test] + async fn test_no_hint_updates_for_unrelated_language_files(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/a", + json!({ + "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out", + "other.md": "Test md file with some text", + }), + ) + .await; + let project = Project::test(fs, ["/a".as_ref()], cx).await; + + let mut rs_fake_servers = None; + let mut md_fake_servers = None; + for (name, path_suffix) in [("Rust", "rs"), ("Markdown", "md")] { + let mut language = Language::new( + LanguageConfig { + name: name.into(), + path_suffixes: vec![path_suffix.to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + name, + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + match name { + "Rust" => rs_fake_servers = Some(fake_servers), + "Markdown" => md_fake_servers = Some(fake_servers), + _ => unreachable!(), + } + project.update(cx, |project, _| { + project.languages().add(Arc::new(language)); + }); + } + + let rs_buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/a/main.rs", cx) + }) + .await + .unwrap(); + cx.executor().run_until_parked(); + cx.executor().start_waiting(); + let rs_fake_server = rs_fake_servers.unwrap().next().await.unwrap(); + let rs_editor = + cx.add_window(|cx| Editor::for_buffer(rs_buffer, Some(project.clone()), cx)); + let rs_lsp_request_count = Arc::new(AtomicU32::new(0)); + rs_fake_server + .handle_request::(move |params, _| { + let task_lsp_request_count = Arc::clone(&rs_lsp_request_count); + async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst); + Ok(Some(vec![lsp::InlayHint { + position: lsp::Position::new(0, i), + label: lsp::InlayHintLabel::String(i.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) + } + }) + .next() + .await; + cx.executor().run_until_parked(); + rs_editor.update(cx, |editor, cx| { + let expected_hints = vec!["0".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Should get its first hints when opening the editor" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!( + editor.inlay_hint_cache().version, + 1, + "Rust editor update the cache version after every cache/view change" + ); + }); + + cx.executor().run_until_parked(); + let md_buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/a/other.md", cx) + }) + .await + .unwrap(); + cx.executor().run_until_parked(); + cx.executor().start_waiting(); + let md_fake_server = md_fake_servers.unwrap().next().await.unwrap(); + let md_editor = cx.add_window(|cx| Editor::for_buffer(md_buffer, Some(project), cx)); + let md_lsp_request_count = Arc::new(AtomicU32::new(0)); + md_fake_server + .handle_request::(move |params, _| { + let task_lsp_request_count = Arc::clone(&md_lsp_request_count); + async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/a/other.md").unwrap(), + ); + let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst); + Ok(Some(vec![lsp::InlayHint { + position: lsp::Position::new(0, i), + label: lsp::InlayHintLabel::String(i.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) + } + }) + .next() + .await; + cx.executor().run_until_parked(); + md_editor.update(cx, |editor, cx| { + let expected_hints = vec!["0".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Markdown editor should have a separate verison, repeating Rust editor rules" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, 1); + }); + + rs_editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([13..13])); + editor.handle_input("some rs change", cx); + }); + cx.executor().run_until_parked(); + rs_editor.update(cx, |editor, cx| { + let expected_hints = vec!["1".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Rust inlay cache should change after the edit" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!( + editor.inlay_hint_cache().version, + 2, + "Every time hint cache changes, cache version should be incremented" + ); + }); + md_editor.update(cx, |editor, cx| { + let expected_hints = vec!["0".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Markdown editor should not be affected by Rust editor changes" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, 1); + }); + + md_editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([13..13])); + editor.handle_input("some md change", cx); + }); + cx.executor().run_until_parked(); + md_editor.update(cx, |editor, cx| { + let expected_hints = vec!["1".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Rust editor should not be affected by Markdown editor changes" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, 2); + }); + rs_editor.update(cx, |editor, cx| { + let expected_hints = vec!["1".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Markdown editor should also change independently" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, 2); + }); + } + + #[gpui::test] + async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) { + let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]); + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), + show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)), + show_other_hints: allowed_hint_kinds.contains(&None), + }) + }); + + let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; + let lsp_request_count = Arc::new(AtomicU32::new(0)); + let another_lsp_request_count = Arc::clone(&lsp_request_count); + fake_server + .handle_request::(move |params, _| { + let task_lsp_request_count = Arc::clone(&another_lsp_request_count); + async move { + Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst); + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path(file_with_hints).unwrap(), + ); + Ok(Some(vec![ + lsp::InlayHint { + position: lsp::Position::new(0, 1), + label: lsp::InlayHintLabel::String("type hint".to_string()), + kind: Some(lsp::InlayHintKind::TYPE), + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }, + lsp::InlayHint { + position: lsp::Position::new(0, 2), + label: lsp::InlayHintLabel::String("parameter hint".to_string()), + kind: Some(lsp::InlayHintKind::PARAMETER), + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }, + lsp::InlayHint { + position: lsp::Position::new(0, 3), + label: lsp::InlayHintLabel::String("other hint".to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }, + ])) + } + }) + .next() + .await; + cx.executor().run_until_parked(); + + let mut edits_made = 1; + editor.update(cx, |editor, cx| { + assert_eq!( + lsp_request_count.load(Ordering::Relaxed), + 1, + "Should query new hints once" + ); + assert_eq!( + vec![ + "other hint".to_string(), + "parameter hint".to_string(), + "type hint".to_string(), + ], + cached_hint_labels(editor), + "Should get its first hints when opening the editor" + ); + assert_eq!( + vec!["other hint".to_string(), "type hint".to_string()], + visible_hint_labels(editor, cx) + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.allowed_hint_kinds, allowed_hint_kinds, + "Cache should use editor settings to get the allowed hint kinds" + ); + assert_eq!( + inlay_cache.version, edits_made, + "The editor update the cache version after every cache/view change" + ); + }); + + fake_server + .request::(()) + .await + .expect("inlay refresh request failed"); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + assert_eq!( + lsp_request_count.load(Ordering::Relaxed), + 2, + "Should load new hints twice" + ); + assert_eq!( + vec![ + "other hint".to_string(), + "parameter hint".to_string(), + "type hint".to_string(), + ], + cached_hint_labels(editor), + "Cached hints should not change due to allowed hint kinds settings update" + ); + assert_eq!( + vec!["other hint".to_string(), "type hint".to_string()], + visible_hint_labels(editor, cx) + ); + assert_eq!( + editor.inlay_hint_cache().version, + edits_made, + "Should not update cache version due to new loaded hints being the same" + ); + }); + + for (new_allowed_hint_kinds, expected_visible_hints) in [ + (HashSet::from_iter([None]), vec!["other hint".to_string()]), + ( + HashSet::from_iter([Some(InlayHintKind::Type)]), + vec!["type hint".to_string()], + ), + ( + HashSet::from_iter([Some(InlayHintKind::Parameter)]), + vec!["parameter hint".to_string()], + ), + ( + HashSet::from_iter([None, Some(InlayHintKind::Type)]), + vec!["other hint".to_string(), "type hint".to_string()], + ), + ( + HashSet::from_iter([None, Some(InlayHintKind::Parameter)]), + vec!["other hint".to_string(), "parameter hint".to_string()], + ), + ( + HashSet::from_iter([Some(InlayHintKind::Type), Some(InlayHintKind::Parameter)]), + vec!["parameter hint".to_string(), "type hint".to_string()], + ), + ( + HashSet::from_iter([ + None, + Some(InlayHintKind::Type), + Some(InlayHintKind::Parameter), + ]), + vec![ + "other hint".to_string(), + "parameter hint".to_string(), + "type hint".to_string(), + ], + ), + ] { + edits_made += 1; + update_test_language_settings(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: new_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), + show_parameter_hints: new_allowed_hint_kinds + .contains(&Some(InlayHintKind::Parameter)), + show_other_hints: new_allowed_hint_kinds.contains(&None), + }) + }); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + assert_eq!( + lsp_request_count.load(Ordering::Relaxed), + 2, + "Should not load new hints on allowed hint kinds change for hint kinds {new_allowed_hint_kinds:?}" + ); + assert_eq!( + vec![ + "other hint".to_string(), + "parameter hint".to_string(), + "type hint".to_string(), + ], + cached_hint_labels(editor), + "Should get its cached hints unchanged after the settings change for hint kinds {new_allowed_hint_kinds:?}" + ); + assert_eq!( + expected_visible_hints, + visible_hint_labels(editor, cx), + "Should get its visible hints filtered after the settings change for hint kinds {new_allowed_hint_kinds:?}" + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.allowed_hint_kinds, new_allowed_hint_kinds, + "Cache should use editor settings to get the allowed hint kinds for hint kinds {new_allowed_hint_kinds:?}" + ); + assert_eq!( + inlay_cache.version, edits_made, + "The editor should update the cache version after every cache/view change for hint kinds {new_allowed_hint_kinds:?} due to visible hints change" + ); + }); + } + + edits_made += 1; + let another_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Type)]); + update_test_language_settings(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: false, + show_type_hints: another_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), + show_parameter_hints: another_allowed_hint_kinds + .contains(&Some(InlayHintKind::Parameter)), + show_other_hints: another_allowed_hint_kinds.contains(&None), + }) + }); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + assert_eq!( + lsp_request_count.load(Ordering::Relaxed), + 2, + "Should not load new hints when hints got disabled" + ); + assert!( + cached_hint_labels(editor).is_empty(), + "Should clear the cache when hints got disabled" + ); + assert!( + visible_hint_labels(editor, cx).is_empty(), + "Should clear visible hints when hints got disabled" + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.allowed_hint_kinds, another_allowed_hint_kinds, + "Should update its allowed hint kinds even when hints got disabled" + ); + assert_eq!( + inlay_cache.version, edits_made, + "The editor should update the cache version after hints got disabled" + ); + }); + + fake_server + .request::(()) + .await + .expect("inlay refresh request failed"); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + assert_eq!( + lsp_request_count.load(Ordering::Relaxed), + 2, + "Should not load new hints when they got disabled" + ); + assert!(cached_hint_labels(editor).is_empty()); + assert!(visible_hint_labels(editor, cx).is_empty()); + assert_eq!( + editor.inlay_hint_cache().version, edits_made, + "The editor should not update the cache version after /refresh query without updates" + ); + }); + + let final_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Parameter)]); + edits_made += 1; + update_test_language_settings(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: final_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), + show_parameter_hints: final_allowed_hint_kinds + .contains(&Some(InlayHintKind::Parameter)), + show_other_hints: final_allowed_hint_kinds.contains(&None), + }) + }); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + assert_eq!( + lsp_request_count.load(Ordering::Relaxed), + 3, + "Should query for new hints when they got reenabled" + ); + assert_eq!( + vec![ + "other hint".to_string(), + "parameter hint".to_string(), + "type hint".to_string(), + ], + cached_hint_labels(editor), + "Should get its cached hints fully repopulated after the hints got reenabled" + ); + assert_eq!( + vec!["parameter hint".to_string()], + visible_hint_labels(editor, cx), + "Should get its visible hints repopulated and filtered after the h" + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.allowed_hint_kinds, final_allowed_hint_kinds, + "Cache should update editor settings when hints got reenabled" + ); + assert_eq!( + inlay_cache.version, edits_made, + "Cache should update its version after hints got reenabled" + ); + }); + + fake_server + .request::(()) + .await + .expect("inlay refresh request failed"); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + assert_eq!( + lsp_request_count.load(Ordering::Relaxed), + 4, + "Should query for new hints again" + ); + assert_eq!( + vec![ + "other hint".to_string(), + "parameter hint".to_string(), + "type hint".to_string(), + ], + cached_hint_labels(editor), + ); + assert_eq!( + vec!["parameter hint".to_string()], + visible_hint_labels(editor, cx), + ); + assert_eq!(editor.inlay_hint_cache().version, edits_made); + }); + } + + #[gpui::test] + async fn test_hint_request_cancellation(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + + let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; + let fake_server = Arc::new(fake_server); + let lsp_request_count = Arc::new(AtomicU32::new(0)); + let another_lsp_request_count = Arc::clone(&lsp_request_count); + fake_server + .handle_request::(move |params, _| { + let task_lsp_request_count = Arc::clone(&another_lsp_request_count); + async move { + let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1; + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path(file_with_hints).unwrap(), + ); + Ok(Some(vec![lsp::InlayHint { + position: lsp::Position::new(0, i), + label: lsp::InlayHintLabel::String(i.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) + } + }) + .next() + .await; + + let mut expected_changes = Vec::new(); + for change_after_opening in [ + "initial change #1", + "initial change #2", + "initial change #3", + ] { + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([13..13])); + editor.handle_input(change_after_opening, cx); + }); + expected_changes.push(change_after_opening); + } + + cx.executor().run_until_parked(); + + editor.update(cx, |editor, cx| { + let current_text = editor.text(cx); + for change in &expected_changes { + assert!( + current_text.contains(change), + "Should apply all changes made" + ); + } + assert_eq!( + lsp_request_count.load(Ordering::Relaxed), + 2, + "Should query new hints twice: for editor init and for the last edit that interrupted all others" + ); + let expected_hints = vec!["2".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Should get hints from the last edit landed only" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!( + editor.inlay_hint_cache().version, 1, + "Only one update should be registered in the cache after all cancellations" + ); + }); + + let mut edits = Vec::new(); + for async_later_change in [ + "another change #1", + "another change #2", + "another change #3", + ] { + expected_changes.push(async_later_change); + let task_editor = editor.clone(); + edits.push(cx.spawn(|mut cx| async move { + task_editor.update(&mut cx, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([13..13])); + editor.handle_input(async_later_change, cx); + }); + })); + } + let _ = future::join_all(edits).await; + cx.executor().run_until_parked(); + + editor.update(cx, |editor, cx| { + let current_text = editor.text(cx); + for change in &expected_changes { + assert!( + current_text.contains(change), + "Should apply all changes made" + ); + } + assert_eq!( + lsp_request_count.load(Ordering::SeqCst), + 3, + "Should query new hints one more time, for the last edit only" + ); + let expected_hints = vec!["3".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Should get hints from the last edit landed only" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!( + editor.inlay_hint_cache().version, + 2, + "Should update the cache version once more, for the new change" + ); + }); + } + + #[gpui::test(iterations = 10)] + async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/a", + json!({ + "main.rs": format!("fn main() {{\n{}\n}}", "let i = 5;\n".repeat(500)), + "other.rs": "// Test file", + }), + ) + .await; + let project = Project::test(fs, ["/a".as_ref()], cx).await; + project.update(cx, |project, _| project.languages().add(Arc::new(language))); + let buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/a/main.rs", cx) + }) + .await + .unwrap(); + cx.executor().run_until_parked(); + cx.executor().start_waiting(); + let fake_server = fake_servers.next().await.unwrap(); + let editor = cx.add_window(|cx| Editor::for_buffer(buffer, Some(project), cx)); + let lsp_request_ranges = Arc::new(Mutex::new(Vec::new())); + let lsp_request_count = Arc::new(AtomicUsize::new(0)); + let closure_lsp_request_ranges = Arc::clone(&lsp_request_ranges); + let closure_lsp_request_count = Arc::clone(&lsp_request_count); + fake_server + .handle_request::(move |params, _| { + let task_lsp_request_ranges = Arc::clone(&closure_lsp_request_ranges); + let task_lsp_request_count = Arc::clone(&closure_lsp_request_count); + async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + + task_lsp_request_ranges.lock().push(params.range); + let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::Release) + 1; + Ok(Some(vec![lsp::InlayHint { + position: params.range.end, + label: lsp::InlayHintLabel::String(i.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) + } + }) + .next() + .await; + + fn editor_visible_range( + editor: &WindowHandle, + cx: &mut gpui::TestAppContext, + ) -> Range { + let ranges = editor + .update(cx, |editor, cx| editor.excerpt_visible_offsets(None, cx)) + .unwrap(); + assert_eq!( + ranges.len(), + 1, + "Single buffer should produce a single excerpt with visible range" + ); + let (_, (excerpt_buffer, _, excerpt_visible_range)) = + ranges.into_iter().next().unwrap(); + excerpt_buffer.update(cx, |buffer, _| { + let snapshot = buffer.snapshot(); + let start = buffer + .anchor_before(excerpt_visible_range.start) + .to_point(&snapshot); + let end = buffer + .anchor_after(excerpt_visible_range.end) + .to_point(&snapshot); + start..end + }) + } + + // in large buffers, requests are made for more than visible range of a buffer. + // invisible parts are queried later, to avoid excessive requests on quick typing. + // wait the timeout needed to get all requests. + cx.executor().advance_clock(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, + )); + cx.executor().run_until_parked(); + let initial_visible_range = editor_visible_range(&editor, cx); + let lsp_initial_visible_range = lsp::Range::new( + lsp::Position::new( + initial_visible_range.start.row, + initial_visible_range.start.column, + ), + lsp::Position::new( + initial_visible_range.end.row, + initial_visible_range.end.column, + ), + ); + let expected_initial_query_range_end = + lsp::Position::new(initial_visible_range.end.row * 2, 2); + let mut expected_invisible_query_start = lsp_initial_visible_range.end; + expected_invisible_query_start.character += 1; + editor.update(cx, |editor, cx| { + let ranges = lsp_request_ranges.lock().drain(..).collect::>(); + assert_eq!(ranges.len(), 2, + "When scroll is at the edge of a big document, its visible part and the same range further should be queried in order, but got: {ranges:?}"); + let visible_query_range = &ranges[0]; + assert_eq!(visible_query_range.start, lsp_initial_visible_range.start); + assert_eq!(visible_query_range.end, lsp_initial_visible_range.end); + let invisible_query_range = &ranges[1]; + + assert_eq!(invisible_query_range.start, expected_invisible_query_start, "Should initially query visible edge of the document"); + assert_eq!(invisible_query_range.end, expected_initial_query_range_end, "Should initially query visible edge of the document"); + + let requests_count = lsp_request_count.load(Ordering::Acquire); + assert_eq!(requests_count, 2, "Visible + invisible request"); + let expected_hints = vec!["1".to_string(), "2".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Should have hints from both LSP requests made for a big file" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx), "Should display only hints from the visible range"); + assert_eq!( + editor.inlay_hint_cache().version, requests_count, + "LSP queries should've bumped the cache version" + ); + }); + + editor.update(cx, |editor, cx| { + editor.scroll_screen(&ScrollAmount::Page(1.0), cx); + editor.scroll_screen(&ScrollAmount::Page(1.0), cx); + }); + cx.executor().advance_clock(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, + )); + cx.executor().run_until_parked(); + let visible_range_after_scrolls = editor_visible_range(&editor, cx); + let visible_line_count = editor + .update(cx, |editor, _| editor.visible_line_count().unwrap()) + .unwrap(); + let selection_in_cached_range = editor + .update(cx, |editor, cx| { + let ranges = lsp_request_ranges + .lock() + .drain(..) + .sorted_by_key(|r| r.start) + .collect::>(); + assert_eq!( + ranges.len(), + 2, + "Should query 2 ranges after both scrolls, but got: {ranges:?}" + ); + let first_scroll = &ranges[0]; + let second_scroll = &ranges[1]; + assert_eq!( + first_scroll.end, second_scroll.start, + "Should query 2 adjacent ranges after the scrolls, but got: {ranges:?}" + ); + assert_eq!( + first_scroll.start, expected_initial_query_range_end, + "First scroll should start the query right after the end of the original scroll", + ); + assert_eq!( + second_scroll.end, + lsp::Position::new( + visible_range_after_scrolls.end.row + + visible_line_count.ceil() as u32, + 1, + ), + "Second scroll should query one more screen down after the end of the visible range" + ); + + let lsp_requests = lsp_request_count.load(Ordering::Acquire); + assert_eq!(lsp_requests, 4, "Should query for hints after every scroll"); + let expected_hints = vec![ + "1".to_string(), + "2".to_string(), + "3".to_string(), + "4".to_string(), + ]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Should have hints from the new LSP response after the edit" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!( + editor.inlay_hint_cache().version, + lsp_requests, + "Should update the cache for every LSP response with hints added" + ); + + let mut selection_in_cached_range = visible_range_after_scrolls.end; + selection_in_cached_range.row -= visible_line_count.ceil() as u32; + selection_in_cached_range + }) + .unwrap(); + + editor.update(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::center()), cx, |s| { + s.select_ranges([selection_in_cached_range..selection_in_cached_range]) + }); + }); + cx.executor().advance_clock(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, + )); + cx.executor().run_until_parked(); + editor.update(cx, |_, _| { + let ranges = lsp_request_ranges + .lock() + .drain(..) + .sorted_by_key(|r| r.start) + .collect::>(); + assert!(ranges.is_empty(), "No new ranges or LSP queries should be made after returning to the selection with cached hints"); + assert_eq!(lsp_request_count.load(Ordering::Acquire), 4); + }); + + editor.update(cx, |editor, cx| { + editor.handle_input("++++more text++++", cx); + }); + cx.executor().advance_clock(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, + )); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + let mut ranges = lsp_request_ranges.lock().drain(..).collect::>(); + ranges.sort_by_key(|r| r.start); + + assert_eq!(ranges.len(), 3, + "On edit, should scroll to selection and query a range around it: visible + same range above and below. Instead, got query ranges {ranges:?}"); + let above_query_range = &ranges[0]; + let visible_query_range = &ranges[1]; + let below_query_range = &ranges[2]; + assert!(above_query_range.end.character < visible_query_range.start.character || above_query_range.end.line + 1 == visible_query_range.start.line, + "Above range {above_query_range:?} should be before visible range {visible_query_range:?}"); + assert!(visible_query_range.end.character < below_query_range.start.character || visible_query_range.end.line + 1 == below_query_range.start.line, + "Visible range {visible_query_range:?} should be before below range {below_query_range:?}"); + assert!(above_query_range.start.line < selection_in_cached_range.row, + "Hints should be queried with the selected range after the query range start"); + assert!(below_query_range.end.line > selection_in_cached_range.row, + "Hints should be queried with the selected range before the query range end"); + assert!(above_query_range.start.line <= selection_in_cached_range.row - (visible_line_count * 3.0 / 2.0) as u32, + "Hints query range should contain one more screen before"); + assert!(below_query_range.end.line >= selection_in_cached_range.row + (visible_line_count * 3.0 / 2.0) as u32, + "Hints query range should contain one more screen after"); + + let lsp_requests = lsp_request_count.load(Ordering::Acquire); + assert_eq!(lsp_requests, 7, "There should be a visible range and two ranges above and below it queried"); + let expected_hints = vec!["5".to_string(), "6".to_string(), "7".to_string()]; + assert_eq!(expected_hints, cached_hint_labels(editor), + "Should have hints from the new LSP response after the edit"); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, lsp_requests, "Should update the cache for every LSP response with hints added"); + }); + } + + #[gpui::test(iterations = 10)] + async fn test_multiple_excerpts_large_multibuffer(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + let language = Arc::new(language); + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/a", + json!({ + "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::>().join("")), + "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::>().join("")), + }), + ) + .await; + let project = Project::test(fs, ["/a".as_ref()], cx).await; + project.update(cx, |project, _| { + project.languages().add(Arc::clone(&language)) + }); + let worktree_id = project.update(cx, |project, cx| { + project.worktrees().next().unwrap().read(cx).id() + }); + + let buffer_1 = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "main.rs"), cx) + }) + .await + .unwrap(); + let buffer_2 = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "other.rs"), cx) + }) + .await + .unwrap(); + let multibuffer = cx.build_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + multibuffer.push_excerpts( + buffer_1.clone(), + [ + ExcerptRange { + context: Point::new(0, 0)..Point::new(2, 0), + primary: None, + }, + ExcerptRange { + context: Point::new(4, 0)..Point::new(11, 0), + primary: None, + }, + ExcerptRange { + context: Point::new(22, 0)..Point::new(33, 0), + primary: None, + }, + ExcerptRange { + context: Point::new(44, 0)..Point::new(55, 0), + primary: None, + }, + ExcerptRange { + context: Point::new(56, 0)..Point::new(66, 0), + primary: None, + }, + ExcerptRange { + context: Point::new(67, 0)..Point::new(77, 0), + primary: None, + }, + ], + cx, + ); + multibuffer.push_excerpts( + buffer_2.clone(), + [ + ExcerptRange { + context: Point::new(0, 1)..Point::new(2, 1), + primary: None, + }, + ExcerptRange { + context: Point::new(4, 1)..Point::new(11, 1), + primary: None, + }, + ExcerptRange { + context: Point::new(22, 1)..Point::new(33, 1), + primary: None, + }, + ExcerptRange { + context: Point::new(44, 1)..Point::new(55, 1), + primary: None, + }, + ExcerptRange { + context: Point::new(56, 1)..Point::new(66, 1), + primary: None, + }, + ExcerptRange { + context: Point::new(67, 1)..Point::new(77, 1), + primary: None, + }, + ], + cx, + ); + multibuffer + }); + + cx.executor().run_until_parked(); + let editor = + cx.add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx)); + let editor_edited = Arc::new(AtomicBool::new(false)); + let fake_server = fake_servers.next().await.unwrap(); + let closure_editor_edited = Arc::clone(&editor_edited); + fake_server + .handle_request::(move |params, _| { + let task_editor_edited = Arc::clone(&closure_editor_edited); + async move { + let hint_text = if params.text_document.uri + == lsp::Url::from_file_path("/a/main.rs").unwrap() + { + "main hint" + } else if params.text_document.uri + == lsp::Url::from_file_path("/a/other.rs").unwrap() + { + "other hint" + } else { + panic!("unexpected uri: {:?}", params.text_document.uri); + }; + + // one hint per excerpt + let positions = [ + lsp::Position::new(0, 2), + lsp::Position::new(4, 2), + lsp::Position::new(22, 2), + lsp::Position::new(44, 2), + lsp::Position::new(56, 2), + lsp::Position::new(67, 2), + ]; + let out_of_range_hint = lsp::InlayHint { + position: lsp::Position::new( + params.range.start.line + 99, + params.range.start.character + 99, + ), + label: lsp::InlayHintLabel::String( + "out of excerpt range, should be ignored".to_string(), + ), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }; + + let edited = task_editor_edited.load(Ordering::Acquire); + Ok(Some( + std::iter::once(out_of_range_hint) + .chain(positions.into_iter().enumerate().map(|(i, position)| { + lsp::InlayHint { + position, + label: lsp::InlayHintLabel::String(format!( + "{hint_text}{} #{i}", + if edited { "(edited)" } else { "" }, + )), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + } + })) + .collect(), + )) + } + }) + .next() + .await; + cx.executor().run_until_parked(); + + editor.update(cx, |editor, cx| { + let expected_hints = vec![ + "main hint #0".to_string(), + "main hint #1".to_string(), + "main hint #2".to_string(), + "main hint #3".to_string(), + // todo!() there used to be no these hints, but new gpui2 presumably scrolls a bit farther + // (or renders less?) note that tests below pass + "main hint #4".to_string(), + "main hint #5".to_string(), + ]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), "Every visible excerpt hints should bump the verison"); + }); + + editor.update(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::Next), cx, |s| { + s.select_ranges([Point::new(4, 0)..Point::new(4, 0)]) + }); + editor.change_selections(Some(Autoscroll::Next), cx, |s| { + s.select_ranges([Point::new(22, 0)..Point::new(22, 0)]) + }); + editor.change_selections(Some(Autoscroll::Next), cx, |s| { + s.select_ranges([Point::new(50, 0)..Point::new(50, 0)]) + }); + }); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_hints = vec![ + "main hint #0".to_string(), + "main hint #1".to_string(), + "main hint #2".to_string(), + "main hint #3".to_string(), + "main hint #4".to_string(), + "main hint #5".to_string(), + "other hint #0".to_string(), + "other hint #1".to_string(), + "other hint #2".to_string(), + ]; + assert_eq!(expected_hints, cached_hint_labels(editor), + "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits"); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), + "Due to every excerpt having one hint, we update cache per new excerpt scrolled"); + }); + + editor.update(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::Next), cx, |s| { + s.select_ranges([Point::new(100, 0)..Point::new(100, 0)]) + }); + }); + cx.executor().advance_clock(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, + )); + cx.executor().run_until_parked(); + let last_scroll_update_version = editor.update(cx, |editor, cx| { + let expected_hints = vec![ + "main hint #0".to_string(), + "main hint #1".to_string(), + "main hint #2".to_string(), + "main hint #3".to_string(), + "main hint #4".to_string(), + "main hint #5".to_string(), + "other hint #0".to_string(), + "other hint #1".to_string(), + "other hint #2".to_string(), + "other hint #3".to_string(), + "other hint #4".to_string(), + "other hint #5".to_string(), + ]; + assert_eq!(expected_hints, cached_hint_labels(editor), + "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched"); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, expected_hints.len()); + expected_hints.len() + }).unwrap(); + + editor.update(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::Next), cx, |s| { + s.select_ranges([Point::new(4, 0)..Point::new(4, 0)]) + }); + }); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_hints = vec![ + "main hint #0".to_string(), + "main hint #1".to_string(), + "main hint #2".to_string(), + "main hint #3".to_string(), + "main hint #4".to_string(), + "main hint #5".to_string(), + "other hint #0".to_string(), + "other hint #1".to_string(), + "other hint #2".to_string(), + "other hint #3".to_string(), + "other hint #4".to_string(), + "other hint #5".to_string(), + ]; + assert_eq!(expected_hints, cached_hint_labels(editor), + "After multibuffer was scrolled to the end, further scrolls up should not bring more hints"); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, last_scroll_update_version, "No updates should happen during scrolling already scolled buffer"); + }); + + editor_edited.store(true, Ordering::Release); + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(56, 0)..Point::new(56, 0)]) + }); + editor.handle_input("++++more text++++", cx); + }); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_hints = vec![ + "main hint(edited) #0".to_string(), + "main hint(edited) #1".to_string(), + "main hint(edited) #2".to_string(), + "main hint(edited) #3".to_string(), + "main hint(edited) #4".to_string(), + "main hint(edited) #5".to_string(), + "other hint(edited) #0".to_string(), + "other hint(edited) #1".to_string(), + ]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "After multibuffer edit, editor gets scolled back to the last selection; \ +all hints should be invalidated and requeried for all of its visible excerpts" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + + let current_cache_version = editor.inlay_hint_cache().version; + let minimum_expected_version = last_scroll_update_version + expected_hints.len(); + assert!( + current_cache_version == minimum_expected_version || current_cache_version == minimum_expected_version + 1, + "Due to every excerpt having one hint, cache should update per new excerpt received + 1 potential sporadic update" + ); + }); + } + + #[gpui::test] + async fn test_excerpts_removed(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: false, + show_parameter_hints: false, + show_other_hints: false, + }) + }); + + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + let language = Arc::new(language); + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/a", + json!({ + "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::>().join("")), + "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::>().join("")), + }), + ) + .await; + let project = Project::test(fs, ["/a".as_ref()], cx).await; + project.update(cx, |project, _| { + project.languages().add(Arc::clone(&language)) + }); + let worktree_id = project.update(cx, |project, cx| { + project.worktrees().next().unwrap().read(cx).id() + }); + + let buffer_1 = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "main.rs"), cx) + }) + .await + .unwrap(); + let buffer_2 = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "other.rs"), cx) + }) + .await + .unwrap(); + let multibuffer = cx.build_model(|_| MultiBuffer::new(0)); + let (buffer_1_excerpts, buffer_2_excerpts) = multibuffer.update(cx, |multibuffer, cx| { + let buffer_1_excerpts = multibuffer.push_excerpts( + buffer_1.clone(), + [ExcerptRange { + context: Point::new(0, 0)..Point::new(2, 0), + primary: None, + }], + cx, + ); + let buffer_2_excerpts = multibuffer.push_excerpts( + buffer_2.clone(), + [ExcerptRange { + context: Point::new(0, 1)..Point::new(2, 1), + primary: None, + }], + cx, + ); + (buffer_1_excerpts, buffer_2_excerpts) + }); + + assert!(!buffer_1_excerpts.is_empty()); + assert!(!buffer_2_excerpts.is_empty()); + + cx.executor().run_until_parked(); + let editor = + cx.add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx)); + let editor_edited = Arc::new(AtomicBool::new(false)); + let fake_server = fake_servers.next().await.unwrap(); + let closure_editor_edited = Arc::clone(&editor_edited); + fake_server + .handle_request::(move |params, _| { + let task_editor_edited = Arc::clone(&closure_editor_edited); + async move { + let hint_text = if params.text_document.uri + == lsp::Url::from_file_path("/a/main.rs").unwrap() + { + "main hint" + } else if params.text_document.uri + == lsp::Url::from_file_path("/a/other.rs").unwrap() + { + "other hint" + } else { + panic!("unexpected uri: {:?}", params.text_document.uri); + }; + + let positions = [ + lsp::Position::new(0, 2), + lsp::Position::new(4, 2), + lsp::Position::new(22, 2), + lsp::Position::new(44, 2), + lsp::Position::new(56, 2), + lsp::Position::new(67, 2), + ]; + let out_of_range_hint = lsp::InlayHint { + position: lsp::Position::new( + params.range.start.line + 99, + params.range.start.character + 99, + ), + label: lsp::InlayHintLabel::String( + "out of excerpt range, should be ignored".to_string(), + ), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }; + + let edited = task_editor_edited.load(Ordering::Acquire); + Ok(Some( + std::iter::once(out_of_range_hint) + .chain(positions.into_iter().enumerate().map(|(i, position)| { + lsp::InlayHint { + position, + label: lsp::InlayHintLabel::String(format!( + "{hint_text}{} #{i}", + if edited { "(edited)" } else { "" }, + )), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + } + })) + .collect(), + )) + } + }) + .next() + .await; + cx.executor().run_until_parked(); + + editor.update(cx, |editor, cx| { + assert_eq!( + vec!["main hint #0".to_string(), "other hint #0".to_string()], + cached_hint_labels(editor), + "Cache should update for both excerpts despite hints display was disabled" + ); + assert!( + visible_hint_labels(editor, cx).is_empty(), + "All hints are disabled and should not be shown despite being present in the cache" + ); + assert_eq!( + editor.inlay_hint_cache().version, + 2, + "Cache should update once per excerpt query" + ); + }); + + editor.update(cx, |editor, cx| { + editor.buffer().update(cx, |multibuffer, cx| { + multibuffer.remove_excerpts(buffer_2_excerpts, cx) + }) + }); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + assert_eq!( + vec!["main hint #0".to_string()], + cached_hint_labels(editor), + "For the removed excerpt, should clean corresponding cached hints" + ); + assert!( + visible_hint_labels(editor, cx).is_empty(), + "All hints are disabled and should not be shown despite being present in the cache" + ); + assert_eq!( + editor.inlay_hint_cache().version, + 3, + "Excerpt removal should trigger a cache update" + ); + }); + + update_test_language_settings(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_hints = vec!["main hint #0".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Hint display settings change should not change the cache" + ); + assert_eq!( + expected_hints, + visible_hint_labels(editor, cx), + "Settings change should make cached hints visible" + ); + assert_eq!( + editor.inlay_hint_cache().version, + 4, + "Settings change should trigger a cache update" + ); + }); + } + + #[gpui::test] + async fn test_inside_char_boundary_range_hints(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/a", + json!({ + "main.rs": format!(r#"fn main() {{\n{}\n}}"#, format!("let i = {};\n", "√".repeat(10)).repeat(500)), + "other.rs": "// Test file", + }), + ) + .await; + let project = Project::test(fs, ["/a".as_ref()], cx).await; + project.update(cx, |project, _| project.languages().add(Arc::new(language))); + let buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/a/main.rs", cx) + }) + .await + .unwrap(); + cx.executor().run_until_parked(); + cx.executor().start_waiting(); + let fake_server = fake_servers.next().await.unwrap(); + let editor = cx.add_window(|cx| Editor::for_buffer(buffer, Some(project), cx)); + let lsp_request_count = Arc::new(AtomicU32::new(0)); + let closure_lsp_request_count = Arc::clone(&lsp_request_count); + fake_server + .handle_request::(move |params, _| { + let task_lsp_request_count = Arc::clone(&closure_lsp_request_count); + async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + let query_start = params.range.start; + let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::Release) + 1; + Ok(Some(vec![lsp::InlayHint { + position: query_start, + label: lsp::InlayHintLabel::String(i.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) + } + }) + .next() + .await; + + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(10, 0)..Point::new(10, 0)]) + }) + }); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_hints = vec!["1".to_string()]; + assert_eq!(expected_hints, cached_hint_labels(editor)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, 1); + }); + } + + #[gpui::test] + async fn test_toggle_inlay_hints(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: false, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + + let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; + + editor.update(cx, |editor, cx| { + editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx) + }); + cx.executor().start_waiting(); + let lsp_request_count = Arc::new(AtomicU32::new(0)); + let closure_lsp_request_count = Arc::clone(&lsp_request_count); + fake_server + .handle_request::(move |params, _| { + let task_lsp_request_count = Arc::clone(&closure_lsp_request_count); + async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path(file_with_hints).unwrap(), + ); + + let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1; + Ok(Some(vec![lsp::InlayHint { + position: lsp::Position::new(0, i), + label: lsp::InlayHintLabel::String(i.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) + } + }) + .next() + .await; + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_hints = vec!["1".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Should display inlays after toggle despite them disabled in settings" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!( + editor.inlay_hint_cache().version, + 1, + "First toggle should be cache's first update" + ); + }); + + editor.update(cx, |editor, cx| { + editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx) + }); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + assert!( + cached_hint_labels(editor).is_empty(), + "Should clear hints after 2nd toggle" + ); + assert!(visible_hint_labels(editor, cx).is_empty()); + assert_eq!(editor.inlay_hint_cache().version, 2); + }); + + update_test_language_settings(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_hints = vec!["2".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Should query LSP hints for the 2nd time after enabling hints in settings" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, 3); + }); + + editor.update(cx, |editor, cx| { + editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx) + }); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + assert!( + cached_hint_labels(editor).is_empty(), + "Should clear hints after enabling in settings and a 3rd toggle" + ); + assert!(visible_hint_labels(editor, cx).is_empty()); + assert_eq!(editor.inlay_hint_cache().version, 4); + }); + + editor.update(cx, |editor, cx| { + editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx) + }); + cx.executor().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_hints = vec!["3".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Should query LSP hints for the 3rd time after enabling hints in settings and toggling them back on" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, 5); + }); + } + + pub(crate) fn init_test(cx: &mut TestAppContext, f: impl Fn(&mut AllLanguageSettingsContent)) { + cx.update(|cx| { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + theme::init(cx); + client::init_settings(cx); + language::init(cx); + Project::init_settings(cx); + workspace::init_settings(cx); + crate::init(cx); + }); + + update_test_language_settings(cx, f); + } + + async fn prepare_test_objects( + cx: &mut TestAppContext, + ) -> (&'static str, WindowHandle, FakeLanguageServer) { + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/a", + json!({ + "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out", + "other.rs": "// Test file", + }), + ) + .await; + + let project = Project::test(fs, ["/a".as_ref()], cx).await; + project.update(cx, |project, _| project.languages().add(Arc::new(language))); + let buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/a/main.rs", cx) + }) + .await + .unwrap(); + cx.executor().run_until_parked(); + cx.executor().start_waiting(); + let fake_server = fake_servers.next().await.unwrap(); + let editor = cx.add_window(|cx| Editor::for_buffer(buffer, Some(project), cx)); + + editor.update(cx, |editor, cx| { + assert!(cached_hint_labels(editor).is_empty()); + assert!(visible_hint_labels(editor, cx).is_empty()); + assert_eq!(editor.inlay_hint_cache().version, 0); + }); + + ("/a/main.rs", editor, fake_server) + } + + pub fn cached_hint_labels(editor: &Editor) -> Vec { + let mut labels = Vec::new(); + for (_, excerpt_hints) in &editor.inlay_hint_cache().hints { + let excerpt_hints = excerpt_hints.read(); + for id in &excerpt_hints.ordered_hints { + labels.push(excerpt_hints.hints_by_id[id].text()); + } + } + + labels.sort(); + labels + } + + pub fn visible_hint_labels(editor: &Editor, cx: &ViewContext<'_, Editor>) -> Vec { + let mut hints = editor + .visible_inlay_hints(cx) + .into_iter() + .map(|hint| hint.text.to_string()) + .collect::>(); + hints.sort(); + hints + } } -// let cached_excerpt_hints = editor -// .inlay_hint_cache -// .hints -// .entry(new_update.excerpt_id) -// .or_insert_with(|| { -// Arc::new(RwLock::new(CachedExcerptHints { -// version: query.cache_version, -// buffer_version: buffer_snapshot.version().clone(), -// buffer_id: query.buffer_id, -// ordered_hints: Vec::new(), -// hints_by_id: HashMap::default(), -// })) -// }); -// let mut cached_excerpt_hints = cached_excerpt_hints.write(); -// match query.cache_version.cmp(&cached_excerpt_hints.version) { -// cmp::Ordering::Less => return, -// cmp::Ordering::Greater | cmp::Ordering::Equal => { -// cached_excerpt_hints.version = query.cache_version; -// } -// } - -// let mut cached_inlays_changed = !new_update.remove_from_cache.is_empty(); -// cached_excerpt_hints -// .ordered_hints -// .retain(|hint_id| !new_update.remove_from_cache.contains(hint_id)); -// cached_excerpt_hints -// .hints_by_id -// .retain(|hint_id, _| !new_update.remove_from_cache.contains(hint_id)); -// let mut splice = InlaySplice { -// to_remove: new_update.remove_from_visible, -// to_insert: Vec::new(), -// }; -// for new_hint in new_update.add_to_cache { -// let insert_position = match cached_excerpt_hints -// .ordered_hints -// .binary_search_by(|probe| { -// cached_excerpt_hints.hints_by_id[probe] -// .position -// .cmp(&new_hint.position, &buffer_snapshot) -// }) { -// Ok(i) => { -// let mut insert_position = Some(i); -// for id in &cached_excerpt_hints.ordered_hints[i..] { -// let cached_hint = &cached_excerpt_hints.hints_by_id[id]; -// if new_hint -// .position -// .cmp(&cached_hint.position, &buffer_snapshot) -// .is_gt() -// { -// break; -// } -// if cached_hint.text() == new_hint.text() { -// insert_position = None; -// break; -// } -// } -// insert_position -// } -// Err(i) => Some(i), -// }; - -// if let Some(insert_position) = insert_position { -// let new_inlay_id = post_inc(&mut editor.next_inlay_id); -// if editor -// .inlay_hint_cache -// .allowed_hint_kinds -// .contains(&new_hint.kind) -// { -// let new_hint_position = -// multi_buffer_snapshot.anchor_in_excerpt(query.excerpt_id, new_hint.position); -// splice -// .to_insert -// .push(Inlay::hint(new_inlay_id, new_hint_position, &new_hint)); -// } -// let new_id = InlayId::Hint(new_inlay_id); -// cached_excerpt_hints.hints_by_id.insert(new_id, new_hint); -// cached_excerpt_hints -// .ordered_hints -// .insert(insert_position, new_id); -// cached_inlays_changed = true; -// } -// } -// cached_excerpt_hints.buffer_version = buffer_snapshot.version().clone(); -// drop(cached_excerpt_hints); - -// if invalidate { -// let mut outdated_excerpt_caches = HashSet::default(); -// for (excerpt_id, excerpt_hints) in &editor.inlay_hint_cache().hints { -// let excerpt_hints = excerpt_hints.read(); -// if excerpt_hints.buffer_id == query.buffer_id -// && excerpt_id != &query.excerpt_id -// && buffer_snapshot -// .version() -// .changed_since(&excerpt_hints.buffer_version) -// { -// outdated_excerpt_caches.insert(*excerpt_id); -// splice -// .to_remove -// .extend(excerpt_hints.ordered_hints.iter().copied()); -// } -// } -// cached_inlays_changed |= !outdated_excerpt_caches.is_empty(); -// editor -// .inlay_hint_cache -// .hints -// .retain(|excerpt_id, _| !outdated_excerpt_caches.contains(excerpt_id)); -// } - -// let InlaySplice { -// to_remove, -// to_insert, -// } = splice; -// let displayed_inlays_changed = !to_remove.is_empty() || !to_insert.is_empty(); -// if cached_inlays_changed || displayed_inlays_changed { -// editor.inlay_hint_cache.version += 1; -// } -// if displayed_inlays_changed { -// editor.splice_inlay_hints(to_remove, to_insert, cx) -// } -// } - -// #[cfg(test)] -// pub mod tests { -// use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering}; - -// use crate::{ -// scroll::{autoscroll::Autoscroll, scroll_amount::ScrollAmount}, -// serde_json::json, -// ExcerptRange, -// }; -// use futures::StreamExt; -// use gpui::{executor::Deterministic, TestAppContext, View}; -// use itertools::Itertools; -// use language::{ -// language_settings::AllLanguageSettingsContent, FakeLspAdapter, Language, LanguageConfig, -// }; -// use lsp::FakeLanguageServer; -// use parking_lot::Mutex; -// use project::{FakeFs, Project}; -// use settings::SettingsStore; -// use text::{Point, ToPoint}; -// use workspace::Workspace; - -// use crate::editor_tests::update_test_language_settings; - -// use super::*; - -// #[gpui::test] -// async fn test_basic_cache_update_with_duplicate_hints(cx: &mut gpui::TestAppContext) { -// let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]); -// init_test(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), -// show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)), -// show_other_hints: allowed_hint_kinds.contains(&None), -// }) -// }); - -// let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; -// let lsp_request_count = Arc::new(AtomicU32::new(0)); -// fake_server -// .handle_request::(move |params, _| { -// let task_lsp_request_count = Arc::clone(&lsp_request_count); -// async move { -// assert_eq!( -// params.text_document.uri, -// lsp::Url::from_file_path(file_with_hints).unwrap(), -// ); -// let current_call_id = -// Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst); -// let mut new_hints = Vec::with_capacity(2 * current_call_id as usize); -// for _ in 0..2 { -// let mut i = current_call_id; -// loop { -// new_hints.push(lsp::InlayHint { -// position: lsp::Position::new(0, i), -// label: lsp::InlayHintLabel::String(i.to_string()), -// kind: None, -// text_edits: None, -// tooltip: None, -// padding_left: None, -// padding_right: None, -// data: None, -// }); -// if i == 0 { -// break; -// } -// i -= 1; -// } -// } - -// Ok(Some(new_hints)) -// } -// }) -// .next() -// .await; -// cx.foreground().run_until_parked(); - -// let mut edits_made = 1; -// editor.update(cx, |editor, cx| { -// let expected_hints = vec!["0".to_string()]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "Should get its first hints when opening the editor" -// ); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!( -// inlay_cache.allowed_hint_kinds, allowed_hint_kinds, -// "Cache should use editor settings to get the allowed hint kinds" -// ); -// assert_eq!( -// inlay_cache.version, edits_made, -// "The editor update the cache version after every cache/view change" -// ); -// }); - -// editor.update(cx, |editor, cx| { -// editor.change_selections(None, cx, |s| s.select_ranges([13..13])); -// editor.handle_input("some change", cx); -// edits_made += 1; -// }); -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// let expected_hints = vec!["0".to_string(), "1".to_string()]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "Should get new hints after an edit" -// ); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!( -// inlay_cache.allowed_hint_kinds, allowed_hint_kinds, -// "Cache should use editor settings to get the allowed hint kinds" -// ); -// assert_eq!( -// inlay_cache.version, edits_made, -// "The editor update the cache version after every cache/view change" -// ); -// }); - -// fake_server -// .request::(()) -// .await -// .expect("inlay refresh request failed"); -// edits_made += 1; -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// let expected_hints = vec!["0".to_string(), "1".to_string(), "2".to_string()]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "Should get new hints after hint refresh/ request" -// ); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!( -// inlay_cache.allowed_hint_kinds, allowed_hint_kinds, -// "Cache should use editor settings to get the allowed hint kinds" -// ); -// assert_eq!( -// inlay_cache.version, edits_made, -// "The editor update the cache version after every cache/view change" -// ); -// }); -// } - -// #[gpui::test] -// async fn test_cache_update_on_lsp_completion_tasks(cx: &mut gpui::TestAppContext) { -// init_test(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: true, -// show_parameter_hints: true, -// show_other_hints: true, -// }) -// }); - -// let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; -// let lsp_request_count = Arc::new(AtomicU32::new(0)); -// fake_server -// .handle_request::(move |params, _| { -// let task_lsp_request_count = Arc::clone(&lsp_request_count); -// async move { -// assert_eq!( -// params.text_document.uri, -// lsp::Url::from_file_path(file_with_hints).unwrap(), -// ); -// let current_call_id = -// Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst); -// Ok(Some(vec![lsp::InlayHint { -// position: lsp::Position::new(0, current_call_id), -// label: lsp::InlayHintLabel::String(current_call_id.to_string()), -// kind: None, -// text_edits: None, -// tooltip: None, -// padding_left: None, -// padding_right: None, -// data: None, -// }])) -// } -// }) -// .next() -// .await; -// cx.foreground().run_until_parked(); - -// let mut edits_made = 1; -// editor.update(cx, |editor, cx| { -// let expected_hints = vec!["0".to_string()]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "Should get its first hints when opening the editor" -// ); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// assert_eq!( -// editor.inlay_hint_cache().version, -// edits_made, -// "The editor update the cache version after every cache/view change" -// ); -// }); - -// let progress_token = "test_progress_token"; -// fake_server -// .request::(lsp::WorkDoneProgressCreateParams { -// token: lsp::ProgressToken::String(progress_token.to_string()), -// }) -// .await -// .expect("work done progress create request failed"); -// cx.foreground().run_until_parked(); -// fake_server.notify::(lsp::ProgressParams { -// token: lsp::ProgressToken::String(progress_token.to_string()), -// value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin( -// lsp::WorkDoneProgressBegin::default(), -// )), -// }); -// cx.foreground().run_until_parked(); - -// editor.update(cx, |editor, cx| { -// let expected_hints = vec!["0".to_string()]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "Should not update hints while the work task is running" -// ); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// assert_eq!( -// editor.inlay_hint_cache().version, -// edits_made, -// "Should not update the cache while the work task is running" -// ); -// }); - -// fake_server.notify::(lsp::ProgressParams { -// token: lsp::ProgressToken::String(progress_token.to_string()), -// value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End( -// lsp::WorkDoneProgressEnd::default(), -// )), -// }); -// cx.foreground().run_until_parked(); - -// edits_made += 1; -// editor.update(cx, |editor, cx| { -// let expected_hints = vec!["1".to_string()]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "New hints should be queried after the work task is done" -// ); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// assert_eq!( -// editor.inlay_hint_cache().version, -// edits_made, -// "Cache version should udpate once after the work task is done" -// ); -// }); -// } - -// #[gpui::test] -// async fn test_no_hint_updates_for_unrelated_language_files(cx: &mut gpui::TestAppContext) { -// init_test(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: true, -// show_parameter_hints: true, -// show_other_hints: true, -// }) -// }); - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/a", -// json!({ -// "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out", -// "other.md": "Test md file with some text", -// }), -// ) -// .await; -// let project = Project::test(fs, ["/a".as_ref()], cx).await; -// let workspace = cx -// .add_window(|cx| Workspace::test_new(project.clone(), cx)) -// .root(cx); -// let worktree_id = workspace.update(cx, |workspace, cx| { -// workspace.project().read_with(cx, |project, cx| { -// project.worktrees(cx).next().unwrap().read(cx).id() -// }) -// }); - -// let mut rs_fake_servers = None; -// let mut md_fake_servers = None; -// for (name, path_suffix) in [("Rust", "rs"), ("Markdown", "md")] { -// let mut language = Language::new( -// LanguageConfig { -// name: name.into(), -// path_suffixes: vec![path_suffix.to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ); -// let fake_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// name, -// capabilities: lsp::ServerCapabilities { -// inlay_hint_provider: Some(lsp::OneOf::Left(true)), -// ..Default::default() -// }, -// ..Default::default() -// })) -// .await; -// match name { -// "Rust" => rs_fake_servers = Some(fake_servers), -// "Markdown" => md_fake_servers = Some(fake_servers), -// _ => unreachable!(), -// } -// project.update(cx, |project, _| { -// project.languages().add(Arc::new(language)); -// }); -// } - -// let _rs_buffer = project -// .update(cx, |project, cx| { -// project.open_local_buffer("/a/main.rs", cx) -// }) -// .await -// .unwrap(); -// cx.foreground().run_until_parked(); -// cx.foreground().start_waiting(); -// let rs_fake_server = rs_fake_servers.unwrap().next().await.unwrap(); -// let rs_editor = workspace -// .update(cx, |workspace, cx| { -// workspace.open_path((worktree_id, "main.rs"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); -// let rs_lsp_request_count = Arc::new(AtomicU32::new(0)); -// rs_fake_server -// .handle_request::(move |params, _| { -// let task_lsp_request_count = Arc::clone(&rs_lsp_request_count); -// async move { -// assert_eq!( -// params.text_document.uri, -// lsp::Url::from_file_path("/a/main.rs").unwrap(), -// ); -// let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst); -// Ok(Some(vec![lsp::InlayHint { -// position: lsp::Position::new(0, i), -// label: lsp::InlayHintLabel::String(i.to_string()), -// kind: None, -// text_edits: None, -// tooltip: None, -// padding_left: None, -// padding_right: None, -// data: None, -// }])) -// } -// }) -// .next() -// .await; -// cx.foreground().run_until_parked(); -// rs_editor.update(cx, |editor, cx| { -// let expected_hints = vec!["0".to_string()]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "Should get its first hints when opening the editor" -// ); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// assert_eq!( -// editor.inlay_hint_cache().version, -// 1, -// "Rust editor update the cache version after every cache/view change" -// ); -// }); - -// cx.foreground().run_until_parked(); -// let _md_buffer = project -// .update(cx, |project, cx| { -// project.open_local_buffer("/a/other.md", cx) -// }) -// .await -// .unwrap(); -// cx.foreground().run_until_parked(); -// cx.foreground().start_waiting(); -// let md_fake_server = md_fake_servers.unwrap().next().await.unwrap(); -// let md_editor = workspace -// .update(cx, |workspace, cx| { -// workspace.open_path((worktree_id, "other.md"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); -// let md_lsp_request_count = Arc::new(AtomicU32::new(0)); -// md_fake_server -// .handle_request::(move |params, _| { -// let task_lsp_request_count = Arc::clone(&md_lsp_request_count); -// async move { -// assert_eq!( -// params.text_document.uri, -// lsp::Url::from_file_path("/a/other.md").unwrap(), -// ); -// let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst); -// Ok(Some(vec![lsp::InlayHint { -// position: lsp::Position::new(0, i), -// label: lsp::InlayHintLabel::String(i.to_string()), -// kind: None, -// text_edits: None, -// tooltip: None, -// padding_left: None, -// padding_right: None, -// data: None, -// }])) -// } -// }) -// .next() -// .await; -// cx.foreground().run_until_parked(); -// md_editor.update(cx, |editor, cx| { -// let expected_hints = vec!["0".to_string()]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "Markdown editor should have a separate verison, repeating Rust editor rules" -// ); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// assert_eq!(editor.inlay_hint_cache().version, 1); -// }); - -// rs_editor.update(cx, |editor, cx| { -// editor.change_selections(None, cx, |s| s.select_ranges([13..13])); -// editor.handle_input("some rs change", cx); -// }); -// cx.foreground().run_until_parked(); -// rs_editor.update(cx, |editor, cx| { -// let expected_hints = vec!["1".to_string()]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "Rust inlay cache should change after the edit" -// ); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// assert_eq!( -// editor.inlay_hint_cache().version, -// 2, -// "Every time hint cache changes, cache version should be incremented" -// ); -// }); -// md_editor.update(cx, |editor, cx| { -// let expected_hints = vec!["0".to_string()]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "Markdown editor should not be affected by Rust editor changes" -// ); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// assert_eq!(editor.inlay_hint_cache().version, 1); -// }); - -// md_editor.update(cx, |editor, cx| { -// editor.change_selections(None, cx, |s| s.select_ranges([13..13])); -// editor.handle_input("some md change", cx); -// }); -// cx.foreground().run_until_parked(); -// md_editor.update(cx, |editor, cx| { -// let expected_hints = vec!["1".to_string()]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "Rust editor should not be affected by Markdown editor changes" -// ); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// assert_eq!(editor.inlay_hint_cache().version, 2); -// }); -// rs_editor.update(cx, |editor, cx| { -// let expected_hints = vec!["1".to_string()]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "Markdown editor should also change independently" -// ); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// assert_eq!(editor.inlay_hint_cache().version, 2); -// }); -// } - -// #[gpui::test] -// async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) { -// let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]); -// init_test(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), -// show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)), -// show_other_hints: allowed_hint_kinds.contains(&None), -// }) -// }); - -// let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; -// let lsp_request_count = Arc::new(AtomicU32::new(0)); -// let another_lsp_request_count = Arc::clone(&lsp_request_count); -// fake_server -// .handle_request::(move |params, _| { -// let task_lsp_request_count = Arc::clone(&another_lsp_request_count); -// async move { -// Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst); -// assert_eq!( -// params.text_document.uri, -// lsp::Url::from_file_path(file_with_hints).unwrap(), -// ); -// Ok(Some(vec![ -// lsp::InlayHint { -// position: lsp::Position::new(0, 1), -// label: lsp::InlayHintLabel::String("type hint".to_string()), -// kind: Some(lsp::InlayHintKind::TYPE), -// text_edits: None, -// tooltip: None, -// padding_left: None, -// padding_right: None, -// data: None, -// }, -// lsp::InlayHint { -// position: lsp::Position::new(0, 2), -// label: lsp::InlayHintLabel::String("parameter hint".to_string()), -// kind: Some(lsp::InlayHintKind::PARAMETER), -// text_edits: None, -// tooltip: None, -// padding_left: None, -// padding_right: None, -// data: None, -// }, -// lsp::InlayHint { -// position: lsp::Position::new(0, 3), -// label: lsp::InlayHintLabel::String("other hint".to_string()), -// kind: None, -// text_edits: None, -// tooltip: None, -// padding_left: None, -// padding_right: None, -// data: None, -// }, -// ])) -// } -// }) -// .next() -// .await; -// cx.foreground().run_until_parked(); - -// let mut edits_made = 1; -// editor.update(cx, |editor, cx| { -// assert_eq!( -// lsp_request_count.load(Ordering::Relaxed), -// 1, -// "Should query new hints once" -// ); -// assert_eq!( -// vec![ -// "other hint".to_string(), -// "parameter hint".to_string(), -// "type hint".to_string(), -// ], -// cached_hint_labels(editor), -// "Should get its first hints when opening the editor" -// ); -// assert_eq!( -// vec!["other hint".to_string(), "type hint".to_string()], -// visible_hint_labels(editor, cx) -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!( -// inlay_cache.allowed_hint_kinds, allowed_hint_kinds, -// "Cache should use editor settings to get the allowed hint kinds" -// ); -// assert_eq!( -// inlay_cache.version, edits_made, -// "The editor update the cache version after every cache/view change" -// ); -// }); - -// fake_server -// .request::(()) -// .await -// .expect("inlay refresh request failed"); -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// assert_eq!( -// lsp_request_count.load(Ordering::Relaxed), -// 2, -// "Should load new hints twice" -// ); -// assert_eq!( -// vec![ -// "other hint".to_string(), -// "parameter hint".to_string(), -// "type hint".to_string(), -// ], -// cached_hint_labels(editor), -// "Cached hints should not change due to allowed hint kinds settings update" -// ); -// assert_eq!( -// vec!["other hint".to_string(), "type hint".to_string()], -// visible_hint_labels(editor, cx) -// ); -// assert_eq!( -// editor.inlay_hint_cache().version, -// edits_made, -// "Should not update cache version due to new loaded hints being the same" -// ); -// }); - -// for (new_allowed_hint_kinds, expected_visible_hints) in [ -// (HashSet::from_iter([None]), vec!["other hint".to_string()]), -// ( -// HashSet::from_iter([Some(InlayHintKind::Type)]), -// vec!["type hint".to_string()], -// ), -// ( -// HashSet::from_iter([Some(InlayHintKind::Parameter)]), -// vec!["parameter hint".to_string()], -// ), -// ( -// HashSet::from_iter([None, Some(InlayHintKind::Type)]), -// vec!["other hint".to_string(), "type hint".to_string()], -// ), -// ( -// HashSet::from_iter([None, Some(InlayHintKind::Parameter)]), -// vec!["other hint".to_string(), "parameter hint".to_string()], -// ), -// ( -// HashSet::from_iter([Some(InlayHintKind::Type), Some(InlayHintKind::Parameter)]), -// vec!["parameter hint".to_string(), "type hint".to_string()], -// ), -// ( -// HashSet::from_iter([ -// None, -// Some(InlayHintKind::Type), -// Some(InlayHintKind::Parameter), -// ]), -// vec![ -// "other hint".to_string(), -// "parameter hint".to_string(), -// "type hint".to_string(), -// ], -// ), -// ] { -// edits_made += 1; -// update_test_language_settings(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: new_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), -// show_parameter_hints: new_allowed_hint_kinds -// .contains(&Some(InlayHintKind::Parameter)), -// show_other_hints: new_allowed_hint_kinds.contains(&None), -// }) -// }); -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// assert_eq!( -// lsp_request_count.load(Ordering::Relaxed), -// 2, -// "Should not load new hints on allowed hint kinds change for hint kinds {new_allowed_hint_kinds:?}" -// ); -// assert_eq!( -// vec![ -// "other hint".to_string(), -// "parameter hint".to_string(), -// "type hint".to_string(), -// ], -// cached_hint_labels(editor), -// "Should get its cached hints unchanged after the settings change for hint kinds {new_allowed_hint_kinds:?}" -// ); -// assert_eq!( -// expected_visible_hints, -// visible_hint_labels(editor, cx), -// "Should get its visible hints filtered after the settings change for hint kinds {new_allowed_hint_kinds:?}" -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!( -// inlay_cache.allowed_hint_kinds, new_allowed_hint_kinds, -// "Cache should use editor settings to get the allowed hint kinds for hint kinds {new_allowed_hint_kinds:?}" -// ); -// assert_eq!( -// inlay_cache.version, edits_made, -// "The editor should update the cache version after every cache/view change for hint kinds {new_allowed_hint_kinds:?} due to visible hints change" -// ); -// }); -// } - -// edits_made += 1; -// let another_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Type)]); -// update_test_language_settings(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: false, -// show_type_hints: another_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), -// show_parameter_hints: another_allowed_hint_kinds -// .contains(&Some(InlayHintKind::Parameter)), -// show_other_hints: another_allowed_hint_kinds.contains(&None), -// }) -// }); -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// assert_eq!( -// lsp_request_count.load(Ordering::Relaxed), -// 2, -// "Should not load new hints when hints got disabled" -// ); -// assert!( -// cached_hint_labels(editor).is_empty(), -// "Should clear the cache when hints got disabled" -// ); -// assert!( -// visible_hint_labels(editor, cx).is_empty(), -// "Should clear visible hints when hints got disabled" -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!( -// inlay_cache.allowed_hint_kinds, another_allowed_hint_kinds, -// "Should update its allowed hint kinds even when hints got disabled" -// ); -// assert_eq!( -// inlay_cache.version, edits_made, -// "The editor should update the cache version after hints got disabled" -// ); -// }); - -// fake_server -// .request::(()) -// .await -// .expect("inlay refresh request failed"); -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// assert_eq!( -// lsp_request_count.load(Ordering::Relaxed), -// 2, -// "Should not load new hints when they got disabled" -// ); -// assert!(cached_hint_labels(editor).is_empty()); -// assert!(visible_hint_labels(editor, cx).is_empty()); -// assert_eq!( -// editor.inlay_hint_cache().version, edits_made, -// "The editor should not update the cache version after /refresh query without updates" -// ); -// }); - -// let final_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Parameter)]); -// edits_made += 1; -// update_test_language_settings(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: final_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), -// show_parameter_hints: final_allowed_hint_kinds -// .contains(&Some(InlayHintKind::Parameter)), -// show_other_hints: final_allowed_hint_kinds.contains(&None), -// }) -// }); -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// assert_eq!( -// lsp_request_count.load(Ordering::Relaxed), -// 3, -// "Should query for new hints when they got reenabled" -// ); -// assert_eq!( -// vec![ -// "other hint".to_string(), -// "parameter hint".to_string(), -// "type hint".to_string(), -// ], -// cached_hint_labels(editor), -// "Should get its cached hints fully repopulated after the hints got reenabled" -// ); -// assert_eq!( -// vec!["parameter hint".to_string()], -// visible_hint_labels(editor, cx), -// "Should get its visible hints repopulated and filtered after the h" -// ); -// let inlay_cache = editor.inlay_hint_cache(); -// assert_eq!( -// inlay_cache.allowed_hint_kinds, final_allowed_hint_kinds, -// "Cache should update editor settings when hints got reenabled" -// ); -// assert_eq!( -// inlay_cache.version, edits_made, -// "Cache should update its version after hints got reenabled" -// ); -// }); - -// fake_server -// .request::(()) -// .await -// .expect("inlay refresh request failed"); -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// assert_eq!( -// lsp_request_count.load(Ordering::Relaxed), -// 4, -// "Should query for new hints again" -// ); -// assert_eq!( -// vec![ -// "other hint".to_string(), -// "parameter hint".to_string(), -// "type hint".to_string(), -// ], -// cached_hint_labels(editor), -// ); -// assert_eq!( -// vec!["parameter hint".to_string()], -// visible_hint_labels(editor, cx), -// ); -// assert_eq!(editor.inlay_hint_cache().version, edits_made); -// }); -// } - -// #[gpui::test] -// async fn test_hint_request_cancellation(cx: &mut gpui::TestAppContext) { -// init_test(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: true, -// show_parameter_hints: true, -// show_other_hints: true, -// }) -// }); - -// let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; -// let fake_server = Arc::new(fake_server); -// let lsp_request_count = Arc::new(AtomicU32::new(0)); -// let another_lsp_request_count = Arc::clone(&lsp_request_count); -// fake_server -// .handle_request::(move |params, _| { -// let task_lsp_request_count = Arc::clone(&another_lsp_request_count); -// async move { -// let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1; -// assert_eq!( -// params.text_document.uri, -// lsp::Url::from_file_path(file_with_hints).unwrap(), -// ); -// Ok(Some(vec![lsp::InlayHint { -// position: lsp::Position::new(0, i), -// label: lsp::InlayHintLabel::String(i.to_string()), -// kind: None, -// text_edits: None, -// tooltip: None, -// padding_left: None, -// padding_right: None, -// data: None, -// }])) -// } -// }) -// .next() -// .await; - -// let mut expected_changes = Vec::new(); -// for change_after_opening in [ -// "initial change #1", -// "initial change #2", -// "initial change #3", -// ] { -// editor.update(cx, |editor, cx| { -// editor.change_selections(None, cx, |s| s.select_ranges([13..13])); -// editor.handle_input(change_after_opening, cx); -// }); -// expected_changes.push(change_after_opening); -// } - -// cx.foreground().run_until_parked(); - -// editor.update(cx, |editor, cx| { -// let current_text = editor.text(cx); -// for change in &expected_changes { -// assert!( -// current_text.contains(change), -// "Should apply all changes made" -// ); -// } -// assert_eq!( -// lsp_request_count.load(Ordering::Relaxed), -// 2, -// "Should query new hints twice: for editor init and for the last edit that interrupted all others" -// ); -// let expected_hints = vec!["2".to_string()]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "Should get hints from the last edit landed only" -// ); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// assert_eq!( -// editor.inlay_hint_cache().version, 1, -// "Only one update should be registered in the cache after all cancellations" -// ); -// }); - -// let mut edits = Vec::new(); -// for async_later_change in [ -// "another change #1", -// "another change #2", -// "another change #3", -// ] { -// expected_changes.push(async_later_change); -// let task_editor = editor.clone(); -// let mut task_cx = cx.clone(); -// edits.push(cx.foreground().spawn(async move { -// task_editor.update(&mut task_cx, |editor, cx| { -// editor.change_selections(None, cx, |s| s.select_ranges([13..13])); -// editor.handle_input(async_later_change, cx); -// }); -// })); -// } -// let _ = future::join_all(edits).await; -// cx.foreground().run_until_parked(); - -// editor.update(cx, |editor, cx| { -// let current_text = editor.text(cx); -// for change in &expected_changes { -// assert!( -// current_text.contains(change), -// "Should apply all changes made" -// ); -// } -// assert_eq!( -// lsp_request_count.load(Ordering::SeqCst), -// 3, -// "Should query new hints one more time, for the last edit only" -// ); -// let expected_hints = vec!["3".to_string()]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "Should get hints from the last edit landed only" -// ); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// assert_eq!( -// editor.inlay_hint_cache().version, -// 2, -// "Should update the cache version once more, for the new change" -// ); -// }); -// } - -// #[gpui::test(iterations = 10)] -// async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) { -// init_test(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: true, -// show_parameter_hints: true, -// show_other_hints: true, -// }) -// }); - -// let mut language = Language::new( -// LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ); -// let mut fake_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// capabilities: lsp::ServerCapabilities { -// inlay_hint_provider: Some(lsp::OneOf::Left(true)), -// ..Default::default() -// }, -// ..Default::default() -// })) -// .await; -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/a", -// json!({ -// "main.rs": format!("fn main() {{\n{}\n}}", "let i = 5;\n".repeat(500)), -// "other.rs": "// Test file", -// }), -// ) -// .await; -// let project = Project::test(fs, ["/a".as_ref()], cx).await; -// project.update(cx, |project, _| project.languages().add(Arc::new(language))); -// let workspace = cx -// .add_window(|cx| Workspace::test_new(project.clone(), cx)) -// .root(cx); -// let worktree_id = workspace.update(cx, |workspace, cx| { -// workspace.project().read_with(cx, |project, cx| { -// project.worktrees(cx).next().unwrap().read(cx).id() -// }) -// }); - -// let _buffer = project -// .update(cx, |project, cx| { -// project.open_local_buffer("/a/main.rs", cx) -// }) -// .await -// .unwrap(); -// cx.foreground().run_until_parked(); -// cx.foreground().start_waiting(); -// let fake_server = fake_servers.next().await.unwrap(); -// let editor = workspace -// .update(cx, |workspace, cx| { -// workspace.open_path((worktree_id, "main.rs"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); -// let lsp_request_ranges = Arc::new(Mutex::new(Vec::new())); -// let lsp_request_count = Arc::new(AtomicUsize::new(0)); -// let closure_lsp_request_ranges = Arc::clone(&lsp_request_ranges); -// let closure_lsp_request_count = Arc::clone(&lsp_request_count); -// fake_server -// .handle_request::(move |params, _| { -// let task_lsp_request_ranges = Arc::clone(&closure_lsp_request_ranges); -// let task_lsp_request_count = Arc::clone(&closure_lsp_request_count); -// async move { -// assert_eq!( -// params.text_document.uri, -// lsp::Url::from_file_path("/a/main.rs").unwrap(), -// ); - -// task_lsp_request_ranges.lock().push(params.range); -// let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::Release) + 1; -// Ok(Some(vec![lsp::InlayHint { -// position: params.range.end, -// label: lsp::InlayHintLabel::String(i.to_string()), -// kind: None, -// text_edits: None, -// tooltip: None, -// padding_left: None, -// padding_right: None, -// data: None, -// }])) -// } -// }) -// .next() -// .await; -// fn editor_visible_range( -// editor: &ViewHandle, -// cx: &mut gpui::TestAppContext, -// ) -> Range { -// let ranges = editor.update(cx, |editor, cx| editor.excerpt_visible_offsets(None, cx)); -// assert_eq!( -// ranges.len(), -// 1, -// "Single buffer should produce a single excerpt with visible range" -// ); -// let (_, (excerpt_buffer, _, excerpt_visible_range)) = -// ranges.into_iter().next().unwrap(); -// excerpt_buffer.update(cx, |buffer, _| { -// let snapshot = buffer.snapshot(); -// let start = buffer -// .anchor_before(excerpt_visible_range.start) -// .to_point(&snapshot); -// let end = buffer -// .anchor_after(excerpt_visible_range.end) -// .to_point(&snapshot); -// start..end -// }) -// } - -// // in large buffers, requests are made for more than visible range of a buffer. -// // invisible parts are queried later, to avoid excessive requests on quick typing. -// // wait the timeout needed to get all requests. -// cx.foreground().advance_clock(Duration::from_millis( -// INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, -// )); -// cx.foreground().run_until_parked(); -// let initial_visible_range = editor_visible_range(&editor, cx); -// let lsp_initial_visible_range = lsp::Range::new( -// lsp::Position::new( -// initial_visible_range.start.row, -// initial_visible_range.start.column, -// ), -// lsp::Position::new( -// initial_visible_range.end.row, -// initial_visible_range.end.column, -// ), -// ); -// let expected_initial_query_range_end = -// lsp::Position::new(initial_visible_range.end.row * 2, 2); -// let mut expected_invisible_query_start = lsp_initial_visible_range.end; -// expected_invisible_query_start.character += 1; -// editor.update(cx, |editor, cx| { -// let ranges = lsp_request_ranges.lock().drain(..).collect::>(); -// assert_eq!(ranges.len(), 2, -// "When scroll is at the edge of a big document, its visible part and the same range further should be queried in order, but got: {ranges:?}"); -// let visible_query_range = &ranges[0]; -// assert_eq!(visible_query_range.start, lsp_initial_visible_range.start); -// assert_eq!(visible_query_range.end, lsp_initial_visible_range.end); -// let invisible_query_range = &ranges[1]; - -// assert_eq!(invisible_query_range.start, expected_invisible_query_start, "Should initially query visible edge of the document"); -// assert_eq!(invisible_query_range.end, expected_initial_query_range_end, "Should initially query visible edge of the document"); - -// let requests_count = lsp_request_count.load(Ordering::Acquire); -// assert_eq!(requests_count, 2, "Visible + invisible request"); -// let expected_hints = vec!["1".to_string(), "2".to_string()]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "Should have hints from both LSP requests made for a big file" -// ); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx), "Should display only hints from the visible range"); -// assert_eq!( -// editor.inlay_hint_cache().version, requests_count, -// "LSP queries should've bumped the cache version" -// ); -// }); - -// editor.update(cx, |editor, cx| { -// editor.scroll_screen(&ScrollAmount::Page(1.0), cx); -// editor.scroll_screen(&ScrollAmount::Page(1.0), cx); -// }); -// cx.foreground().advance_clock(Duration::from_millis( -// INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, -// )); -// cx.foreground().run_until_parked(); -// let visible_range_after_scrolls = editor_visible_range(&editor, cx); -// let visible_line_count = -// editor.update(cx, |editor, _| editor.visible_line_count().unwrap()); -// let selection_in_cached_range = editor.update(cx, |editor, cx| { -// let ranges = lsp_request_ranges -// .lock() -// .drain(..) -// .sorted_by_key(|r| r.start) -// .collect::>(); -// assert_eq!( -// ranges.len(), -// 2, -// "Should query 2 ranges after both scrolls, but got: {ranges:?}" -// ); -// let first_scroll = &ranges[0]; -// let second_scroll = &ranges[1]; -// assert_eq!( -// first_scroll.end, second_scroll.start, -// "Should query 2 adjacent ranges after the scrolls, but got: {ranges:?}" -// ); -// assert_eq!( -// first_scroll.start, expected_initial_query_range_end, -// "First scroll should start the query right after the end of the original scroll", -// ); -// assert_eq!( -// second_scroll.end, -// lsp::Position::new( -// visible_range_after_scrolls.end.row -// + visible_line_count.ceil() as u32, -// 1, -// ), -// "Second scroll should query one more screen down after the end of the visible range" -// ); - -// let lsp_requests = lsp_request_count.load(Ordering::Acquire); -// assert_eq!(lsp_requests, 4, "Should query for hints after every scroll"); -// let expected_hints = vec![ -// "1".to_string(), -// "2".to_string(), -// "3".to_string(), -// "4".to_string(), -// ]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "Should have hints from the new LSP response after the edit" -// ); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// assert_eq!( -// editor.inlay_hint_cache().version, -// lsp_requests, -// "Should update the cache for every LSP response with hints added" -// ); - -// let mut selection_in_cached_range = visible_range_after_scrolls.end; -// selection_in_cached_range.row -= visible_line_count.ceil() as u32; -// selection_in_cached_range -// }); - -// editor.update(cx, |editor, cx| { -// editor.change_selections(Some(Autoscroll::center()), cx, |s| { -// s.select_ranges([selection_in_cached_range..selection_in_cached_range]) -// }); -// }); -// cx.foreground().advance_clock(Duration::from_millis( -// INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, -// )); -// cx.foreground().run_until_parked(); -// editor.update(cx, |_, _| { -// let ranges = lsp_request_ranges -// .lock() -// .drain(..) -// .sorted_by_key(|r| r.start) -// .collect::>(); -// assert!(ranges.is_empty(), "No new ranges or LSP queries should be made after returning to the selection with cached hints"); -// assert_eq!(lsp_request_count.load(Ordering::Acquire), 4); -// }); - -// editor.update(cx, |editor, cx| { -// editor.handle_input("++++more text++++", cx); -// }); -// cx.foreground().advance_clock(Duration::from_millis( -// INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, -// )); -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// let mut ranges = lsp_request_ranges.lock().drain(..).collect::>(); -// ranges.sort_by_key(|r| r.start); - -// assert_eq!(ranges.len(), 3, -// "On edit, should scroll to selection and query a range around it: visible + same range above and below. Instead, got query ranges {ranges:?}"); -// let above_query_range = &ranges[0]; -// let visible_query_range = &ranges[1]; -// let below_query_range = &ranges[2]; -// assert!(above_query_range.end.character < visible_query_range.start.character || above_query_range.end.line + 1 == visible_query_range.start.line, -// "Above range {above_query_range:?} should be before visible range {visible_query_range:?}"); -// assert!(visible_query_range.end.character < below_query_range.start.character || visible_query_range.end.line + 1 == below_query_range.start.line, -// "Visible range {visible_query_range:?} should be before below range {below_query_range:?}"); -// assert!(above_query_range.start.line < selection_in_cached_range.row, -// "Hints should be queried with the selected range after the query range start"); -// assert!(below_query_range.end.line > selection_in_cached_range.row, -// "Hints should be queried with the selected range before the query range end"); -// assert!(above_query_range.start.line <= selection_in_cached_range.row - (visible_line_count * 3.0 / 2.0) as u32, -// "Hints query range should contain one more screen before"); -// assert!(below_query_range.end.line >= selection_in_cached_range.row + (visible_line_count * 3.0 / 2.0) as u32, -// "Hints query range should contain one more screen after"); - -// let lsp_requests = lsp_request_count.load(Ordering::Acquire); -// assert_eq!(lsp_requests, 7, "There should be a visible range and two ranges above and below it queried"); -// let expected_hints = vec!["5".to_string(), "6".to_string(), "7".to_string()]; -// assert_eq!(expected_hints, cached_hint_labels(editor), -// "Should have hints from the new LSP response after the edit"); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// assert_eq!(editor.inlay_hint_cache().version, lsp_requests, "Should update the cache for every LSP response with hints added"); -// }); -// } - -// #[gpui::test(iterations = 10)] -// async fn test_multiple_excerpts_large_multibuffer( -// deterministic: Arc, -// cx: &mut gpui::TestAppContext, -// ) { -// init_test(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: true, -// show_parameter_hints: true, -// show_other_hints: true, -// }) -// }); - -// let mut language = Language::new( -// LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ); -// let mut fake_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// capabilities: lsp::ServerCapabilities { -// inlay_hint_provider: Some(lsp::OneOf::Left(true)), -// ..Default::default() -// }, -// ..Default::default() -// })) -// .await; -// let language = Arc::new(language); -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/a", -// json!({ -// "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::>().join("")), -// "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::>().join("")), -// }), -// ) -// .await; -// let project = Project::test(fs, ["/a".as_ref()], cx).await; -// project.update(cx, |project, _| { -// project.languages().add(Arc::clone(&language)) -// }); -// let workspace = cx -// .add_window(|cx| Workspace::test_new(project.clone(), cx)) -// .root(cx); -// let worktree_id = workspace.update(cx, |workspace, cx| { -// workspace.project().read_with(cx, |project, cx| { -// project.worktrees(cx).next().unwrap().read(cx).id() -// }) -// }); - -// let buffer_1 = project -// .update(cx, |project, cx| { -// project.open_buffer((worktree_id, "main.rs"), cx) -// }) -// .await -// .unwrap(); -// let buffer_2 = project -// .update(cx, |project, cx| { -// project.open_buffer((worktree_id, "other.rs"), cx) -// }) -// .await -// .unwrap(); -// let multibuffer = cx.add_model(|cx| { -// let mut multibuffer = MultiBuffer::new(0); -// multibuffer.push_excerpts( -// buffer_1.clone(), -// [ -// ExcerptRange { -// context: Point::new(0, 0)..Point::new(2, 0), -// primary: None, -// }, -// ExcerptRange { -// context: Point::new(4, 0)..Point::new(11, 0), -// primary: None, -// }, -// ExcerptRange { -// context: Point::new(22, 0)..Point::new(33, 0), -// primary: None, -// }, -// ExcerptRange { -// context: Point::new(44, 0)..Point::new(55, 0), -// primary: None, -// }, -// ExcerptRange { -// context: Point::new(56, 0)..Point::new(66, 0), -// primary: None, -// }, -// ExcerptRange { -// context: Point::new(67, 0)..Point::new(77, 0), -// primary: None, -// }, -// ], -// cx, -// ); -// multibuffer.push_excerpts( -// buffer_2.clone(), -// [ -// ExcerptRange { -// context: Point::new(0, 1)..Point::new(2, 1), -// primary: None, -// }, -// ExcerptRange { -// context: Point::new(4, 1)..Point::new(11, 1), -// primary: None, -// }, -// ExcerptRange { -// context: Point::new(22, 1)..Point::new(33, 1), -// primary: None, -// }, -// ExcerptRange { -// context: Point::new(44, 1)..Point::new(55, 1), -// primary: None, -// }, -// ExcerptRange { -// context: Point::new(56, 1)..Point::new(66, 1), -// primary: None, -// }, -// ExcerptRange { -// context: Point::new(67, 1)..Point::new(77, 1), -// primary: None, -// }, -// ], -// cx, -// ); -// multibuffer -// }); - -// deterministic.run_until_parked(); -// cx.foreground().run_until_parked(); -// let editor = cx -// .add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx)) -// .root(cx); -// let editor_edited = Arc::new(AtomicBool::new(false)); -// let fake_server = fake_servers.next().await.unwrap(); -// let closure_editor_edited = Arc::clone(&editor_edited); -// fake_server -// .handle_request::(move |params, _| { -// let task_editor_edited = Arc::clone(&closure_editor_edited); -// async move { -// let hint_text = if params.text_document.uri -// == lsp::Url::from_file_path("/a/main.rs").unwrap() -// { -// "main hint" -// } else if params.text_document.uri -// == lsp::Url::from_file_path("/a/other.rs").unwrap() -// { -// "other hint" -// } else { -// panic!("unexpected uri: {:?}", params.text_document.uri); -// }; - -// // one hint per excerpt -// let positions = [ -// lsp::Position::new(0, 2), -// lsp::Position::new(4, 2), -// lsp::Position::new(22, 2), -// lsp::Position::new(44, 2), -// lsp::Position::new(56, 2), -// lsp::Position::new(67, 2), -// ]; -// let out_of_range_hint = lsp::InlayHint { -// position: lsp::Position::new( -// params.range.start.line + 99, -// params.range.start.character + 99, -// ), -// label: lsp::InlayHintLabel::String( -// "out of excerpt range, should be ignored".to_string(), -// ), -// kind: None, -// text_edits: None, -// tooltip: None, -// padding_left: None, -// padding_right: None, -// data: None, -// }; - -// let edited = task_editor_edited.load(Ordering::Acquire); -// Ok(Some( -// std::iter::once(out_of_range_hint) -// .chain(positions.into_iter().enumerate().map(|(i, position)| { -// lsp::InlayHint { -// position, -// label: lsp::InlayHintLabel::String(format!( -// "{hint_text}{} #{i}", -// if edited { "(edited)" } else { "" }, -// )), -// kind: None, -// text_edits: None, -// tooltip: None, -// padding_left: None, -// padding_right: None, -// data: None, -// } -// })) -// .collect(), -// )) -// } -// }) -// .next() -// .await; -// cx.foreground().run_until_parked(); - -// editor.update(cx, |editor, cx| { -// let expected_hints = vec![ -// "main hint #0".to_string(), -// "main hint #1".to_string(), -// "main hint #2".to_string(), -// "main hint #3".to_string(), -// ]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints" -// ); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), "Every visible excerpt hints should bump the verison"); -// }); - -// editor.update(cx, |editor, cx| { -// editor.change_selections(Some(Autoscroll::Next), cx, |s| { -// s.select_ranges([Point::new(4, 0)..Point::new(4, 0)]) -// }); -// editor.change_selections(Some(Autoscroll::Next), cx, |s| { -// s.select_ranges([Point::new(22, 0)..Point::new(22, 0)]) -// }); -// editor.change_selections(Some(Autoscroll::Next), cx, |s| { -// s.select_ranges([Point::new(50, 0)..Point::new(50, 0)]) -// }); -// }); -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// let expected_hints = vec![ -// "main hint #0".to_string(), -// "main hint #1".to_string(), -// "main hint #2".to_string(), -// "main hint #3".to_string(), -// "main hint #4".to_string(), -// "main hint #5".to_string(), -// "other hint #0".to_string(), -// "other hint #1".to_string(), -// "other hint #2".to_string(), -// ]; -// assert_eq!(expected_hints, cached_hint_labels(editor), -// "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits"); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), -// "Due to every excerpt having one hint, we update cache per new excerpt scrolled"); -// }); - -// editor.update(cx, |editor, cx| { -// editor.change_selections(Some(Autoscroll::Next), cx, |s| { -// s.select_ranges([Point::new(100, 0)..Point::new(100, 0)]) -// }); -// }); -// cx.foreground().advance_clock(Duration::from_millis( -// INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, -// )); -// cx.foreground().run_until_parked(); -// let last_scroll_update_version = editor.update(cx, |editor, cx| { -// let expected_hints = vec![ -// "main hint #0".to_string(), -// "main hint #1".to_string(), -// "main hint #2".to_string(), -// "main hint #3".to_string(), -// "main hint #4".to_string(), -// "main hint #5".to_string(), -// "other hint #0".to_string(), -// "other hint #1".to_string(), -// "other hint #2".to_string(), -// "other hint #3".to_string(), -// "other hint #4".to_string(), -// "other hint #5".to_string(), -// ]; -// assert_eq!(expected_hints, cached_hint_labels(editor), -// "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched"); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// assert_eq!(editor.inlay_hint_cache().version, expected_hints.len()); -// expected_hints.len() -// }); - -// editor.update(cx, |editor, cx| { -// editor.change_selections(Some(Autoscroll::Next), cx, |s| { -// s.select_ranges([Point::new(4, 0)..Point::new(4, 0)]) -// }); -// }); -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// let expected_hints = vec![ -// "main hint #0".to_string(), -// "main hint #1".to_string(), -// "main hint #2".to_string(), -// "main hint #3".to_string(), -// "main hint #4".to_string(), -// "main hint #5".to_string(), -// "other hint #0".to_string(), -// "other hint #1".to_string(), -// "other hint #2".to_string(), -// "other hint #3".to_string(), -// "other hint #4".to_string(), -// "other hint #5".to_string(), -// ]; -// assert_eq!(expected_hints, cached_hint_labels(editor), -// "After multibuffer was scrolled to the end, further scrolls up should not bring more hints"); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// assert_eq!(editor.inlay_hint_cache().version, last_scroll_update_version, "No updates should happen during scrolling already scolled buffer"); -// }); - -// editor_edited.store(true, Ordering::Release); -// editor.update(cx, |editor, cx| { -// editor.change_selections(None, cx, |s| { -// s.select_ranges([Point::new(56, 0)..Point::new(56, 0)]) -// }); -// editor.handle_input("++++more text++++", cx); -// }); -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// let expected_hints = vec![ -// "main hint(edited) #0".to_string(), -// "main hint(edited) #1".to_string(), -// "main hint(edited) #2".to_string(), -// "main hint(edited) #3".to_string(), -// "main hint(edited) #4".to_string(), -// "main hint(edited) #5".to_string(), -// "other hint(edited) #0".to_string(), -// "other hint(edited) #1".to_string(), -// ]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "After multibuffer edit, editor gets scolled back to the last selection; \ -// all hints should be invalidated and requeried for all of its visible excerpts" -// ); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - -// let current_cache_version = editor.inlay_hint_cache().version; -// let minimum_expected_version = last_scroll_update_version + expected_hints.len(); -// assert!( -// current_cache_version == minimum_expected_version || current_cache_version == minimum_expected_version + 1, -// "Due to every excerpt having one hint, cache should update per new excerpt received + 1 potential sporadic update" -// ); -// }); -// } - -// #[gpui::test] -// async fn test_excerpts_removed( -// deterministic: Arc, -// cx: &mut gpui::TestAppContext, -// ) { -// init_test(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: false, -// show_parameter_hints: false, -// show_other_hints: false, -// }) -// }); - -// let mut language = Language::new( -// LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ); -// let mut fake_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// capabilities: lsp::ServerCapabilities { -// inlay_hint_provider: Some(lsp::OneOf::Left(true)), -// ..Default::default() -// }, -// ..Default::default() -// })) -// .await; -// let language = Arc::new(language); -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/a", -// json!({ -// "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::>().join("")), -// "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::>().join("")), -// }), -// ) -// .await; -// let project = Project::test(fs, ["/a".as_ref()], cx).await; -// project.update(cx, |project, _| { -// project.languages().add(Arc::clone(&language)) -// }); -// let workspace = cx -// .add_window(|cx| Workspace::test_new(project.clone(), cx)) -// .root(cx); -// let worktree_id = workspace.update(cx, |workspace, cx| { -// workspace.project().read_with(cx, |project, cx| { -// project.worktrees(cx).next().unwrap().read(cx).id() -// }) -// }); - -// let buffer_1 = project -// .update(cx, |project, cx| { -// project.open_buffer((worktree_id, "main.rs"), cx) -// }) -// .await -// .unwrap(); -// let buffer_2 = project -// .update(cx, |project, cx| { -// project.open_buffer((worktree_id, "other.rs"), cx) -// }) -// .await -// .unwrap(); -// let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); -// let (buffer_1_excerpts, buffer_2_excerpts) = multibuffer.update(cx, |multibuffer, cx| { -// let buffer_1_excerpts = multibuffer.push_excerpts( -// buffer_1.clone(), -// [ExcerptRange { -// context: Point::new(0, 0)..Point::new(2, 0), -// primary: None, -// }], -// cx, -// ); -// let buffer_2_excerpts = multibuffer.push_excerpts( -// buffer_2.clone(), -// [ExcerptRange { -// context: Point::new(0, 1)..Point::new(2, 1), -// primary: None, -// }], -// cx, -// ); -// (buffer_1_excerpts, buffer_2_excerpts) -// }); - -// assert!(!buffer_1_excerpts.is_empty()); -// assert!(!buffer_2_excerpts.is_empty()); - -// deterministic.run_until_parked(); -// cx.foreground().run_until_parked(); -// let editor = cx -// .add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx)) -// .root(cx); -// let editor_edited = Arc::new(AtomicBool::new(false)); -// let fake_server = fake_servers.next().await.unwrap(); -// let closure_editor_edited = Arc::clone(&editor_edited); -// fake_server -// .handle_request::(move |params, _| { -// let task_editor_edited = Arc::clone(&closure_editor_edited); -// async move { -// let hint_text = if params.text_document.uri -// == lsp::Url::from_file_path("/a/main.rs").unwrap() -// { -// "main hint" -// } else if params.text_document.uri -// == lsp::Url::from_file_path("/a/other.rs").unwrap() -// { -// "other hint" -// } else { -// panic!("unexpected uri: {:?}", params.text_document.uri); -// }; - -// let positions = [ -// lsp::Position::new(0, 2), -// lsp::Position::new(4, 2), -// lsp::Position::new(22, 2), -// lsp::Position::new(44, 2), -// lsp::Position::new(56, 2), -// lsp::Position::new(67, 2), -// ]; -// let out_of_range_hint = lsp::InlayHint { -// position: lsp::Position::new( -// params.range.start.line + 99, -// params.range.start.character + 99, -// ), -// label: lsp::InlayHintLabel::String( -// "out of excerpt range, should be ignored".to_string(), -// ), -// kind: None, -// text_edits: None, -// tooltip: None, -// padding_left: None, -// padding_right: None, -// data: None, -// }; - -// let edited = task_editor_edited.load(Ordering::Acquire); -// Ok(Some( -// std::iter::once(out_of_range_hint) -// .chain(positions.into_iter().enumerate().map(|(i, position)| { -// lsp::InlayHint { -// position, -// label: lsp::InlayHintLabel::String(format!( -// "{hint_text}{} #{i}", -// if edited { "(edited)" } else { "" }, -// )), -// kind: None, -// text_edits: None, -// tooltip: None, -// padding_left: None, -// padding_right: None, -// data: None, -// } -// })) -// .collect(), -// )) -// } -// }) -// .next() -// .await; -// cx.foreground().run_until_parked(); - -// editor.update(cx, |editor, cx| { -// assert_eq!( -// vec!["main hint #0".to_string(), "other hint #0".to_string()], -// cached_hint_labels(editor), -// "Cache should update for both excerpts despite hints display was disabled" -// ); -// assert!( -// visible_hint_labels(editor, cx).is_empty(), -// "All hints are disabled and should not be shown despite being present in the cache" -// ); -// assert_eq!( -// editor.inlay_hint_cache().version, -// 2, -// "Cache should update once per excerpt query" -// ); -// }); - -// editor.update(cx, |editor, cx| { -// editor.buffer().update(cx, |multibuffer, cx| { -// multibuffer.remove_excerpts(buffer_2_excerpts, cx) -// }) -// }); -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// assert_eq!( -// vec!["main hint #0".to_string()], -// cached_hint_labels(editor), -// "For the removed excerpt, should clean corresponding cached hints" -// ); -// assert!( -// visible_hint_labels(editor, cx).is_empty(), -// "All hints are disabled and should not be shown despite being present in the cache" -// ); -// assert_eq!( -// editor.inlay_hint_cache().version, -// 3, -// "Excerpt removal should trigger a cache update" -// ); -// }); - -// update_test_language_settings(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: true, -// show_parameter_hints: true, -// show_other_hints: true, -// }) -// }); -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// let expected_hints = vec!["main hint #0".to_string()]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "Hint display settings change should not change the cache" -// ); -// assert_eq!( -// expected_hints, -// visible_hint_labels(editor, cx), -// "Settings change should make cached hints visible" -// ); -// assert_eq!( -// editor.inlay_hint_cache().version, -// 4, -// "Settings change should trigger a cache update" -// ); -// }); -// } - -// #[gpui::test] -// async fn test_inside_char_boundary_range_hints(cx: &mut gpui::TestAppContext) { -// init_test(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: true, -// show_parameter_hints: true, -// show_other_hints: true, -// }) -// }); - -// let mut language = Language::new( -// LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ); -// let mut fake_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// capabilities: lsp::ServerCapabilities { -// inlay_hint_provider: Some(lsp::OneOf::Left(true)), -// ..Default::default() -// }, -// ..Default::default() -// })) -// .await; -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/a", -// json!({ -// "main.rs": format!(r#"fn main() {{\n{}\n}}"#, format!("let i = {};\n", "√".repeat(10)).repeat(500)), -// "other.rs": "// Test file", -// }), -// ) -// .await; -// let project = Project::test(fs, ["/a".as_ref()], cx).await; -// project.update(cx, |project, _| project.languages().add(Arc::new(language))); -// let workspace = cx -// .add_window(|cx| Workspace::test_new(project.clone(), cx)) -// .root(cx); -// let worktree_id = workspace.update(cx, |workspace, cx| { -// workspace.project().read_with(cx, |project, cx| { -// project.worktrees(cx).next().unwrap().read(cx).id() -// }) -// }); - -// let _buffer = project -// .update(cx, |project, cx| { -// project.open_local_buffer("/a/main.rs", cx) -// }) -// .await -// .unwrap(); -// cx.foreground().run_until_parked(); -// cx.foreground().start_waiting(); -// let fake_server = fake_servers.next().await.unwrap(); -// let editor = workspace -// .update(cx, |workspace, cx| { -// workspace.open_path((worktree_id, "main.rs"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); -// let lsp_request_count = Arc::new(AtomicU32::new(0)); -// let closure_lsp_request_count = Arc::clone(&lsp_request_count); -// fake_server -// .handle_request::(move |params, _| { -// let task_lsp_request_count = Arc::clone(&closure_lsp_request_count); -// async move { -// assert_eq!( -// params.text_document.uri, -// lsp::Url::from_file_path("/a/main.rs").unwrap(), -// ); -// let query_start = params.range.start; -// let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::Release) + 1; -// Ok(Some(vec![lsp::InlayHint { -// position: query_start, -// label: lsp::InlayHintLabel::String(i.to_string()), -// kind: None, -// text_edits: None, -// tooltip: None, -// padding_left: None, -// padding_right: None, -// data: None, -// }])) -// } -// }) -// .next() -// .await; - -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// editor.change_selections(None, cx, |s| { -// s.select_ranges([Point::new(10, 0)..Point::new(10, 0)]) -// }) -// }); -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// let expected_hints = vec!["1".to_string()]; -// assert_eq!(expected_hints, cached_hint_labels(editor)); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// assert_eq!(editor.inlay_hint_cache().version, 1); -// }); -// } - -// #[gpui::test] -// async fn test_toggle_inlay_hints(cx: &mut gpui::TestAppContext) { -// init_test(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: false, -// show_type_hints: true, -// show_parameter_hints: true, -// show_other_hints: true, -// }) -// }); - -// let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; - -// editor.update(cx, |editor, cx| { -// editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx) -// }); -// cx.foreground().start_waiting(); -// let lsp_request_count = Arc::new(AtomicU32::new(0)); -// let closure_lsp_request_count = Arc::clone(&lsp_request_count); -// fake_server -// .handle_request::(move |params, _| { -// let task_lsp_request_count = Arc::clone(&closure_lsp_request_count); -// async move { -// assert_eq!( -// params.text_document.uri, -// lsp::Url::from_file_path(file_with_hints).unwrap(), -// ); - -// let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1; -// Ok(Some(vec![lsp::InlayHint { -// position: lsp::Position::new(0, i), -// label: lsp::InlayHintLabel::String(i.to_string()), -// kind: None, -// text_edits: None, -// tooltip: None, -// padding_left: None, -// padding_right: None, -// data: None, -// }])) -// } -// }) -// .next() -// .await; -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// let expected_hints = vec!["1".to_string()]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "Should display inlays after toggle despite them disabled in settings" -// ); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// assert_eq!( -// editor.inlay_hint_cache().version, -// 1, -// "First toggle should be cache's first update" -// ); -// }); - -// editor.update(cx, |editor, cx| { -// editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx) -// }); -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// assert!( -// cached_hint_labels(editor).is_empty(), -// "Should clear hints after 2nd toggle" -// ); -// assert!(visible_hint_labels(editor, cx).is_empty()); -// assert_eq!(editor.inlay_hint_cache().version, 2); -// }); - -// update_test_language_settings(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: true, -// show_parameter_hints: true, -// show_other_hints: true, -// }) -// }); -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// let expected_hints = vec!["2".to_string()]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "Should query LSP hints for the 2nd time after enabling hints in settings" -// ); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// assert_eq!(editor.inlay_hint_cache().version, 3); -// }); - -// editor.update(cx, |editor, cx| { -// editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx) -// }); -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// assert!( -// cached_hint_labels(editor).is_empty(), -// "Should clear hints after enabling in settings and a 3rd toggle" -// ); -// assert!(visible_hint_labels(editor, cx).is_empty()); -// assert_eq!(editor.inlay_hint_cache().version, 4); -// }); - -// editor.update(cx, |editor, cx| { -// editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx) -// }); -// cx.foreground().run_until_parked(); -// editor.update(cx, |editor, cx| { -// let expected_hints = vec!["3".to_string()]; -// assert_eq!( -// expected_hints, -// cached_hint_labels(editor), -// "Should query LSP hints for the 3rd time after enabling hints in settings and toggling them back on" -// ); -// assert_eq!(expected_hints, visible_hint_labels(editor, cx)); -// assert_eq!(editor.inlay_hint_cache().version, 5); -// }); -// } - -// pub(crate) fn init_test(cx: &mut TestAppContext, f: impl Fn(&mut AllLanguageSettingsContent)) { -// cx.foreground().forbid_parking(); - -// cx.update(|cx| { -// cx.set_global(SettingsStore::test(cx)); -// theme::init(cx); -// client::init_settings(cx); -// language::init(cx); -// Project::init_settings(cx); -// workspace::init_settings(cx); -// crate::init(cx); -// }); - -// update_test_language_settings(cx, f); -// } - -// async fn prepare_test_objects( -// cx: &mut TestAppContext, -// ) -> (&'static str, ViewHandle, FakeLanguageServer) { -// let mut language = Language::new( -// LanguageConfig { -// name: "Rust".into(), -// path_suffixes: vec!["rs".to_string()], -// ..Default::default() -// }, -// Some(tree_sitter_rust::language()), -// ); -// let mut fake_servers = language -// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { -// capabilities: lsp::ServerCapabilities { -// inlay_hint_provider: Some(lsp::OneOf::Left(true)), -// ..Default::default() -// }, -// ..Default::default() -// })) -// .await; - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/a", -// json!({ -// "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out", -// "other.rs": "// Test file", -// }), -// ) -// .await; - -// let project = Project::test(fs, ["/a".as_ref()], cx).await; -// project.update(cx, |project, _| project.languages().add(Arc::new(language))); -// let workspace = cx -// .add_window(|cx| Workspace::test_new(project.clone(), cx)) -// .root(cx); -// let worktree_id = workspace.update(cx, |workspace, cx| { -// workspace.project().read_with(cx, |project, cx| { -// project.worktrees(cx).next().unwrap().read(cx).id() -// }) -// }); - -// let _buffer = project -// .update(cx, |project, cx| { -// project.open_local_buffer("/a/main.rs", cx) -// }) -// .await -// .unwrap(); -// cx.foreground().run_until_parked(); -// cx.foreground().start_waiting(); -// let fake_server = fake_servers.next().await.unwrap(); -// let editor = workspace -// .update(cx, |workspace, cx| { -// workspace.open_path((worktree_id, "main.rs"), None, true, cx) -// }) -// .await -// .unwrap() -// .downcast::() -// .unwrap(); - -// editor.update(cx, |editor, cx| { -// assert!(cached_hint_labels(editor).is_empty()); -// assert!(visible_hint_labels(editor, cx).is_empty()); -// assert_eq!(editor.inlay_hint_cache().version, 0); -// }); - -// ("/a/main.rs", editor, fake_server) -// } - -// pub fn cached_hint_labels(editor: &Editor) -> Vec { -// let mut labels = Vec::new(); -// for (_, excerpt_hints) in &editor.inlay_hint_cache().hints { -// let excerpt_hints = excerpt_hints.read(); -// for id in &excerpt_hints.ordered_hints { -// labels.push(excerpt_hints.hints_by_id[id].text()); -// } -// } - -// labels.sort(); -// labels -// } - -// pub fn visible_hint_labels(editor: &Editor, cx: &ViewContext<'_, '_, Editor>) -> Vec { -// let mut hints = editor -// .visible_inlay_hints(cx) -// .into_iter() -// .map(|hint| hint.text.to_string()) -// .collect::>(); -// hints.sort(); -// hints -// } -// } diff --git a/crates/editor2/src/scroll/scroll_amount.rs b/crates/editor2/src/scroll/scroll_amount.rs index 89d188e324..2cb22d1516 100644 --- a/crates/editor2/src/scroll/scroll_amount.rs +++ b/crates/editor2/src/scroll/scroll_amount.rs @@ -11,19 +11,18 @@ pub enum ScrollAmount { impl ScrollAmount { pub fn lines(&self, editor: &mut Editor) -> f32 { - todo!() - // match self { - // Self::Line(count) => *count, - // Self::Page(count) => editor - // .visible_line_count() - // .map(|mut l| { - // // for full pages subtract one to leave an anchor line - // if count.abs() == 1.0 { - // l -= 1.0 - // } - // (l * count).trunc() - // }) - // .unwrap_or(0.), - // } + match self { + Self::Line(count) => *count, + Self::Page(count) => editor + .visible_line_count() + .map(|mut l| { + // for full pages subtract one to leave an anchor line + if count.abs() == 1.0 { + l -= 1.0 + } + (l * count).trunc() + }) + .unwrap_or(0.), + } } } diff --git a/crates/go_to_line2/src/go_to_line.rs b/crates/go_to_line2/src/go_to_line.rs index 8f0d94f3e8..9ec770e05c 100644 --- a/crates/go_to_line2/src/go_to_line.rs +++ b/crates/go_to_line2/src/go_to_line.rs @@ -8,25 +8,12 @@ use text::{Bias, Point}; use theme::ActiveTheme; use ui::{h_stack, modal, v_stack, Label, LabelColor}; use util::paths::FILE_ROW_COLUMN_DELIMITER; -use workspace::{ModalEvent, Workspace}; +use workspace::{Modal, ModalEvent, Workspace}; actions!(Toggle); pub fn init(cx: &mut AppContext) { - cx.observe_new_views( - |workspace: &mut Workspace, _: &mut ViewContext| { - workspace - .modal_layer() - .register_modal(Toggle, |workspace, cx| { - let editor = workspace - .active_item(cx) - .and_then(|active_item| active_item.downcast::())?; - - Some(cx.build_view(|cx| GoToLine::new(editor, cx))) - }); - }, - ) - .detach(); + cx.observe_new_views(GoToLine::register).detach(); } pub struct GoToLine { @@ -38,14 +25,28 @@ pub struct GoToLine { } impl EventEmitter for GoToLine {} +impl Modal for GoToLine { + fn focus(&self, cx: &mut WindowContext) { + self.line_editor.update(cx, |editor, cx| editor.focus(cx)) + } +} impl GoToLine { - pub fn new(active_editor: View, cx: &mut ViewContext) -> Self { - let line_editor = cx.build_view(|cx| { - let editor = Editor::single_line(cx); - editor.focus(cx); - editor + fn register(workspace: &mut Workspace, _: &mut ViewContext) { + workspace.register_action(|workspace, _: &Toggle, cx| { + let Some(editor) = workspace + .active_item(cx) + .and_then(|active_item| active_item.downcast::()) + else { + return; + }; + + workspace.toggle_modal(cx, move |cx| GoToLine::new(editor, cx)); }); + } + + pub fn new(active_editor: View, cx: &mut ViewContext) -> Self { + let line_editor = cx.build_view(|cx| Editor::single_line(cx)); let line_editor_change = cx.subscribe(&line_editor, Self::on_line_editor_event); let editor = active_editor.read(cx); @@ -123,10 +124,6 @@ impl GoToLine { } fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { - self.active_editor.update(cx, |editor, cx| { - editor.focus(cx); - cx.notify(); - }); cx.emit(ModalEvent::Dismissed); } diff --git a/crates/gpui/src/elements/flex.rs b/crates/gpui/src/elements/flex.rs index ba387c5e48..f4b8578d17 100644 --- a/crates/gpui/src/elements/flex.rs +++ b/crates/gpui/src/elements/flex.rs @@ -67,14 +67,21 @@ impl Flex { where Tag: 'static, { + // Don't assume that this initialization is what scroll_state really is in other panes: + // `element_state` is shared and there could be init races. let scroll_state = cx.element_state::>( element_id, Rc::new(ScrollState { - scroll_to: Cell::new(scroll_to), - scroll_position: Default::default(), type_tag: TypeTag::new::(), + scroll_to: Default::default(), + scroll_position: Default::default(), }), ); + // Set scroll_to separately, because the default state is already picked as `None` by other panes + // by the time we start setting it here, hence update all others' state too. + scroll_state.update(cx, |this, _| { + this.scroll_to.set(scroll_to); + }); self.scroll_state = Some((scroll_state, cx.handle().id())); self } diff --git a/crates/gpui2/src/action.rs b/crates/gpui2/src/action.rs index 85149f5d55..170ddf942f 100644 --- a/crates/gpui2/src/action.rs +++ b/crates/gpui2/src/action.rs @@ -4,7 +4,7 @@ use collections::{HashMap, HashSet}; use lazy_static::lazy_static; use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard}; use serde::Deserialize; -use std::any::{type_name, Any}; +use std::any::{type_name, Any, TypeId}; /// Actions are used to implement keyboard-driven UI. /// When you declare an action, you can bind keys to the action in the keymap and @@ -100,6 +100,21 @@ where } } +impl dyn Action { + pub fn type_id(&self) -> TypeId { + self.as_any().type_id() + } + + pub fn name(&self) -> SharedString { + ACTION_REGISTRY + .read() + .names_by_type_id + .get(&self.type_id()) + .expect("type is not a registered action") + .clone() + } +} + type ActionBuilder = fn(json: Option) -> anyhow::Result>; lazy_static! { @@ -109,6 +124,7 @@ lazy_static! { #[derive(Default)] struct ActionRegistry { builders_by_name: HashMap, + names_by_type_id: HashMap, all_names: Vec, // So we can return a static slice. } @@ -117,9 +133,24 @@ pub fn register_action() { let name = A::qualified_name(); let mut lock = ACTION_REGISTRY.write(); lock.builders_by_name.insert(name.clone(), A::build); + lock.names_by_type_id + .insert(TypeId::of::(), name.clone()); lock.all_names.push(name); } +/// Construct an action based on its name and optional JSON parameters sourced from the keymap. +pub fn build_action_from_type(type_id: &TypeId) -> Result> { + let lock = ACTION_REGISTRY.read(); + let name = lock + .names_by_type_id + .get(type_id) + .ok_or_else(|| anyhow!("no action type registered for {:?}", type_id))? + .clone(); + drop(lock); + + build_action(&name, None) +} + /// Construct an action based on its name and optional JSON parameters sourced from the keymap. pub fn build_action(name: &str, params: Option) -> Result> { let lock = ACTION_REGISTRY.read(); diff --git a/crates/gpui2/src/element.rs b/crates/gpui2/src/element.rs index 8fdc17de07..9ee9eaa7c3 100644 --- a/crates/gpui2/src/element.rs +++ b/crates/gpui2/src/element.rs @@ -1,4 +1,6 @@ -use crate::{BorrowWindow, Bounds, ElementId, LayoutId, Pixels, ViewContext}; +use crate::{ + AvailableSpace, BorrowWindow, Bounds, ElementId, LayoutId, Pixels, Point, Size, ViewContext, +}; use derive_more::{Deref, DerefMut}; pub(crate) use smallvec::SmallVec; use std::{any::Any, mem}; @@ -61,6 +63,19 @@ trait ElementObject { fn initialize(&mut self, view_state: &mut V, cx: &mut ViewContext); fn layout(&mut self, view_state: &mut V, cx: &mut ViewContext) -> LayoutId; fn paint(&mut self, view_state: &mut V, cx: &mut ViewContext); + fn measure( + &mut self, + available_space: Size, + view_state: &mut V, + cx: &mut ViewContext, + ) -> Size; + fn draw( + &mut self, + origin: Point, + available_space: Size, + view_state: &mut V, + cx: &mut ViewContext, + ); } struct RenderedElement> { @@ -79,6 +94,11 @@ enum ElementRenderPhase { layout_id: LayoutId, frame_state: Option, }, + LayoutComputed { + layout_id: LayoutId, + available_space: Size, + frame_state: Option, + }, Painted, } @@ -135,7 +155,9 @@ where } } ElementRenderPhase::Start => panic!("must call initialize before layout"), - ElementRenderPhase::LayoutRequested { .. } | ElementRenderPhase::Painted => { + ElementRenderPhase::LayoutRequested { .. } + | ElementRenderPhase::LayoutComputed { .. } + | ElementRenderPhase::Painted => { panic!("element rendered twice") } }; @@ -152,6 +174,11 @@ where ElementRenderPhase::LayoutRequested { layout_id, mut frame_state, + } + | ElementRenderPhase::LayoutComputed { + layout_id, + mut frame_state, + .. } => { let bounds = cx.layout_bounds(layout_id); if let Some(id) = self.element.id() { @@ -171,6 +198,65 @@ where _ => panic!("must call layout before paint"), }; } + + fn measure( + &mut self, + available_space: Size, + view_state: &mut V, + cx: &mut ViewContext, + ) -> Size { + if matches!(&self.phase, ElementRenderPhase::Start) { + self.initialize(view_state, cx); + } + + if matches!(&self.phase, ElementRenderPhase::Initialized { .. }) { + self.layout(view_state, cx); + } + + let layout_id = match &mut self.phase { + ElementRenderPhase::LayoutRequested { + layout_id, + frame_state, + } => { + cx.compute_layout(*layout_id, available_space); + let layout_id = *layout_id; + self.phase = ElementRenderPhase::LayoutComputed { + layout_id, + available_space, + frame_state: frame_state.take(), + }; + layout_id + } + ElementRenderPhase::LayoutComputed { + layout_id, + available_space: prev_available_space, + .. + } => { + if available_space != *prev_available_space { + cx.compute_layout(*layout_id, available_space); + *prev_available_space = available_space; + } + *layout_id + } + _ => panic!("cannot measure after painting"), + }; + + cx.layout_bounds(layout_id).size + } + + fn draw( + &mut self, + mut origin: Point, + available_space: Size, + view_state: &mut V, + cx: &mut ViewContext, + ) { + self.measure(available_space, view_state, cx); + // Ignore the element offset when drawing this element, as the origin is already specified + // in absolute terms. + origin -= cx.element_offset(); + cx.with_element_offset(Some(origin), |cx| self.paint(view_state, cx)) + } } pub struct AnyElement(Box>); @@ -196,6 +282,27 @@ impl AnyElement { pub fn paint(&mut self, view_state: &mut V, cx: &mut ViewContext) { self.0.paint(view_state, cx) } + + /// Initializes this element and performs layout within the given available space to determine its size. + pub fn measure( + &mut self, + available_space: Size, + view_state: &mut V, + cx: &mut ViewContext, + ) -> Size { + self.0.measure(available_space, view_state, cx) + } + + /// Initializes this element and performs layout in the available space, then paints it at the given origin. + pub fn draw( + &mut self, + origin: Point, + available_space: Size, + view_state: &mut V, + cx: &mut ViewContext, + ) { + self.0.draw(origin, available_space, view_state, cx) + } } pub trait Component { diff --git a/crates/gpui2/src/elements/text.rs b/crates/gpui2/src/elements/text.rs index e258d3e7dc..5c5709d32e 100644 --- a/crates/gpui2/src/elements/text.rs +++ b/crates/gpui2/src/elements/text.rs @@ -101,7 +101,12 @@ impl Element for Text { .map(|line| line.wrap_count() + 1) .sum::(); let size = Size { - width: lines.iter().map(|line| line.layout.width).max().unwrap(), + width: lines + .iter() + .map(|line| line.layout.width) + .max() + .unwrap() + .ceil(), height: line_height * line_count, }; diff --git a/crates/gpui2/src/elements/uniform_list.rs b/crates/gpui2/src/elements/uniform_list.rs index e116022763..6687559d1c 100644 --- a/crates/gpui2/src/elements/uniform_list.rs +++ b/crates/gpui2/src/elements/uniform_list.rs @@ -1,6 +1,6 @@ use crate::{ - point, px, AnyElement, AvailableSpace, BorrowWindow, Bounds, Component, Element, ElementId, - ElementInteractivity, InteractiveElementState, LayoutId, Pixels, Point, Size, + point, px, size, AnyElement, AvailableSpace, BorrowWindow, Bounds, Component, Element, + ElementId, ElementInteractivity, InteractiveElementState, LayoutId, Pixels, Point, Size, StatefulInteractive, StatefulInteractivity, StatelessInteractive, StatelessInteractivity, StyleRefinement, Styled, ViewContext, }; @@ -9,6 +9,9 @@ use smallvec::SmallVec; use std::{cmp, ops::Range, sync::Arc}; use taffy::style::Overflow; +/// uniform_list provides lazy rendering for a set of items that are of uniform height. +/// When rendered into a container with overflow-y: hidden and a fixed (or max) height, +/// uniform_list will only render the visibile subset of items. pub fn uniform_list( id: Id, item_count: usize, @@ -20,10 +23,14 @@ where C: Component, { let id = id.into(); + let mut style = StyleRefinement::default(); + style.overflow.y = Some(Overflow::Hidden); + UniformList { id: id.clone(), - style: Default::default(), + style, item_count, + item_to_measure_index: 0, render_items: Box::new(move |view, visible_range, cx| { f(view, visible_range, cx) .into_iter() @@ -39,6 +46,7 @@ pub struct UniformList { id: ElementId, style: StyleRefinement, item_count: usize, + item_to_measure_index: usize, render_items: Box< dyn for<'a> Fn( &'a mut V, @@ -50,7 +58,7 @@ pub struct UniformList { scroll_handle: Option, } -#[derive(Clone)] +#[derive(Clone, Default)] pub struct UniformListScrollHandle(Arc>>); #[derive(Clone, Debug)] @@ -86,8 +94,14 @@ impl Styled for UniformList { } } +#[derive(Default)] +pub struct UniformListState { + interactive: InteractiveElementState, + item_size: Size, +} + impl Element for UniformList { - type ElementState = InteractiveElementState; + type ElementState = UniformListState; fn id(&self) -> Option { Some(self.id.clone()) @@ -95,20 +109,47 @@ impl Element for UniformList { fn initialize( &mut self, - _: &mut V, + view_state: &mut V, element_state: Option, - _: &mut ViewContext, + cx: &mut ViewContext, ) -> Self::ElementState { - element_state.unwrap_or_default() + element_state.unwrap_or_else(|| { + let item_size = self.measure_item(view_state, None, cx); + UniformListState { + interactive: InteractiveElementState::default(), + item_size, + } + }) } fn layout( &mut self, _view_state: &mut V, - _element_state: &mut Self::ElementState, + element_state: &mut Self::ElementState, cx: &mut ViewContext, ) -> LayoutId { - cx.request_layout(&self.computed_style(), None) + let max_items = self.item_count; + let item_size = element_state.item_size; + let rem_size = cx.rem_size(); + + cx.request_measured_layout( + self.computed_style(), + rem_size, + move |known_dimensions: Size>, available_space: Size| { + let desired_height = item_size.height * max_items; + let width = known_dimensions + .width + .unwrap_or(match available_space.width { + AvailableSpace::Definite(x) => x, + AvailableSpace::MinContent | AvailableSpace::MaxContent => item_size.width, + }); + let height = match available_space.height { + AvailableSpace::Definite(x) => desired_height.min(x), + AvailableSpace::MinContent | AvailableSpace::MaxContent => desired_height, + }; + size(width, height) + }, + ) } fn paint( @@ -119,7 +160,6 @@ impl Element for UniformList { cx: &mut ViewContext, ) { let style = self.computed_style(); - style.paint(bounds, cx); let border = style.border_widths.to_pixels(cx.rem_size()); let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size()); @@ -131,14 +171,18 @@ impl Element for UniformList { ); cx.with_z_index(style.z_index.unwrap_or(0), |cx| { + style.paint(bounds, cx); + let content_size; if self.item_count > 0 { - let item_height = self.measure_item_height(view_state, padded_bounds, cx); + let item_height = self + .measure_item(view_state, Some(padded_bounds.size.width), cx) + .height; if let Some(scroll_handle) = self.scroll_handle.clone() { scroll_handle.0.lock().replace(ScrollHandleState { item_height, list_height: padded_bounds.size.height, - scroll_offset: element_state.track_scroll_offset(), + scroll_offset: element_state.interactive.track_scroll_offset(), }); } let visible_item_count = if item_height > px(0.) { @@ -147,6 +191,7 @@ impl Element for UniformList { 0 }; let scroll_offset = element_state + .interactive .scroll_offset() .map_or((0.0).into(), |offset| offset.y); let first_visible_element_ix = (-scroll_offset / item_height).floor() as usize; @@ -165,19 +210,13 @@ impl Element for UniformList { cx.with_z_index(1, |cx| { for (item, ix) in items.iter_mut().zip(visible_range) { - item.initialize(view_state, cx); - - let layout_id = item.layout(view_state, cx); - cx.compute_layout( - layout_id, - Size { - width: AvailableSpace::Definite(bounds.size.width), - height: AvailableSpace::Definite(item_height), - }, - ); - let offset = + let item_origin = padded_bounds.origin + point(px(0.), item_height * ix + scroll_offset); - cx.with_element_offset(Some(offset), |cx| item.paint(view_state, cx)) + let available_space = size( + AvailableSpace::Definite(padded_bounds.size.width), + AvailableSpace::Definite(item_height), + ); + item.draw(item_origin, available_space, view_state, cx); } }); } else { @@ -190,33 +229,44 @@ impl Element for UniformList { let overflow = point(style.overflow.x, Overflow::Scroll); cx.with_z_index(0, |cx| { - self.interactivity - .paint(bounds, content_size, overflow, element_state, cx); + self.interactivity.paint( + bounds, + content_size, + overflow, + &mut element_state.interactive, + cx, + ); }); }) } } impl UniformList { - fn measure_item_height( + pub fn with_width_from_item(mut self, item_index: Option) -> Self { + self.item_to_measure_index = item_index.unwrap_or(0); + self + } + + fn measure_item( &self, view_state: &mut V, - list_bounds: Bounds, + list_width: Option, cx: &mut ViewContext, - ) -> Pixels { - let mut items = (self.render_items)(view_state, 0..1, cx); - debug_assert!(items.len() == 1); + ) -> Size { + if self.item_count == 0 { + return Size::default(); + } + + let item_ix = cmp::min(self.item_to_measure_index, self.item_count - 1); + let mut items = (self.render_items)(view_state, item_ix..item_ix + 1, cx); let mut item_to_measure = items.pop().unwrap(); - item_to_measure.initialize(view_state, cx); - let layout_id = item_to_measure.layout(view_state, cx); - cx.compute_layout( - layout_id, - Size { - width: AvailableSpace::Definite(list_bounds.size.width), - height: AvailableSpace::MinContent, - }, + let available_space = size( + list_width.map_or(AvailableSpace::MinContent, |width| { + AvailableSpace::Definite(width) + }), + AvailableSpace::MinContent, ); - cx.layout_bounds(layout_id).size.height + item_to_measure.measure(available_space, view_state, cx) } pub fn track_scroll(mut self, handle: UniformListScrollHandle) -> Self { diff --git a/crates/gpui2/src/geometry.rs b/crates/gpui2/src/geometry.rs index f290c6a81c..e07300951e 100644 --- a/crates/gpui2/src/geometry.rs +++ b/crates/gpui2/src/geometry.rs @@ -785,6 +785,10 @@ impl Pixels { Self(self.0.round()) } + pub fn ceil(&self) -> Self { + Self(self.0.ceil()) + } + pub fn scale(&self, factor: f32) -> ScaledPixels { ScaledPixels(self.0 * factor) } diff --git a/crates/gpui2/src/interactive.rs b/crates/gpui2/src/interactive.rs index a546c1b40b..243eb3cb07 100644 --- a/crates/gpui2/src/interactive.rs +++ b/crates/gpui2/src/interactive.rs @@ -94,7 +94,6 @@ pub trait StatelessInteractive: Element { fn on_mouse_down_out( mut self, - button: MouseButton, handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext) + 'static, ) -> Self where @@ -103,10 +102,7 @@ pub trait StatelessInteractive: Element { self.stateless_interactivity() .mouse_down_listeners .push(Box::new(move |view, event, bounds, phase, cx| { - if phase == DispatchPhase::Capture - && event.button == button - && !bounds.contains_point(&event.position) - { + if phase == DispatchPhase::Capture && !bounds.contains_point(&event.position) { handler(view, event, cx) } })); diff --git a/crates/gpui2/src/platform/test/platform.rs b/crates/gpui2/src/platform/test/platform.rs index 37978e7ad7..4afcc4fc1a 100644 --- a/crates/gpui2/src/platform/test/platform.rs +++ b/crates/gpui2/src/platform/test/platform.rs @@ -182,7 +182,8 @@ impl Platform for TestPlatform { } fn should_auto_hide_scrollbars(&self) -> bool { - unimplemented!() + // todo() + true } fn write_to_clipboard(&self, _item: crate::ClipboardItem) { diff --git a/crates/gpui2/src/platform/test/window.rs b/crates/gpui2/src/platform/test/window.rs index f132719655..289ecf7e6b 100644 --- a/crates/gpui2/src/platform/test/window.rs +++ b/crates/gpui2/src/platform/test/window.rs @@ -1,10 +1,14 @@ -use std::{rc::Rc, sync::Arc}; +use std::{ + rc::Rc, + sync::{self, Arc}, +}; +use collections::HashMap; use parking_lot::Mutex; use crate::{ - px, Pixels, PlatformAtlas, PlatformDisplay, PlatformWindow, Point, Scene, Size, - WindowAppearance, WindowBounds, WindowOptions, + px, AtlasKey, AtlasTextureId, AtlasTile, Pixels, PlatformAtlas, PlatformDisplay, + PlatformWindow, Point, Scene, Size, TileId, WindowAppearance, WindowBounds, WindowOptions, }; #[derive(Default)] @@ -30,7 +34,7 @@ impl TestWindow { current_scene: Default::default(), display, - sprite_atlas: Arc::new(TestAtlas), + sprite_atlas: Arc::new(TestAtlas::new()), handlers: Default::default(), } } @@ -154,26 +158,71 @@ impl PlatformWindow for TestWindow { self.current_scene.lock().replace(scene); } - fn sprite_atlas(&self) -> std::sync::Arc { + fn sprite_atlas(&self) -> sync::Arc { self.sprite_atlas.clone() } } -pub struct TestAtlas; +pub struct TestAtlasState { + next_id: u32, + tiles: HashMap, +} + +pub struct TestAtlas(Mutex); + +impl TestAtlas { + pub fn new() -> Self { + TestAtlas(Mutex::new(TestAtlasState { + next_id: 0, + tiles: HashMap::default(), + })) + } +} impl PlatformAtlas for TestAtlas { fn get_or_insert_with<'a>( &self, - _key: &crate::AtlasKey, - _build: &mut dyn FnMut() -> anyhow::Result<( + key: &crate::AtlasKey, + build: &mut dyn FnMut() -> anyhow::Result<( Size, std::borrow::Cow<'a, [u8]>, )>, ) -> anyhow::Result { - todo!() + let mut state = self.0.lock(); + if let Some(tile) = state.tiles.get(key) { + return Ok(tile.clone()); + } + + state.next_id += 1; + let texture_id = state.next_id; + state.next_id += 1; + let tile_id = state.next_id; + + drop(state); + let (size, _) = build()?; + let mut state = self.0.lock(); + + state.tiles.insert( + key.clone(), + crate::AtlasTile { + texture_id: AtlasTextureId { + index: texture_id, + kind: crate::AtlasTextureKind::Path, + }, + tile_id: TileId(tile_id), + bounds: crate::Bounds { + origin: Point::zero(), + size, + }, + }, + ); + + Ok(state.tiles[key].clone()) } fn clear(&self) { - todo!() + let mut state = self.0.lock(); + state.tiles = HashMap::default(); + state.next_id = 0; } } diff --git a/crates/gpui2/src/taffy.rs b/crates/gpui2/src/taffy.rs index 9724179eed..ea87f73872 100644 --- a/crates/gpui2/src/taffy.rs +++ b/crates/gpui2/src/taffy.rs @@ -1,5 +1,6 @@ use super::{AbsoluteLength, Bounds, DefiniteLength, Edges, Length, Pixels, Point, Size, Style}; -use collections::HashMap; +use collections::{HashMap, HashSet}; +use smallvec::SmallVec; use std::fmt::Debug; use taffy::{ geometry::{Point as TaffyPoint, Rect as TaffyRect, Size as TaffySize}, @@ -12,6 +13,7 @@ pub struct TaffyLayoutEngine { taffy: Taffy, children_to_parents: HashMap, absolute_layout_bounds: HashMap>, + computed_layouts: HashSet, } static EXPECT_MESSAGE: &'static str = @@ -23,9 +25,17 @@ impl TaffyLayoutEngine { taffy: Taffy::new(), children_to_parents: HashMap::default(), absolute_layout_bounds: HashMap::default(), + computed_layouts: HashSet::default(), } } + pub fn clear(&mut self) { + self.taffy.clear(); + self.children_to_parents.clear(); + self.absolute_layout_bounds.clear(); + self.computed_layouts.clear(); + } + pub fn request_layout( &mut self, style: &Style, @@ -115,6 +125,7 @@ impl TaffyLayoutEngine { } pub fn compute_layout(&mut self, id: LayoutId, available_space: Size) { + // Leaving this here until we have a better instrumentation approach. // println!("Laying out {} children", self.count_all_children(id)?); // println!("Max layout depth: {}", self.max_depth(0, id)?); @@ -124,6 +135,22 @@ impl TaffyLayoutEngine { // println!("N{} --> N{}", u64::from(a), u64::from(b)); // } // println!(""); + // + + if !self.computed_layouts.insert(id) { + let mut stack = SmallVec::<[LayoutId; 64]>::new(); + stack.push(id); + while let Some(id) = stack.pop() { + self.absolute_layout_bounds.remove(&id); + stack.extend( + self.taffy + .children(id.into()) + .expect(EXPECT_MESSAGE) + .into_iter() + .map(Into::into), + ); + } + } // let started_at = std::time::Instant::now(); self.taffy @@ -397,7 +424,7 @@ where } } -#[derive(Copy, Clone, Default, Debug)] +#[derive(Copy, Clone, Default, Debug, Eq, PartialEq)] pub enum AvailableSpace { /// The amount of space available is the specified number of pixels Definite(Pixels), diff --git a/crates/gpui2/src/view.rs b/crates/gpui2/src/view.rs index d12d84f43b..00e1e55cd5 100644 --- a/crates/gpui2/src/view.rs +++ b/crates/gpui2/src/view.rs @@ -184,6 +184,10 @@ impl AnyView { .compute_layout(layout_id, available_space); (self.paint)(self, &mut rendered_element, cx); } + + pub(crate) fn draw_dispatch_stack(&self, cx: &mut WindowContext) { + (self.initialize)(self, cx); + } } impl Component for AnyView { diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index ac7dcf0256..fd3890d644 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1,14 +1,15 @@ use crate::{ - px, size, Action, AnyBox, AnyDrag, AnyView, AppContext, AsyncWindowContext, AvailableSpace, - Bounds, BoxShadow, Context, Corners, CursorStyle, DevicePixels, DispatchContext, DisplayId, - Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, FocusEvent, FontId, - GlobalElementId, GlyphId, Hsla, ImageData, InputEvent, IsZero, KeyListener, KeyMatch, - KeyMatcher, Keystroke, LayoutId, Model, ModelContext, Modifiers, MonochromeSprite, MouseButton, - MouseDownEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay, - PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, PromptLevel, Quad, Render, - RenderGlyphParams, RenderImageParams, RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, - SharedString, Size, Style, SubscriberSet, Subscription, TaffyLayoutEngine, Task, Underline, - UnderlineStyle, View, VisualContext, WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS, + build_action_from_type, px, size, Action, AnyBox, AnyDrag, AnyView, AppContext, + AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, CursorStyle, + DevicePixels, DispatchContext, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, + FileDropEvent, FocusEvent, FontId, GlobalElementId, GlyphId, Hsla, ImageData, InputEvent, + IsZero, KeyListener, KeyMatch, KeyMatcher, Keystroke, LayoutId, Model, ModelContext, Modifiers, + MonochromeSprite, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels, + PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, + PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams, RenderSvgParams, ScaledPixels, + SceneBuilder, Shadow, SharedString, Size, Style, SubscriberSet, Subscription, + TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext, WeakView, + WindowBounds, WindowOptions, SUBPIXEL_VARIANTS, }; use anyhow::{anyhow, Result}; use collections::HashMap; @@ -145,6 +146,11 @@ impl FocusHandle { } } + /// Moves the focus to the element associated with this handle. + pub fn focus(&self, cx: &mut WindowContext) { + cx.focus(self) + } + /// Obtains whether the element associated with this handle is currently focused. pub fn is_focused(&self, cx: &WindowContext) -> bool { self.id.is_focused(cx) @@ -200,7 +206,7 @@ pub struct Window { display_id: DisplayId, sprite_atlas: Arc, rem_size: Pixels, - content_size: Size, + viewport_size: Size, pub(crate) layout_engine: TaffyLayoutEngine, pub(crate) root_view: Option, pub(crate) element_id_stack: GlobalElementId, @@ -227,7 +233,7 @@ pub(crate) struct Frame { key_matchers: HashMap, mouse_listeners: HashMap>, pub(crate) focus_listeners: Vec, - key_dispatch_stack: Vec, + pub(crate) key_dispatch_stack: Vec, freeze_key_dispatch_stack: bool, focus_parents_by_child: HashMap, pub(crate) scene_builder: SceneBuilder, @@ -299,7 +305,7 @@ impl Window { display_id, sprite_atlas, rem_size: px(16.), - content_size, + viewport_size: content_size, layout_engine: TaffyLayoutEngine::new(), root_view: None, element_id_stack: GlobalElementId::default(), @@ -326,7 +332,7 @@ impl Window { /// find the focused element. We interleave key listeners with dispatch contexts so we can use the /// contexts when matching key events against the keymap. A key listener can be either an action /// handler or a [KeyDown] / [KeyUp] event listener. -enum KeyDispatchStackFrame { +pub(crate) enum KeyDispatchStackFrame { Listener { event_type: TypeId, listener: AnyKeyListener, @@ -401,11 +407,18 @@ impl<'a> WindowContext<'a> { /// Move focus to the element associated with the given `FocusHandle`. pub fn focus(&mut self, handle: &FocusHandle) { + if self.window.focus == Some(handle.id) { + return; + } + if self.window.last_blur.is_none() { self.window.last_blur = Some(self.window.focus); } self.window.focus = Some(handle.id); + + // self.window.current_frame.key_dispatch_stack.clear() + // self.window.root_view.initialize() self.app.push_effect(Effect::FocusChanged { window_handle: self.window.handle, focused: Some(handle.id), @@ -427,6 +440,14 @@ impl<'a> WindowContext<'a> { self.notify(); } + pub fn dispatch_action(&mut self, action: Box) { + self.defer(|cx| { + cx.app.propagate_event = true; + let stack = cx.dispatch_stack(); + cx.dispatch_action_internal(action, &stack[..]) + }) + } + /// Schedules the given function to be run at the end of the current effect cycle, allowing entities /// that are currently on the stack to be returned to the app. pub fn defer(&mut self, f: impl FnOnce(&mut WindowContext) + 'static) { @@ -609,7 +630,7 @@ impl<'a> WindowContext<'a> { fn window_bounds_changed(&mut self) { self.window.scale_factor = self.window.platform_window.scale_factor(); - self.window.content_size = self.window.platform_window.content_size(); + self.window.viewport_size = self.window.platform_window.content_size(); self.window.bounds = self.window.platform_window.bounds(); self.window.display_id = self.window.platform_window.display().id(); self.window.dirty = true; @@ -624,6 +645,10 @@ impl<'a> WindowContext<'a> { self.window.bounds } + pub fn viewport_size(&self) -> Size { + self.window.viewport_size + } + pub fn is_window_active(&self) -> bool { self.window.active } @@ -717,7 +742,7 @@ impl<'a> WindowContext<'a> { /// Called during painting to invoke the given closure in a new stacking context. The given /// z-index is interpreted relative to the previous call to `stack`. - pub fn stack(&mut self, z_index: u32, f: impl FnOnce(&mut Self) -> R) -> R { + pub fn with_z_index(&mut self, z_index: u32, f: impl FnOnce(&mut Self) -> R) -> R { self.window.current_frame.z_index_stack.push(z_index); let result = f(self); self.window.current_frame.z_index_stack.pop(); @@ -1015,13 +1040,13 @@ impl<'a> WindowContext<'a> { self.start_frame(); - self.stack(0, |cx| { - let available_space = cx.window.content_size.map(Into::into); + self.with_z_index(0, |cx| { + let available_space = cx.window.viewport_size.map(Into::into); root_view.draw(available_space, cx); }); if let Some(active_drag) = self.app.active_drag.take() { - self.stack(1, |cx| { + self.with_z_index(1, |cx| { let offset = cx.mouse_position() - active_drag.cursor_offset; cx.with_element_offset(Some(offset), |cx| { let available_space = @@ -1031,7 +1056,7 @@ impl<'a> WindowContext<'a> { }); }); } else if let Some(active_tooltip) = self.app.active_tooltip.take() { - self.stack(1, |cx| { + self.with_z_index(1, |cx| { cx.with_element_offset(Some(active_tooltip.cursor_offset), |cx| { let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent); @@ -1054,12 +1079,34 @@ impl<'a> WindowContext<'a> { self.window.dirty = false; } + pub(crate) fn dispatch_stack(&mut self) -> Vec { + let root_view = self.window.root_view.take().unwrap(); + let window = &mut *self.window; + let mut spare_frame = Frame::default(); + mem::swap(&mut spare_frame, &mut window.previous_frame); + + self.start_frame(); + + root_view.draw_dispatch_stack(self); + + let window = &mut *self.window; + // restore the old values of current and previous frame, + // putting the new frame into spare_frame. + mem::swap(&mut window.current_frame, &mut window.previous_frame); + mem::swap(&mut spare_frame, &mut window.previous_frame); + self.window.root_view = Some(root_view); + + spare_frame.key_dispatch_stack + } + /// Rotate the current frame and the previous frame, then clear the current frame. /// We repopulate all state in the current frame during each paint. fn start_frame(&mut self) { self.text_system().start_frame(); let window = &mut *self.window; + window.layout_engine.clear(); + mem::swap(&mut window.previous_frame, &mut window.current_frame); let frame = &mut window.current_frame; frame.element_states.clear(); @@ -1196,7 +1243,7 @@ impl<'a> WindowContext<'a> { DispatchPhase::Capture, self, ) { - self.dispatch_action(action, &key_dispatch_stack[..ix]); + self.dispatch_action_internal(action, &key_dispatch_stack[..ix]); } if !self.app.propagate_event { break; @@ -1223,7 +1270,10 @@ impl<'a> WindowContext<'a> { DispatchPhase::Bubble, self, ) { - self.dispatch_action(action, &key_dispatch_stack[..ix]); + self.dispatch_action_internal( + action, + &key_dispatch_stack[..ix], + ); } if !self.app.propagate_event { @@ -1295,7 +1345,29 @@ impl<'a> WindowContext<'a> { self.window.platform_window.prompt(level, msg, answers) } - fn dispatch_action( + pub fn available_actions(&self) -> impl Iterator> + '_ { + let key_dispatch_stack = &self.window.previous_frame.key_dispatch_stack; + key_dispatch_stack.iter().filter_map(|frame| { + match frame { + // todo!factor out a KeyDispatchStackFrame::Action + KeyDispatchStackFrame::Listener { + event_type, + listener: _, + } => { + match build_action_from_type(event_type) { + Ok(action) => Some(action), + Err(err) => { + dbg!(err); + None + } // we'll hit his if TypeId == KeyDown + } + } + KeyDispatchStackFrame::Context(_) => None, + } + }) + } + + pub(crate) fn dispatch_action_internal( &mut self, action: Box, dispatch_stack: &[KeyDispatchStackFrame], @@ -1684,7 +1756,7 @@ pub trait BorrowWindow: BorrowMut + BorrowMut { .unwrap_or_else(|| ContentMask { bounds: Bounds { origin: Point::default(), - size: self.window().content_size, + size: self.window().viewport_size, }, }) } diff --git a/crates/picker2/Cargo.toml b/crates/picker2/Cargo.toml index 90e1ae931c..3c4d21ad50 100644 --- a/crates/picker2/Cargo.toml +++ b/crates/picker2/Cargo.toml @@ -10,6 +10,7 @@ doctest = false [dependencies] editor = { package = "editor2", path = "../editor2" } +ui = { package = "ui2", path = "../ui2" } gpui = { package = "gpui2", path = "../gpui2" } menu = { package = "menu2", path = "../menu2" } settings = { package = "settings2", path = "../settings2" } diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index 075cf10ff6..0a731b4a27 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -4,7 +4,8 @@ use gpui::{ StatelessInteractive, Styled, Task, UniformListScrollHandle, View, ViewContext, VisualContext, WindowContext, }; -use std::cmp; +use std::{cmp, sync::Arc}; +use ui::{prelude::*, v_stack, Divider, Label, LabelColor}; pub struct Picker { pub delegate: D, @@ -20,7 +21,7 @@ pub trait PickerDelegate: Sized + 'static { fn selected_index(&self) -> usize; fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext>); - // fn placeholder_text(&self) -> Arc; + fn placeholder_text(&self) -> Arc; fn update_matches(&mut self, query: String, cx: &mut ViewContext>) -> Task<()>; fn confirm(&mut self, secondary: bool, cx: &mut ViewContext>); @@ -36,7 +37,11 @@ pub trait PickerDelegate: Sized + 'static { impl Picker { pub fn new(delegate: D, cx: &mut ViewContext) -> Self { - let editor = cx.build_view(|cx| Editor::single_line(cx)); + let editor = cx.build_view(|cx| { + let mut editor = Editor::single_line(cx); + editor.set_placeholder_text(delegate.placeholder_text(), cx); + editor + }); cx.subscribe(&editor, Self::on_input_editor_event).detach(); Self { delegate, @@ -57,6 +62,7 @@ impl Picker { let ix = cmp::min(index + 1, count - 1); self.delegate.set_selected_index(ix, cx); self.scroll_handle.scroll_to_item(ix); + cx.notify(); } } @@ -67,6 +73,7 @@ impl Picker { let ix = index.saturating_sub(1); self.delegate.set_selected_index(ix, cx); self.scroll_handle.scroll_to_item(ix); + cx.notify(); } } @@ -75,6 +82,7 @@ impl Picker { if count > 0 { self.delegate.set_selected_index(0, cx); self.scroll_handle.scroll_to_item(0); + cx.notify(); } } @@ -83,6 +91,7 @@ impl Picker { if count > 0 { self.delegate.set_selected_index(count - 1, cx); self.scroll_handle.scroll_to_item(count - 1); + cx.notify(); } } @@ -133,12 +142,13 @@ impl Picker { impl Render for Picker { type Element = Div, FocusEnabled>; - fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { div() .context("picker") .id("picker-container") .focusable() .size_full() + .elevation_2(cx) .on_action(Self::select_next) .on_action(Self::select_prev) .on_action(Self::select_first) @@ -146,18 +156,42 @@ impl Render for Picker { .on_action(Self::cancel) .on_action(Self::confirm) .on_action(Self::secondary_confirm) - .child(self.editor.clone()) .child( - uniform_list("candidates", self.delegate.match_count(), { - move |this: &mut Self, visible_range, cx| { - let selected_ix = this.delegate.selected_index(); - visible_range - .map(|ix| this.delegate.render_match(ix, ix == selected_ix, cx)) - .collect() - } - }) - .track_scroll(self.scroll_handle.clone()) - .size_full(), + v_stack() + .py_0p5() + .px_1() + .child(div().px_1().py_0p5().child(self.editor.clone())), ) + .child(Divider::horizontal()) + .when(self.delegate.match_count() > 0, |el| { + el.child( + v_stack() + .p_1() + .grow() + .child( + uniform_list("candidates", self.delegate.match_count(), { + move |this: &mut Self, visible_range, cx| { + let selected_ix = this.delegate.selected_index(); + visible_range + .map(|ix| { + this.delegate.render_match(ix, ix == selected_ix, cx) + }) + .collect() + } + }) + .track_scroll(self.scroll_handle.clone()), + ) + .max_h_72() + .overflow_hidden(), + ) + }) + .when(self.delegate.match_count() == 0, |el| { + el.child( + v_stack() + .p_1() + .grow() + .child(Label::new("No matches").color(LabelColor::Muted)), + ) + }) } } diff --git a/crates/storybook2/src/stories/picker.rs b/crates/storybook2/src/stories/picker.rs index 82a010e6b3..067c190575 100644 --- a/crates/storybook2/src/stories/picker.rs +++ b/crates/storybook2/src/stories/picker.rs @@ -44,6 +44,10 @@ impl PickerDelegate for Delegate { self.candidates.len() } + fn placeholder_text(&self) -> Arc { + "Test".into() + } + fn render_match( &self, ix: usize, diff --git a/crates/theme2/src/themes/synthwave_84.rs b/crates/theme2/src/themes/synthwave_84.rs index a8a2561466..0751e9466d 100644 --- a/crates/theme2/src/themes/synthwave_84.rs +++ b/crates/theme2/src/themes/synthwave_84.rs @@ -98,14 +98,14 @@ pub fn synthwave_84() -> UserThemeFamily { ( "link_text".into(), UserHighlightStyle { - color: Some(rgba(0xd50c50ff).into()), + color: Some(rgba(0xdd5500ff).into()), ..Default::default() }, ), ( "link_uri".into(), UserHighlightStyle { - color: Some(rgba(0xd50c50ff).into()), + color: Some(rgba(0xdd5500ff).into()), ..Default::default() }, ), diff --git a/crates/theme_importer/Cargo.toml b/crates/theme_importer/Cargo.toml index 306cea8ecb..b4b72be499 100644 --- a/crates/theme_importer/Cargo.toml +++ b/crates/theme_importer/Cargo.toml @@ -11,6 +11,7 @@ anyhow.workspace = true convert_case = "0.6.0" gpui = { package = "gpui2", path = "../gpui2" } indexmap = "1.6.2" +json_comments = "0.2.2" log.workspace = true rust-embed.workspace = true serde.workspace = true diff --git a/crates/theme_importer/src/main.rs b/crates/theme_importer/src/main.rs index 579b48fe1d..0c690e891c 100644 --- a/crates/theme_importer/src/main.rs +++ b/crates/theme_importer/src/main.rs @@ -11,6 +11,7 @@ use std::str::FromStr; use anyhow::{anyhow, Context, Result}; use convert_case::{Case, Casing}; use gpui::serde_json; +use json_comments::StripComments; use log::LevelFilter; use serde::Deserialize; use simplelog::SimpleLogger; @@ -111,7 +112,8 @@ fn main() -> Result<()> { } }; - let vscode_theme: VsCodeTheme = serde_json::from_reader(theme_file) + let theme_without_comments = StripComments::new(theme_file); + let vscode_theme: VsCodeTheme = serde_json::from_reader(theme_without_comments) .context(format!("failed to parse theme {theme_file_path:?}"))?; let converter = VsCodeThemeConverter::new(vscode_theme, theme_metadata); diff --git a/crates/ui2/src/components.rs b/crates/ui2/src/components.rs index 706918c080..e7b2d9cf0f 100644 --- a/crates/ui2/src/components.rs +++ b/crates/ui2/src/components.rs @@ -3,6 +3,7 @@ mod button; mod checkbox; mod context_menu; mod details; +mod divider; mod elevated_surface; mod facepile; mod icon; @@ -31,6 +32,7 @@ pub use button::*; pub use checkbox::*; pub use context_menu::*; pub use details::*; +pub use divider::*; pub use elevated_surface::*; pub use facepile::*; pub use icon::*; diff --git a/crates/ui2/src/components/divider.rs b/crates/ui2/src/components/divider.rs new file mode 100644 index 0000000000..5ebfc7a4ff --- /dev/null +++ b/crates/ui2/src/components/divider.rs @@ -0,0 +1,46 @@ +use crate::prelude::*; + +enum DividerDirection { + Horizontal, + Vertical, +} + +#[derive(Component)] +pub struct Divider { + direction: DividerDirection, + inset: bool, +} + +impl Divider { + pub fn horizontal() -> Self { + Self { + direction: DividerDirection::Horizontal, + inset: false, + } + } + + pub fn vertical() -> Self { + Self { + direction: DividerDirection::Vertical, + inset: false, + } + } + + pub fn inset(mut self) -> Self { + self.inset = true; + self + } + + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + div() + .map(|this| match self.direction { + DividerDirection::Horizontal => { + this.h_px().w_full().when(self.inset, |this| this.mx_1p5()) + } + DividerDirection::Vertical => { + this.w_px().h_full().when(self.inset, |this| this.my_1p5()) + } + }) + .bg(cx.theme().colors().border_variant) + } +} diff --git a/crates/ui2/src/components/elevated_surface.rs b/crates/ui2/src/components/elevated_surface.rs index 5d0bbe698c..7a6f11978e 100644 --- a/crates/ui2/src/components/elevated_surface.rs +++ b/crates/ui2/src/components/elevated_surface.rs @@ -24,5 +24,5 @@ pub fn elevated_surface(level: ElevationIndex, cx: &mut ViewContext< } pub fn modal(cx: &mut ViewContext) -> Div { - elevated_surface(ElevationIndex::ModalSurfaces, cx) + elevated_surface(ElevationIndex::ModalSurface, cx) } diff --git a/crates/ui2/src/components/icon_button.rs b/crates/ui2/src/components/icon_button.rs index f0dc85b445..91653ea8cd 100644 --- a/crates/ui2/src/components/icon_button.rs +++ b/crates/ui2/src/components/icon_button.rs @@ -98,6 +98,7 @@ impl IconButton { if let Some(click_handler) = self.handlers.click.clone() { button = button.on_mouse_down(MouseButton::Left, move |state, event, cx| { + cx.stop_propagation(); click_handler(state, cx); }); } diff --git a/crates/ui2/src/components/list.rs b/crates/ui2/src/components/list.rs index 57143e1f0c..5c42975b17 100644 --- a/crates/ui2/src/components/list.rs +++ b/crates/ui2/src/components/list.rs @@ -401,7 +401,7 @@ impl List { v_stack() .w_full() .py_1() - .children(self.header.map(|header| header)) + .children(self.header) .child(list_content) } } diff --git a/crates/ui2/src/elevation.md b/crates/ui2/src/elevation.md index 08acc6b12e..3960adb599 100644 --- a/crates/ui2/src/elevation.md +++ b/crates/ui2/src/elevation.md @@ -34,9 +34,9 @@ Material Design 3 has a some great visualizations of elevation that may be helpf The app background constitutes the lowest elevation layer, appearing behind all other surfaces and components. It is predominantly used for the background color of the app. -### UI Surface +### Surface -The UI Surface, located above the app background, is the standard level for all elements +The Surface elevation level, located above the app background, is the standard level for all elements Example Elements: Title Bar, Panel, Tab Bar, Editor diff --git a/crates/ui2/src/elevation.rs b/crates/ui2/src/elevation.rs index 0dd51e3314..8a01b9e046 100644 --- a/crates/ui2/src/elevation.rs +++ b/crates/ui2/src/elevation.rs @@ -11,43 +11,53 @@ pub enum Elevation { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ElevationIndex { - AppBackground, - UISurface, + Background, + Surface, ElevatedSurface, Wash, - ModalSurfaces, + ModalSurface, DraggedElement, } impl ElevationIndex { pub fn z_index(self) -> u32 { match self { - ElevationIndex::AppBackground => 0, - ElevationIndex::UISurface => 100, + ElevationIndex::Background => 0, + ElevationIndex::Surface => 100, ElevationIndex::ElevatedSurface => 200, ElevationIndex::Wash => 300, - ElevationIndex::ModalSurfaces => 400, + ElevationIndex::ModalSurface => 400, ElevationIndex::DraggedElement => 900, } } pub fn shadow(self) -> SmallVec<[BoxShadow; 2]> { match self { - ElevationIndex::AppBackground => smallvec![], + ElevationIndex::Surface => smallvec![], - ElevationIndex::UISurface => smallvec![BoxShadow { + ElevationIndex::ElevatedSurface => smallvec![BoxShadow { color: hsla(0., 0., 0., 0.12), offset: point(px(0.), px(1.)), blur_radius: px(3.), spread_radius: px(0.), }], - _ => smallvec![BoxShadow { - color: hsla(0., 0., 0., 0.32), - offset: point(px(1.), px(3.)), - blur_radius: px(12.), - spread_radius: px(0.), - }], + ElevationIndex::ModalSurface => smallvec![ + BoxShadow { + color: hsla(0., 0., 0., 0.12), + offset: point(px(0.), px(1.)), + blur_radius: px(3.), + spread_radius: px(0.), + }, + BoxShadow { + color: hsla(0., 0., 0., 0.16), + offset: point(px(3.), px(1.)), + blur_radius: px(12.), + spread_radius: px(0.), + }, + ], + + _ => smallvec![], } } } diff --git a/crates/ui2/src/styled_ext.rs b/crates/ui2/src/styled_ext.rs index 543781ef52..3d6af476a4 100644 --- a/crates/ui2/src/styled_ext.rs +++ b/crates/ui2/src/styled_ext.rs @@ -1,33 +1,33 @@ -use gpui::{Div, ElementFocus, ElementInteractivity, Styled}; +use gpui::{Div, ElementFocus, ElementInteractivity, Styled, UniformList, ViewContext}; +use theme2::ActiveTheme; -use crate::UITextSize; +use crate::{ElevationIndex, UITextSize}; + +fn elevated(this: E, cx: &mut ViewContext, index: ElevationIndex) -> E { + this.bg(cx.theme().colors().elevated_surface_background) + .rounded_lg() + .border() + .border_color(cx.theme().colors().border_variant) + .shadow(index.shadow()) +} /// Extends [`Styled`](gpui::Styled) with Zed specific styling methods. -pub trait StyledExt: Styled { +pub trait StyledExt: Styled + Sized { /// Horizontally stacks elements. /// /// Sets `flex()`, `flex_row()`, `items_center()` - fn h_flex(self) -> Self - where - Self: Sized, - { + fn h_flex(self) -> Self { self.flex().flex_row().items_center() } /// Vertically stacks elements. /// /// Sets `flex()`, `flex_col()` - fn v_flex(self) -> Self - where - Self: Sized, - { + fn v_flex(self) -> Self { self.flex().flex_col() } - fn text_ui_size(self, size: UITextSize) -> Self - where - Self: Sized, - { + fn text_ui_size(self, size: UITextSize) -> Self { let size = size.rems(); self.text_size(size) @@ -40,10 +40,7 @@ pub trait StyledExt: Styled { /// Note: The absolute size of this text will change based on a user's `ui_scale` setting. /// /// Use [`text_ui_sm`] for regular-sized text. - fn text_ui(self) -> Self - where - Self: Sized, - { + fn text_ui(self) -> Self { let size = UITextSize::default().rems(); self.text_size(size) @@ -56,14 +53,44 @@ pub trait StyledExt: Styled { /// Note: The absolute size of this text will change based on a user's `ui_scale` setting. /// /// Use [`text_ui`] for regular-sized text. - fn text_ui_sm(self) -> Self - where - Self: Sized, - { + fn text_ui_sm(self) -> Self { let size = UITextSize::Small.rems(); self.text_size(size) } + + /// The [`Surface`](ui2::ElevationIndex::Surface) elevation level, located above the app background, is the standard level for all elements + /// + /// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()` + /// + /// Example Elements: Title Bar, Panel, Tab Bar, Editor + fn elevation_1(self, cx: &mut ViewContext) -> Self { + elevated(self, cx, ElevationIndex::Surface) + } + + /// Non-Modal Elevated Surfaces appear above the [`Surface`](ui2::ElevationIndex::Surface) layer and is used for things that should appear above most UI elements like an editor or panel, but not elements like popovers, context menus, modals, etc. + /// + /// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()` + /// + /// Examples: Notifications, Palettes, Detached/Floating Windows, Detached/Floating Panels + fn elevation_2(self, cx: &mut ViewContext) -> Self { + elevated(self, cx, ElevationIndex::ElevatedSurface) + } + + // There is no elevation 3, as the third elevation level is reserved for wash layers. See [`Elevation`](ui2::Elevation). + + /// Modal Surfaces are used for elements that should appear above all other UI elements and are located above the wash layer. This is the maximum elevation at which UI elements can be rendered in their default state. + /// + /// Elements rendered at this layer should have an enforced behavior: Any interaction outside of the modal will either dismiss the modal or prompt an action (Save your progress, etc) then dismiss the modal. + /// + /// If the element does not have this behavior, it should be rendered at the [`Elevated Surface`](ui2::ElevationIndex::ElevatedSurface) layer. + /// + /// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()` + /// + /// Examples: Settings Modal, Channel Management, Wizards/Setup UI, Dialogs + fn elevation_4(self, cx: &mut ViewContext) -> Self { + elevated(self, cx, ElevationIndex::ModalSurface) + } } impl StyledExt for Div @@ -72,3 +99,5 @@ where F: ElementFocus, { } + +impl StyledExt for UniformList {} diff --git a/crates/workspace2/src/modal_layer.rs b/crates/workspace2/src/modal_layer.rs index f197718b59..09ffa6c13f 100644 --- a/crates/workspace2/src/modal_layer.rs +++ b/crates/workspace2/src/modal_layer.rs @@ -1,15 +1,22 @@ -use crate::Workspace; use gpui::{ - div, px, AnyView, Component, Div, EventEmitter, ParentElement, Render, StatelessInteractive, - Styled, Subscription, View, ViewContext, + div, px, AnyView, Div, EventEmitter, FocusHandle, ParentElement, Render, StatelessInteractive, + Styled, Subscription, View, ViewContext, VisualContext, WindowContext, }; -use std::{any::TypeId, sync::Arc}; use ui::v_stack; +pub struct ActiveModal { + modal: AnyView, + subscription: Subscription, + previous_focus_handle: Option, + focus_handle: FocusHandle, +} + pub struct ModalLayer { - open_modal: Option, - subscription: Option, - registered_modals: Vec<(TypeId, Box) -> Div>)>, + active_modal: Option, +} + +pub trait Modal: Render + EventEmitter { + fn focus(&self, cx: &mut WindowContext); } pub enum ModalEvent { @@ -18,74 +25,82 @@ pub enum ModalEvent { impl ModalLayer { pub fn new() -> Self { - Self { - open_modal: None, - subscription: None, - registered_modals: Vec::new(), - } + Self { active_modal: None } } - pub fn register_modal(&mut self, action: A, build_view: B) + pub fn toggle_modal(&mut self, cx: &mut ViewContext, build_view: B) where - V: EventEmitter + Render, - B: Fn(&mut Workspace, &mut ViewContext) -> Option> + 'static, + V: Modal, + B: FnOnce(&mut ViewContext) -> V, { - let build_view = Arc::new(build_view); + let previous_focus = cx.focused(); - self.registered_modals.push(( - TypeId::of::(), - Box::new(move |mut div| { - let build_view = build_view.clone(); + if let Some(active_modal) = &self.active_modal { + let is_close = active_modal.modal.clone().downcast::().is_ok(); + self.hide_modal(cx); + if is_close { + return; + } + } + let new_modal = cx.build_view(build_view); + self.show_modal(new_modal, cx); + } - div.on_action(move |workspace, event: &A, cx| { - let Some(new_modal) = (build_view)(workspace, cx) else { - return; - }; - workspace.modal_layer().show_modal(new_modal, cx); - }) + pub fn show_modal(&mut self, new_modal: View, cx: &mut ViewContext) + where + V: Modal, + { + self.active_modal = Some(ActiveModal { + modal: new_modal.clone().into(), + subscription: cx.subscribe(&new_modal, |this, modal, e, cx| match e { + ModalEvent::Dismissed => this.hide_modal(cx), }), - )); - } - - pub fn show_modal(&mut self, new_modal: View, cx: &mut ViewContext) - where - V: EventEmitter + Render, - { - self.subscription = Some(cx.subscribe(&new_modal, |this, modal, e, cx| match e { - ModalEvent::Dismissed => this.modal_layer().hide_modal(cx), - })); - self.open_modal = Some(new_modal.into()); + previous_focus_handle: cx.focused(), + focus_handle: cx.focus_handle(), + }); + new_modal.update(cx, |modal, cx| modal.focus(cx)); cx.notify(); } - pub fn hide_modal(&mut self, cx: &mut ViewContext) { - self.open_modal.take(); - self.subscription.take(); - cx.notify(); - } - - pub fn wrapper_element(&self, cx: &ViewContext) -> Div { - let mut parent = div().relative().size_full(); - - for (_, action) in self.registered_modals.iter() { - parent = (action)(parent); + pub fn hide_modal(&mut self, cx: &mut ViewContext) { + if let Some(active_modal) = self.active_modal.take() { + if let Some(previous_focus) = active_modal.previous_focus_handle { + if active_modal.focus_handle.contains_focused(cx) { + previous_focus.focus(cx); + } + } } - parent.when_some(self.open_modal.as_ref(), |parent, open_modal| { - let container1 = div() - .absolute() - .flex() - .flex_col() - .items_center() - .size_full() - .top_0() - .left_0() - .z_index(400); - - // transparent layer - let container2 = v_stack().h(px(0.0)).relative().top_20(); - - parent.child(container1.child(container2.child(open_modal.clone()))) - }) + cx.notify(); + } +} + +impl Render for ModalLayer { + type Element = Div; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + let Some(active_modal) = &self.active_modal else { + return div(); + }; + + div() + .absolute() + .flex() + .flex_col() + .items_center() + .size_full() + .top_0() + .left_0() + .z_index(400) + .child( + v_stack() + .h(px(0.0)) + .top_20() + .track_focus(&active_modal.focus_handle) + .on_mouse_down_out(|this: &mut Self, event, cx| { + this.hide_modal(cx); + }) + .child(active_modal.modal.clone()), + ) } } diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 8c1c5d9a02..45c3a08908 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -36,11 +36,12 @@ use futures::{ Future, FutureExt, StreamExt, }; use gpui::{ - actions, div, point, rems, size, AnyModel, AnyView, AnyWeakView, AppContext, AsyncAppContext, - AsyncWindowContext, Bounds, Component, Div, Entity, EntityId, EventEmitter, FocusHandle, - GlobalPixels, Model, ModelContext, ParentElement, Point, Render, Size, StatefulInteractive, - StatelessInteractive, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, - WindowBounds, WindowContext, WindowHandle, WindowOptions, + actions, div, point, rems, size, Action, AnyModel, AnyView, AnyWeakView, AppContext, + AsyncAppContext, AsyncWindowContext, Bounds, Component, DispatchContext, Div, Entity, EntityId, + EventEmitter, FocusHandle, GlobalPixels, Model, ModelContext, ParentElement, Point, Render, + Size, StatefulInteractive, StatefulInteractivity, StatelessInteractive, Styled, Subscription, + Task, View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, + WindowOptions, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; use itertools::Itertools; @@ -434,6 +435,13 @@ pub enum Event { pub struct Workspace { weak_self: WeakView, focus_handle: FocusHandle, + workspace_actions: Vec< + Box< + dyn Fn( + Div>, + ) -> Div>, + >, + >, zoomed: Option, zoomed_position: Option, center: PaneGroup, @@ -446,7 +454,7 @@ pub struct Workspace { last_active_center_pane: Option>, last_active_view_id: Option, status_bar: View, - modal_layer: ModalLayer, + modal_layer: View, // titlebar_item: Option, notifications: Vec<(TypeId, usize, Box)>, project: Model, @@ -598,7 +606,7 @@ impl Workspace { }); let workspace_handle = cx.view().downgrade(); - let modal_layer = ModalLayer::new(); + let modal_layer = cx.build_view(|cx| ModalLayer::new()); // todo!() // cx.update_default_global::, _, _>(|drag_and_drop, _| { @@ -679,13 +687,10 @@ impl Workspace { leader_updates_tx, subscriptions, pane_history_timestamp, + workspace_actions: Default::default(), } } - pub fn modal_layer(&mut self) -> &mut ModalLayer { - &mut self.modal_layer - } - fn new_local( abs_paths: Vec, app_state: Arc, @@ -3356,23 +3361,27 @@ impl Workspace { // #[cfg(any(test, feature = "test-support"))] // pub fn test_new(project: ModelHandle, cx: &mut ViewContext) -> Self { // use node_runtime::FakeNodeRuntime; + #[cfg(any(test, feature = "test-support"))] + pub fn test_new(project: Model, cx: &mut ViewContext) -> Self { + use gpui::Context; + use node_runtime::FakeNodeRuntime; - // let client = project.read(cx).client(); - // let user_store = project.read(cx).user_store(); + let client = project.read(cx).client(); + let user_store = project.read(cx).user_store(); - // let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx)); - // let app_state = Arc::new(AppState { - // languages: project.read(cx).languages().clone(), - // workspace_store, - // client, - // user_store, - // fs: project.read(cx).fs().clone(), - // build_window_options: |_, _, _| Default::default(), - // initialize_workspace: |_, _, _, _| Task::ready(Ok(())), - // node_runtime: FakeNodeRuntime::new(), - // }); - // Self::new(0, project, app_state, cx) - // } + let workspace_store = cx.build_model(|cx| WorkspaceStore::new(client.clone(), cx)); + let app_state = Arc::new(AppState { + languages: project.read(cx).languages().clone(), + workspace_store, + client, + user_store, + fs: project.read(cx).fs().clone(), + build_window_options: |_, _, _| Default::default(), + initialize_workspace: |_, _, _, _| Task::ready(Ok(())), + node_runtime: FakeNodeRuntime::new(), + }); + Self::new(0, project, app_state, cx) + } // fn render_dock(&self, position: DockPosition, cx: &WindowContext) -> Option> { // let dock = match position { @@ -3404,6 +3413,35 @@ impl Workspace { // ) // } // } + pub fn register_action( + &mut self, + callback: impl Fn(&mut Self, &A, &mut ViewContext) + 'static, + ) { + let callback = Arc::new(callback); + + self.workspace_actions.push(Box::new(move |div| { + let callback = callback.clone(); + div.on_action(move |workspace, event, cx| (callback.clone())(workspace, event, cx)) + })); + } + + fn add_workspace_actions_listeners( + &self, + mut div: Div>, + ) -> Div> { + for action in self.workspace_actions.iter() { + div = (action)(div) + } + div + } + + pub fn toggle_modal(&mut self, cx: &mut ViewContext, build: B) + where + B: FnOnce(&mut ViewContext) -> V, + { + self.modal_layer + .update(cx, |modal_layer, cx| modal_layer.toggle_modal(cx, build)) + } } fn window_bounds_env_override(cx: &AsyncAppContext) -> Option { @@ -3618,142 +3656,110 @@ impl Render for Workspace { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - div() - .relative() - .size_full() - .flex() - .flex_col() - .font("Zed Sans") - .gap_0() - .justify_start() - .items_start() - .text_color(cx.theme().colors().text) - .bg(cx.theme().colors().background) - .child(self.render_titlebar(cx)) - .child(Workspace::actions( - // todo! should this be a component a view? - self.modal_layer - .wrapper_element(cx) - .relative() - .flex_1() - .w_full() - .flex() - .overflow_hidden() - .border_t() - .border_b() - .border_color(cx.theme().colors().border) - .child( - div() - .flex() - .flex_row() - .flex_1() - .h_full() - .child(div().flex().flex_1().child(self.left_dock.clone())) - .child( - div() - .flex() - .flex_col() - .flex_1() - .child(self.center.render( - &self.project, - &self.follower_states, - self.active_call(), - &self.active_pane, - self.zoomed.as_ref(), - &self.app_state, - cx, - )) - .child(div().flex().flex_1().child(self.bottom_dock.clone())), - ) - .child(div().flex().flex_1().child(self.right_dock.clone())), - ), // .children( - // Some( - // Panel::new("chat-panel-outer", cx) - // .side(PanelSide::Right) - // .child(ChatPanel::new("chat-panel-inner").messages(vec![ - // ChatMessage::new( - // "osiewicz".to_string(), - // "is this thing on?".to_string(), - // DateTime::parse_from_rfc3339("2023-09-27T15:40:52.707Z") - // .unwrap() - // .naive_local(), - // ), - // ChatMessage::new( - // "maxdeviant".to_string(), - // "Reading you loud and clear!".to_string(), - // DateTime::parse_from_rfc3339("2023-09-28T15:40:52.707Z") - // .unwrap() - // .naive_local(), - // ), - // ])), - // ) - // .filter(|_| self.is_chat_panel_open()), - // ) - // .children( - // Some( - // Panel::new("notifications-panel-outer", cx) - // .side(PanelSide::Right) - // .child(NotificationsPanel::new("notifications-panel-inner")), - // ) - // .filter(|_| self.is_notifications_panel_open()), - // ) - // .children( - // Some( - // Panel::new("assistant-panel-outer", cx) - // .child(AssistantPanel::new("assistant-panel-inner")), - // ) - // .filter(|_| self.is_assistant_panel_open()), - // ), - )) - .child(self.status_bar.clone()) - // .when(self.debug.show_toast, |this| { - // this.child(Toast::new(ToastOrigin::Bottom).child(Label::new("A toast"))) - // }) - // .children( - // Some( - // div() - // .absolute() - // .top(px(50.)) - // .left(px(640.)) - // .z_index(8) - // .child(LanguageSelector::new("language-selector")), - // ) - // .filter(|_| self.is_language_selector_open()), - // ) - .z_index(8) - // Debug - .child( - div() - .flex() - .flex_col() - .z_index(9) - .absolute() - .top_20() - .left_1_4() - .w_40() - .gap_2(), // .when(self.show_debug, |this| { - // this.child(Button::::new("Toggle User Settings").on_click( - // Arc::new(|workspace, cx| workspace.debug_toggle_user_settings(cx)), - // )) - // .child( - // Button::::new("Toggle Toasts").on_click(Arc::new( - // |workspace, cx| workspace.debug_toggle_toast(cx), - // )), - // ) - // .child( - // Button::::new("Toggle Livestream").on_click(Arc::new( - // |workspace, cx| workspace.debug_toggle_livestream(cx), - // )), - // ) - // }) - // .child( - // Button::::new("Toggle Debug") - // .on_click(Arc::new(|workspace, cx| workspace.toggle_debug(cx))), - // ), - ) + let mut context = DispatchContext::default(); + context.insert("Workspace"); + cx.with_key_dispatch_context(context, |cx| { + div() + .relative() + .size_full() + .flex() + .flex_col() + .font("Zed Sans") + .gap_0() + .justify_start() + .items_start() + .text_color(cx.theme().colors().text) + .bg(cx.theme().colors().background) + .child(self.render_titlebar(cx)) + .child( + // todo! should this be a component a view? + self.add_workspace_actions_listeners(div().id("workspace")) + .relative() + .flex_1() + .w_full() + .flex() + .overflow_hidden() + .border_t() + .border_b() + .border_color(cx.theme().colors().border) + .child(self.modal_layer.clone()) + .child( + div() + .flex() + .flex_row() + .flex_1() + .h_full() + .child(div().flex().flex_1().child(self.left_dock.clone())) + .child( + div() + .flex() + .flex_col() + .flex_1() + .child(self.center.render( + &self.project, + &self.follower_states, + self.active_call(), + &self.active_pane, + self.zoomed.as_ref(), + &self.app_state, + cx, + )) + .child( + div().flex().flex_1().child(self.bottom_dock.clone()), + ), + ) + .child(div().flex().flex_1().child(self.right_dock.clone())), + ), + ) + .child(self.status_bar.clone()) + // .when(self.debug.show_toast, |this| { + // this.child(Toast::new(ToastOrigin::Bottom).child(Label::new("A toast"))) + // }) + // .children( + // Some( + // div() + // .absolute() + // .top(px(50.)) + // .left(px(640.)) + // .z_index(8) + // .child(LanguageSelector::new("language-selector")), + // ) + // .filter(|_| self.is_language_selector_open()), + // ) + .z_index(8) + // Debug + .child( + div() + .flex() + .flex_col() + .z_index(9) + .absolute() + .top_20() + .left_1_4() + .w_40() + .gap_2(), // .when(self.show_debug, |this| { + // this.child(Button::::new("Toggle User Settings").on_click( + // Arc::new(|workspace, cx| workspace.debug_toggle_user_settings(cx)), + // )) + // .child( + // Button::::new("Toggle Toasts").on_click(Arc::new( + // |workspace, cx| workspace.debug_toggle_toast(cx), + // )), + // ) + // .child( + // Button::::new("Toggle Livestream").on_click(Arc::new( + // |workspace, cx| workspace.debug_toggle_livestream(cx), + // )), + // ) + // }) + // .child( + // Button::::new("Toggle Debug") + // .on_click(Arc::new(|workspace, cx| workspace.toggle_debug(cx))), + // ), + ) + }) } } - // todo!() // impl Entity for Workspace { // type Event = Event; diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml index e03c44547b..b816b59661 100644 --- a/crates/zed2/Cargo.toml +++ b/crates/zed2/Cargo.toml @@ -25,7 +25,7 @@ call = { package = "call2", path = "../call2" } cli = { path = "../cli" } # collab_ui = { path = "../collab_ui" } collections = { path = "../collections" } -# command_palette = { path = "../command_palette" } +command_palette = { package="command_palette2", path = "../command_palette2" } # component_test = { path = "../component_test" } # context_menu = { path = "../context_menu" } client = { package = "client2", path = "../client2" } @@ -74,7 +74,7 @@ util = { path = "../util" } # vim = { path = "../vim" } workspace = { package = "workspace2", path = "../workspace2" } # welcome = { path = "../welcome" } -# zed-actions = {path = "../zed-actions"} +zed_actions = {package = "zed_actions2", path = "../zed_actions2"} anyhow.workspace = true async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] } async-tar = "0.4.2" diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index 6a76ffac48..fda3a5e926 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -142,7 +142,7 @@ fn main() { // context_menu::init(cx); project::Project::init(&client, cx); client::init(&client, cx); - // command_palette::init(cx); + command_palette::init(cx); language::init(cx); editor::init(cx); copilot::init( @@ -761,7 +761,7 @@ fn load_embedded_fonts(cx: &AppContext) { // #[cfg(not(debug_assertions))] // async fn watch_languages(_: Arc, _: Arc) -> Option<()> { // None -// } +// // #[cfg(not(debug_assertions))] // fn watch_file_types(_fs: Arc, _cx: &mut AppContext) {} diff --git a/crates/zed_actions2/Cargo.toml b/crates/zed_actions2/Cargo.toml new file mode 100644 index 0000000000..b3b5b4ce57 --- /dev/null +++ b/crates/zed_actions2/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "zed_actions2" +version = "0.1.0" +edition = "2021" +publish = false + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +gpui = { package = "gpui2", path = "../gpui2" } +serde.workspace = true diff --git a/crates/zed_actions2/src/lib.rs b/crates/zed_actions2/src/lib.rs new file mode 100644 index 0000000000..090352b2cc --- /dev/null +++ b/crates/zed_actions2/src/lib.rs @@ -0,0 +1,34 @@ +use gpui::{action, actions}; + +actions!( + About, + DebugElements, + DecreaseBufferFontSize, + Hide, + HideOthers, + IncreaseBufferFontSize, + Minimize, + OpenDefaultKeymap, + OpenDefaultSettings, + OpenKeymap, + OpenLicenses, + OpenLocalSettings, + OpenLog, + OpenSettings, + OpenTelemetryLog, + Quit, + ResetBufferFontSize, + ResetDatabase, + ShowAll, + ToggleFullScreen, + Zoom, +); + +#[action] +pub struct OpenBrowser { + pub url: String, +} +#[action] +pub struct OpenZedURL { + pub url: String, +}