diff --git a/.cargo/config.toml b/.cargo/config.toml index 717c5e18c8..8db58d2380 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -19,6 +19,8 @@ rustflags = [ "windows_slim_errors", # This cfg will reduce the size of `windows::core::Error` from 16 bytes to 4 bytes "-C", "target-feature=+crt-static", # This fixes the linking issue when compiling livekit on Windows + "-C", + "link-arg=-fuse-ld=lld", ] [env] diff --git a/.config/hakari.toml b/.config/hakari.toml index bd742b33cd..8ce0b77490 100644 --- a/.config/hakari.toml +++ b/.config/hakari.toml @@ -23,6 +23,10 @@ workspace-members = [ ] third-party = [ { name = "reqwest", version = "0.11.27" }, + # build of remote_server should not include scap / its x11 dependency + { name = "scap", git = "https://github.com/zed-industries/scap", rev = "808aa5c45b41e8f44729d02e38fd00a2fe2722e7" }, + # build of remote_server should not need to include on libalsa through rodio + { name = "rodio" }, ] [final-excludes] @@ -30,10 +34,8 @@ workspace-members = [ "zed_extension_api", # exclude all extensions - "zed_emmet", "zed_glsl", "zed_html", - "perplexity", "zed_proto", "zed_ruff", "slash_commands_example", diff --git a/.github/ISSUE_TEMPLATE/01_bug_ai.yml b/.github/ISSUE_TEMPLATE/01_bug_ai.yml index 990f403365..16bdef6c7e 100644 --- a/.github/ISSUE_TEMPLATE/01_bug_ai.yml +++ b/.github/ISSUE_TEMPLATE/01_bug_ai.yml @@ -1,4 +1,4 @@ -name: Bug Report (AI Related) +name: Bug Report (AI) description: Zed Agent Panel Bugs type: "Bug" labels: ["ai"] @@ -19,15 +19,14 @@ body: 2. 3. - Actual Behavior: - Expected Behavior: + **Expected Behavior**: + **Actual Behavior**: ### Model Provider Details - Provider: (Anthropic via ZedPro, Anthropic via API key, Copilot Chat, Mistral, OpenAI, etc) - Model Name: - Mode: (Agent Panel, Inline Assistant, Terminal Assistant or Text Threads) - - MCP Servers in-use: - - Other Details: + - Other Details (MCPs, other settings, etc): validations: required: true diff --git a/.github/ISSUE_TEMPLATE/02_bug_edit_predictions.yml b/.github/ISSUE_TEMPLATE/02_bug_edit_predictions.yml deleted file mode 100644 index 9705bfee7f..0000000000 --- a/.github/ISSUE_TEMPLATE/02_bug_edit_predictions.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Bug Report (Edit Predictions) -description: Zed Edit Predictions bugs -type: "Bug" -labels: ["ai", "inline completion", "zeta"] -title: "Edit Predictions: " -body: - - type: textarea - attributes: - label: Summary - description: Describe the bug with a one line summary, and provide detailed reproduction steps - value: | - - SUMMARY_SENTENCE_HERE - - ### Description - - - Steps to trigger the problem: - 1. - 2. - 3. - - Actual Behavior: - Expected Behavior: - validations: - required: true - - - type: textarea - id: environment - attributes: - label: Zed Version and System Specs - description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"' - placeholder: | - Output of "zed: copy system specs into clipboard" - validations: - required: true diff --git a/.github/ISSUE_TEMPLATE/04_bug_debugger.yml b/.github/ISSUE_TEMPLATE/04_bug_debugger.yml index 7f2a3ad1e9..2682295a43 100644 --- a/.github/ISSUE_TEMPLATE/04_bug_debugger.yml +++ b/.github/ISSUE_TEMPLATE/04_bug_debugger.yml @@ -19,8 +19,8 @@ body: 2. 3. - Actual Behavior: - Expected Behavior: + **Expected Behavior**: + **Actual Behavior**: validations: required: true diff --git a/.github/ISSUE_TEMPLATE/03_bug_git.yml b/.github/ISSUE_TEMPLATE/07_bug_windows_alpha.yml similarity index 66% rename from .github/ISSUE_TEMPLATE/03_bug_git.yml rename to .github/ISSUE_TEMPLATE/07_bug_windows_alpha.yml index 1351ba7952..826c2b8027 100644 --- a/.github/ISSUE_TEMPLATE/03_bug_git.yml +++ b/.github/ISSUE_TEMPLATE/07_bug_windows_alpha.yml @@ -1,15 +1,15 @@ -name: Bug Report (Git) -description: Zed Git-Related Bugs +name: Bug Report (Windows Alpha) +description: Zed Windows Alpha Related Bugs type: "Bug" -labels: ["git"] -title: "Git: " +labels: ["windows"] +title: "Windows Alpha: " body: - type: textarea attributes: label: Summary - description: Describe the bug with a one line summary, and provide detailed reproduction steps + description: Describe the bug with a one-line summary, and provide detailed reproduction steps value: | - + SUMMARY_SENTENCE_HERE ### Description @@ -19,8 +19,8 @@ body: 2. 3. - Actual Behavior: - Expected Behavior: + **Expected Behavior**: + **Actual Behavior**: validations: required: true diff --git a/.github/ISSUE_TEMPLATE/10_bug_report.yml b/.github/ISSUE_TEMPLATE/10_bug_report.yml index f6c6082187..e132eca1e5 100644 --- a/.github/ISSUE_TEMPLATE/10_bug_report.yml +++ b/.github/ISSUE_TEMPLATE/10_bug_report.yml @@ -18,14 +18,16 @@ body: - Issues with insufficient detail may be summarily closed. --> + DESCRIPTION_HERE + Steps to reproduce: 1. 2. 3. 4. - Expected Behavior: - Actual Behavior: + **Expected Behavior**: + **Actual Behavior**: "; fn main() -> Result<()> { - let matches = make_app().get_matches(); + zlog::init(); + zlog::init_output_stderr(); + // call a zed:: function so everything in `zed` crate is linked and + // all actions in the actual app are registered + zed::stdout_is_a_pty(); + let args = std::env::args().skip(1).collect::>(); - if let Some(sub_args) = matches.subcommand_matches("supports") { - handle_supports(sub_args); - } else { - handle_preprocessing()?; + match args.get(0).map(String::as_str) { + Some("supports") => { + let renderer = args.get(1).expect("Required argument"); + let supported = renderer != "not-supported"; + if supported { + process::exit(0); + } else { + process::exit(1); + } + } + Some("postprocess") => handle_postprocessing()?, + _ => handle_preprocessing()?, } Ok(()) } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +enum PreprocessorError { + ActionNotFound { action_name: String }, + DeprecatedActionUsed { used: String, should_be: String }, + InvalidFrontmatterLine(String), +} + +impl PreprocessorError { + fn new_for_not_found_action(action_name: String) -> Self { + for action in &*ALL_ACTIONS { + for alias in action.deprecated_aliases { + if alias == &action_name { + return PreprocessorError::DeprecatedActionUsed { + used: action_name, + should_be: action.name.to_string(), + }; + } + } + } + PreprocessorError::ActionNotFound { action_name } + } +} + +impl std::fmt::Display for PreprocessorError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PreprocessorError::InvalidFrontmatterLine(line) => { + write!(f, "Invalid frontmatter line: {}", line) + } + PreprocessorError::ActionNotFound { action_name } => { + write!(f, "Action not found: {}", action_name) + } + PreprocessorError::DeprecatedActionUsed { used, should_be } => write!( + f, + "Deprecated action used: {} should be {}", + used, should_be + ), + } + } +} + fn handle_preprocessing() -> Result<()> { let mut stdin = io::stdin(); let mut input = String::new(); @@ -46,33 +96,83 @@ fn handle_preprocessing() -> Result<()> { let (_ctx, mut book) = CmdPreprocessor::parse_input(input.as_bytes())?; - template_keybinding(&mut book); - template_action(&mut book); + let mut errors = HashSet::::new(); + + handle_frontmatter(&mut book, &mut errors); + template_big_table_of_actions(&mut book); + template_and_validate_keybindings(&mut book, &mut errors); + template_and_validate_actions(&mut book, &mut errors); + + if !errors.is_empty() { + const ANSI_RED: &str = "\x1b[31m"; + const ANSI_RESET: &str = "\x1b[0m"; + for error in &errors { + eprintln!("{ANSI_RED}ERROR{ANSI_RESET}: {}", error); + } + return Err(anyhow::anyhow!("Found {} errors in docs", errors.len())); + } serde_json::to_writer(io::stdout(), &book)?; Ok(()) } -fn handle_supports(sub_args: &ArgMatches) -> ! { - let renderer = sub_args - .get_one::("renderer") - .expect("Required argument"); - let supported = renderer != "not-supported"; - if supported { - process::exit(0); - } else { - process::exit(1); - } +fn handle_frontmatter(book: &mut Book, errors: &mut HashSet) { + let frontmatter_regex = Regex::new(r"(?s)^\s*---(.*?)---").unwrap(); + for_each_chapter_mut(book, |chapter| { + let new_content = frontmatter_regex.replace(&chapter.content, |caps: ®ex::Captures| { + let frontmatter = caps[1].trim(); + let frontmatter = frontmatter.trim_matches(&[' ', '-', '\n']); + let mut metadata = HashMap::::default(); + for line in frontmatter.lines() { + let Some((name, value)) = line.split_once(':') else { + errors.insert(PreprocessorError::InvalidFrontmatterLine(format!( + "{}: {}", + chapter_breadcrumbs(chapter), + line + ))); + continue; + }; + let name = name.trim(); + let value = value.trim(); + metadata.insert(name.to_string(), value.to_string()); + } + FRONT_MATTER_COMMENT.replace( + "{}", + &serde_json::to_string(&metadata).expect("Failed to serialize metadata"), + ) + }); + if let Cow::Owned(content) = new_content { + chapter.content = content; + } + }); } -fn template_keybinding(book: &mut Book) { +fn template_big_table_of_actions(book: &mut Book) { + for_each_chapter_mut(book, |chapter| { + let needle = "{#ACTIONS_TABLE#}"; + if let Some(start) = chapter.content.rfind(needle) { + chapter.content.replace_range( + start..start + needle.len(), + &generate_big_table_of_actions(), + ); + } + }); +} + +fn template_and_validate_keybindings(book: &mut Book, errors: &mut HashSet) { let regex = Regex::new(r"\{#kb (.*?)\}").unwrap(); for_each_chapter_mut(book, |chapter| { chapter.content = regex .replace_all(&chapter.content, |caps: ®ex::Captures| { let action = caps[1].trim(); + if find_action_by_name(action).is_none() { + errors.insert(PreprocessorError::new_for_not_found_action( + action.to_string(), + )); + return String::new(); + } let macos_binding = find_binding("macos", action).unwrap_or_default(); let linux_binding = find_binding("linux", action).unwrap_or_default(); @@ -86,39 +186,36 @@ fn template_keybinding(book: &mut Book) { }); } -fn template_action(book: &mut Book) { +fn template_and_validate_actions(book: &mut Book, errors: &mut HashSet) { let regex = Regex::new(r"\{#action (.*?)\}").unwrap(); for_each_chapter_mut(book, |chapter| { chapter.content = regex .replace_all(&chapter.content, |caps: ®ex::Captures| { let name = caps[1].trim(); - - let formatted_name = name - .chars() - .enumerate() - .map(|(i, c)| { - if i > 0 && c.is_uppercase() { - format!(" {}", c.to_lowercase()) - } else { - c.to_string() - } - }) - .collect::() - .trim() - .to_string() - .replace("::", ":"); - - format!("{}", formatted_name) + let Some(action) = find_action_by_name(name) else { + errors.insert(PreprocessorError::new_for_not_found_action( + name.to_string(), + )); + return String::new(); + }; + format!("{}", &action.human_name) }) .into_owned() }); } +fn find_action_by_name(name: &str) -> Option<&ActionDef> { + ALL_ACTIONS + .binary_search_by(|action| action.name.cmp(name)) + .ok() + .map(|index| &ALL_ACTIONS[index]) +} + fn find_binding(os: &str, action: &str) -> Option { let keymap = match os { "macos" => &KEYMAP_MACOS, - "linux" => &KEYMAP_LINUX, + "linux" | "freebsd" => &KEYMAP_LINUX, _ => unreachable!("Not a valid OS: {}", os), }; @@ -164,6 +261,13 @@ fn name_for_action(action_as_str: String) -> String { .unwrap_or(action_as_str) } +fn chapter_breadcrumbs(chapter: &Chapter) -> String { + let mut breadcrumbs = Vec::with_capacity(chapter.parent_names.len() + 1); + breadcrumbs.extend(chapter.parent_names.iter().map(String::as_str)); + breadcrumbs.push(chapter.name.as_str()); + format!("[{:?}] {}", chapter.source_path, breadcrumbs.join(" > ")) +} + fn load_keymap(asset_path: &str) -> Result { let content = util::asset_str::(asset_path); KeymapFile::parse(content.as_ref()) @@ -180,3 +284,203 @@ where func(chapter); }); } + +#[derive(Debug, serde::Serialize)] +struct ActionDef { + name: &'static str, + human_name: String, + deprecated_aliases: &'static [&'static str], + docs: Option<&'static str>, +} + +fn dump_all_gpui_actions() -> Vec { + let mut actions = gpui::generate_list_of_all_registered_actions() + .map(|action| ActionDef { + name: action.name, + human_name: command_palette::humanize_action_name(action.name), + deprecated_aliases: action.deprecated_aliases, + docs: action.documentation, + }) + .collect::>(); + + actions.sort_by_key(|a| a.name); + + actions +} + +fn handle_postprocessing() -> Result<()> { + let logger = zlog::scoped!("render"); + let mut ctx = mdbook::renderer::RenderContext::from_json(io::stdin())?; + let output = ctx + .config + .get_mut("output") + .expect("has output") + .as_table_mut() + .expect("output is table"); + let zed_html = output.remove("zed-html").expect("zed-html output defined"); + let default_description = zed_html + .get("default-description") + .expect("Default description not found") + .as_str() + .expect("Default description not a string") + .to_string(); + let default_title = zed_html + .get("default-title") + .expect("Default title not found") + .as_str() + .expect("Default title not a string") + .to_string(); + + output.insert("html".to_string(), zed_html); + mdbook::Renderer::render(&mdbook::renderer::HtmlHandlebars::new(), &ctx)?; + let ignore_list = ["toc.html"]; + + let root_dir = ctx.destination.clone(); + let mut files = Vec::with_capacity(128); + let mut queue = Vec::with_capacity(64); + queue.push(root_dir.clone()); + while let Some(dir) = queue.pop() { + for entry in std::fs::read_dir(&dir).context(dir.to_sanitized_string())? { + let Ok(entry) = entry else { + continue; + }; + let file_type = entry.file_type().context("Failed to determine file type")?; + if file_type.is_dir() { + queue.push(entry.path()); + } + if file_type.is_file() + && matches!( + entry.path().extension().and_then(std::ffi::OsStr::to_str), + Some("html") + ) + { + if ignore_list.contains(&&*entry.file_name().to_string_lossy()) { + zlog::info!(logger => "Ignoring {}", entry.path().to_string_lossy()); + } else { + files.push(entry.path()); + } + } + } + } + + zlog::info!(logger => "Processing {} `.html` files", files.len()); + let meta_regex = Regex::new(&FRONT_MATTER_COMMENT.replace("{}", "(.*)")).unwrap(); + for file in files { + let contents = std::fs::read_to_string(&file)?; + let mut meta_description = None; + let mut meta_title = None; + let contents = meta_regex.replace(&contents, |caps: ®ex::Captures| { + let metadata: HashMap = serde_json::from_str(&caps[1]).with_context(|| format!("JSON Metadata: {:?}", &caps[1])).expect("Failed to deserialize metadata"); + for (kind, content) in metadata { + match kind.as_str() { + "description" => { + meta_description = Some(content); + } + "title" => { + meta_title = Some(content); + } + _ => { + zlog::warn!(logger => "Unrecognized frontmatter key: {} in {:?}", kind, pretty_path(&file, &root_dir)); + } + } + } + String::new() + }); + let meta_description = meta_description.as_ref().unwrap_or_else(|| { + zlog::warn!(logger => "No meta description found for {:?}", pretty_path(&file, &root_dir)); + &default_description + }); + let page_title = extract_title_from_page(&contents, pretty_path(&file, &root_dir)); + let meta_title = meta_title.as_ref().unwrap_or_else(|| { + zlog::debug!(logger => "No meta title found for {:?}", pretty_path(&file, &root_dir)); + &default_title + }); + let meta_title = format!("{} | {}", page_title, meta_title); + zlog::trace!(logger => "Updating {:?}", pretty_path(&file, &root_dir)); + let contents = contents.replace("#description#", meta_description); + let contents = title_regex() + .replace(&contents, |_: ®ex::Captures| { + format!("{}", meta_title) + }) + .to_string(); + // let contents = contents.replace("#title#", &meta_title); + std::fs::write(file, contents)?; + } + return Ok(()); + + fn pretty_path<'a>( + path: &'a std::path::PathBuf, + root: &'a std::path::PathBuf, + ) -> &'a std::path::Path { + path.strip_prefix(&root).unwrap_or(path) + } + fn extract_title_from_page(contents: &str, pretty_path: &std::path::Path) -> String { + let title_tag_contents = &title_regex() + .captures(contents) + .with_context(|| format!("Failed to find title in {:?}", pretty_path)) + .expect("Page has element")[1]; + + title_tag_contents + .trim() + .strip_suffix("- Zed") + .unwrap_or(title_tag_contents) + .trim() + .to_string() + } +} + +fn title_regex() -> &'static Regex { + static TITLE_REGEX: OnceLock<Regex> = OnceLock::new(); + TITLE_REGEX.get_or_init(|| Regex::new(r"<title>\s*(.*?)\s*").unwrap()) +} + +fn generate_big_table_of_actions() -> String { + let actions = &*ALL_ACTIONS; + let mut output = String::new(); + + let mut actions_sorted = actions.iter().collect::>(); + actions_sorted.sort_by_key(|a| a.name); + + // Start the definition list with custom styling for better spacing + output.push_str("
\n"); + + for action in actions_sorted.into_iter() { + // Add the humanized action name as the term with margin + output.push_str( + "
", + ); + output.push_str(&action.human_name); + output.push_str("
\n"); + + // Add the definition with keymap name and description + output.push_str("
\n"); + + // Add the description, escaping HTML if needed + if let Some(description) = action.docs { + output.push_str( + &description + .replace("&", "&") + .replace("<", "<") + .replace(">", ">"), + ); + output.push_str("
\n"); + } + output.push_str("Keymap Name: "); + output.push_str(action.name); + output.push_str("
\n"); + if !action.deprecated_aliases.is_empty() { + output.push_str("Deprecated Aliases:"); + for alias in action.deprecated_aliases.iter() { + output.push_str(""); + output.push_str(alias); + output.push_str(", "); + } + } + output.push_str("\n
\n"); + } + + // Close the definition list + output.push_str("
\n"); + + output +} diff --git a/crates/inline_completion/Cargo.toml b/crates/edit_prediction/Cargo.toml similarity index 69% rename from crates/inline_completion/Cargo.toml rename to crates/edit_prediction/Cargo.toml index 0094385e16..81c1e5dec2 100644 --- a/crates/inline_completion/Cargo.toml +++ b/crates/edit_prediction/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "inline_completion" +name = "edit_prediction" version = "0.1.0" edition.workspace = true publish.workspace = true @@ -9,12 +9,11 @@ license = "GPL-3.0-or-later" workspace = true [lib] -path = "src/inline_completion.rs" +path = "src/edit_prediction.rs" [dependencies] -anyhow.workspace = true +client.workspace = true gpui.workspace = true language.workspace = true project.workspace = true workspace-hack.workspace = true -zed_llm_client.workspace = true diff --git a/crates/edit_prediction/LICENSE-GPL b/crates/edit_prediction/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/edit_prediction/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/inline_completion/src/inline_completion.rs b/crates/edit_prediction/src/edit_prediction.rs similarity index 75% rename from crates/inline_completion/src/inline_completion.rs rename to crates/edit_prediction/src/edit_prediction.rs index 7acfea72b2..6b695af1ae 100644 --- a/crates/inline_completion/src/inline_completion.rs +++ b/crates/edit_prediction/src/edit_prediction.rs @@ -1,18 +1,13 @@ use std::ops::Range; -use std::str::FromStr as _; -use anyhow::{Context as _, Result}; -use gpui::http_client::http::{HeaderMap, HeaderValue}; +use client::EditPredictionUsage; use gpui::{App, Context, Entity, SharedString}; use language::Buffer; use project::Project; -use zed_llm_client::{ - EDIT_PREDICTIONS_USAGE_AMOUNT_HEADER_NAME, EDIT_PREDICTIONS_USAGE_LIMIT_HEADER_NAME, UsageLimit, -}; // TODO: Find a better home for `Direction`. // -// This should live in an ancestor crate of `editor` and `inline_completion`, +// This should live in an ancestor crate of `editor` and `edit_prediction`, // but at time of writing there isn't an obvious spot. #[derive(Copy, Clone, PartialEq, Eq)] pub enum Direction { @@ -21,7 +16,7 @@ pub enum Direction { } #[derive(Clone)] -pub struct InlineCompletion { +pub struct EditPrediction { /// The ID of the completion, if it has one. pub id: Option, pub edits: Vec<(Range, String)>, @@ -39,7 +34,7 @@ pub enum DataCollectionState { impl DataCollectionState { pub fn is_supported(&self) -> bool { - !matches!(self, DataCollectionState::Unsupported { .. }) + !matches!(self, DataCollectionState::Unsupported) } pub fn is_enabled(&self) -> bool { @@ -59,39 +54,6 @@ impl DataCollectionState { } } -#[derive(Debug, Clone, Copy)] -pub struct EditPredictionUsage { - pub limit: UsageLimit, - pub amount: i32, -} - -impl EditPredictionUsage { - pub fn from_headers(headers: &HeaderMap) -> Result { - let limit = headers - .get(EDIT_PREDICTIONS_USAGE_LIMIT_HEADER_NAME) - .with_context(|| { - format!("missing {EDIT_PREDICTIONS_USAGE_LIMIT_HEADER_NAME:?} header") - })?; - let limit = UsageLimit::from_str(limit.to_str()?)?; - - let amount = headers - .get(EDIT_PREDICTIONS_USAGE_AMOUNT_HEADER_NAME) - .with_context(|| { - format!("missing {EDIT_PREDICTIONS_USAGE_AMOUNT_HEADER_NAME:?} header") - })?; - let amount = amount.to_str()?.parse::()?; - - Ok(Self { limit, amount }) - } - - pub fn over_limit(&self) -> bool { - match self.limit { - UsageLimit::Limited(limit) => self.amount >= limit, - UsageLimit::Unlimited => false, - } - } -} - pub trait EditPredictionProvider: 'static + Sized { fn name() -> &'static str; fn display_name() -> &'static str; @@ -99,6 +61,10 @@ pub trait EditPredictionProvider: 'static + Sized { fn show_tab_accept_marker() -> bool { false } + fn supports_jump_to_edit() -> bool { + true + } + fn data_collection_state(&self, _cx: &App) -> DataCollectionState { DataCollectionState::Unsupported } @@ -123,9 +89,6 @@ pub trait EditPredictionProvider: 'static + Sized { debounce: bool, cx: &mut Context, ); - fn needs_terms_acceptance(&self, _cx: &App) -> bool { - false - } fn cycle( &mut self, buffer: Entity, @@ -140,10 +103,10 @@ pub trait EditPredictionProvider: 'static + Sized { buffer: &Entity, cursor_position: language::Anchor, cx: &mut Context, - ) -> Option; + ) -> Option; } -pub trait InlineCompletionProviderHandle { +pub trait EditPredictionProviderHandle { fn name(&self) -> &'static str; fn display_name(&self) -> &'static str; fn is_enabled( @@ -154,10 +117,10 @@ pub trait InlineCompletionProviderHandle { ) -> bool; fn show_completions_in_menu(&self) -> bool; fn show_tab_accept_marker(&self) -> bool; + fn supports_jump_to_edit(&self) -> bool; fn data_collection_state(&self, cx: &App) -> DataCollectionState; fn usage(&self, cx: &App) -> Option; fn toggle_data_collection(&self, cx: &mut App); - fn needs_terms_acceptance(&self, cx: &App) -> bool; fn is_refreshing(&self, cx: &App) -> bool; fn refresh( &self, @@ -181,10 +144,10 @@ pub trait InlineCompletionProviderHandle { buffer: &Entity, cursor_position: language::Anchor, cx: &mut App, - ) -> Option; + ) -> Option; } -impl InlineCompletionProviderHandle for Entity +impl EditPredictionProviderHandle for Entity where T: EditPredictionProvider, { @@ -204,6 +167,10 @@ where T::show_tab_accept_marker() } + fn supports_jump_to_edit(&self) -> bool { + T::supports_jump_to_edit() + } + fn data_collection_state(&self, cx: &App) -> DataCollectionState { self.read(cx).data_collection_state(cx) } @@ -225,10 +192,6 @@ where self.read(cx).is_enabled(buffer, cursor_position, cx) } - fn needs_terms_acceptance(&self, cx: &App) -> bool { - self.read(cx).needs_terms_acceptance(cx) - } - fn is_refreshing(&self, cx: &App) -> bool { self.read(cx).is_refreshing() } @@ -271,7 +234,7 @@ where buffer: &Entity, cursor_position: language::Anchor, cx: &mut App, - ) -> Option { + ) -> Option { self.update(cx, |this, cx| this.suggest(buffer, cursor_position, cx)) } } diff --git a/crates/inline_completion_button/Cargo.toml b/crates/edit_prediction_button/Cargo.toml similarity index 86% rename from crates/inline_completion_button/Cargo.toml rename to crates/edit_prediction_button/Cargo.toml index c2a619d500..07447280fa 100644 --- a/crates/inline_completion_button/Cargo.toml +++ b/crates/edit_prediction_button/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "inline_completion_button" +name = "edit_prediction_button" version = "0.1.0" edition.workspace = true publish.workspace = true @@ -9,21 +9,23 @@ license = "GPL-3.0-or-later" workspace = true [lib] -path = "src/inline_completion_button.rs" +path = "src/edit_prediction_button.rs" doctest = false [dependencies] anyhow.workspace = true client.workspace = true +cloud_llm_client.workspace = true copilot.workspace = true editor.workspace = true feature_flags.workspace = true fs.workspace = true gpui.workspace = true indoc.workspace = true -inline_completion.workspace = true +edit_prediction.workspace = true language.workspace = true paths.workspace = true +project.workspace = true regex.workspace = true settings.workspace = true supermaven.workspace = true @@ -32,7 +34,6 @@ ui.workspace = true workspace-hack.workspace = true workspace.workspace = true zed_actions.workspace = true -zed_llm_client.workspace = true zeta.workspace = true [dev-dependencies] diff --git a/crates/edit_prediction_button/LICENSE-GPL b/crates/edit_prediction_button/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/edit_prediction_button/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/inline_completion_button/src/inline_completion_button.rs b/crates/edit_prediction_button/src/edit_prediction_button.rs similarity index 89% rename from crates/inline_completion_button/src/inline_completion_button.rs rename to crates/edit_prediction_button/src/edit_prediction_button.rs index 4ff793cbaf..0e3fe8cb1a 100644 --- a/crates/inline_completion_button/src/inline_completion_button.rs +++ b/crates/edit_prediction_button/src/edit_prediction_button.rs @@ -1,11 +1,8 @@ use anyhow::Result; use client::{UserStore, zed_urls}; +use cloud_llm_client::UsageLimit; use copilot::{Copilot, Status}; -use editor::{ - Editor, - actions::{ShowEditPrediction, ToggleEditPrediction}, - scroll::Autoscroll, -}; +use editor::{Editor, SelectionEffects, actions::ShowEditPrediction, scroll::Autoscroll}; use feature_flags::{FeatureFlagAppExt, PredictEditsRateCompletionsFeatureFlag}; use fs::Fs; use gpui::{ @@ -18,6 +15,7 @@ use language::{ EditPredictionsMode, File, Language, language_settings::{self, AllLanguageSettings, EditPredictionProvider, all_language_settings}, }; +use project::DisableAiSettings; use regex::Regex; use settings::{Settings, SettingsStore, update_settings_file}; use std::{ @@ -34,23 +32,29 @@ use workspace::{ notifications::NotificationId, }; use zed_actions::OpenBrowser; -use zed_llm_client::UsageLimit; use zeta::RateCompletions; -actions!(edit_prediction, [ToggleMenu]); +actions!( + edit_prediction, + [ + /// Toggles the edit prediction menu. + ToggleMenu + ] +); const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot"; +const PRIVACY_DOCS: &str = "https://zed.dev/docs/ai/privacy-and-security"; struct CopilotErrorToast; -pub struct InlineCompletionButton { +pub struct EditPredictionButton { editor_subscription: Option<(Subscription, usize)>, editor_enabled: Option, editor_show_predictions: bool, editor_focus_handle: Option, language: Option>, file: Option>, - edit_prediction_provider: Option>, + edit_prediction_provider: Option>, fs: Arc, user_store: Entity, popover_menu_handle: PopoverMenuHandle, @@ -63,8 +67,13 @@ enum SupermavenButtonStatus { Initializing, } -impl Render for InlineCompletionButton { +impl Render for EditPredictionButton { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + // Return empty div if AI is disabled + if DisableAiSettings::get_global(cx).disable_ai { + return div(); + } + let all_language_settings = all_language_settings(None, cx); match all_language_settings.edit_predictions.provider { @@ -118,7 +127,7 @@ impl Render for InlineCompletionButton { }), ); } - let this = cx.entity().clone(); + let this = cx.entity(); div().child( PopoverMenu::new("copilot") @@ -159,7 +168,7 @@ impl Render for InlineCompletionButton { let account_status = agent.account_status.clone(); match account_status { AccountStatus::NeedsActivation { activate_url } => { - SupermavenButtonStatus::NeedsActivation(activate_url.clone()) + SupermavenButtonStatus::NeedsActivation(activate_url) } AccountStatus::Unknown => SupermavenButtonStatus::Initializing, AccountStatus::Ready => SupermavenButtonStatus::Ready, @@ -173,10 +182,10 @@ impl Render for InlineCompletionButton { let icon = status.to_icon(); let tooltip_text = status.to_tooltip(); let has_menu = status.has_menu(); - let this = cx.entity().clone(); + let this = cx.entity(); let fs = self.fs.clone(); - return div().child( + div().child( PopoverMenu::new("supermaven") .menu(move |window, cx| match &status { SupermavenButtonStatus::NeedsActivation(activate_url) => { @@ -187,13 +196,13 @@ impl Render for InlineCompletionButton { cx.open_url(activate_url.as_str()) }) .entry( - "Use Copilot", + "Use Zed AI", None, move |_, cx| { set_completion_provider( fs.clone(), cx, - EditPredictionProvider::Copilot, + EditPredictionProvider::Zed, ) }, ) @@ -221,7 +230,7 @@ impl Render for InlineCompletionButton { }, ) .with_handle(self.popover_menu_handle.clone()), - ); + ) } EditPredictionProvider::Zed => { @@ -233,21 +242,11 @@ impl Render for InlineCompletionButton { IconName::ZedPredictDisabled }; - let current_user_terms_accepted = - self.user_store.read(cx).current_user_has_accepted_terms(); - let has_subscription = self.user_store.read(cx).current_plan().is_some() - && self.user_store.read(cx).subscription_period().is_some(); - - if !has_subscription || !current_user_terms_accepted.unwrap_or(false) { - let signed_in = current_user_terms_accepted.is_some(); - let tooltip_meta = if signed_in { - if has_subscription { - "Read Terms of Service" - } else { - "Choose a Plan" - } + if zeta::should_show_upsell_modal() { + let tooltip_meta = if self.user_store.read(cx).current_user().is_some() { + "Choose a Plan" } else { - "Sign in to use" + "Sign In" }; return div().child( @@ -328,7 +327,7 @@ impl Render for InlineCompletionButton { }) }); - let this = cx.entity().clone(); + let this = cx.entity(); let mut popover_menu = PopoverMenu::new("zeta") .menu(move |window, cx| { @@ -340,7 +339,7 @@ impl Render for InlineCompletionButton { let is_refreshing = self .edit_prediction_provider .as_ref() - .map_or(false, |provider| provider.is_refreshing(cx)); + .is_some_and(|provider| provider.is_refreshing(cx)); if is_refreshing { popover_menu = popover_menu.trigger( @@ -362,7 +361,7 @@ impl Render for InlineCompletionButton { } } -impl InlineCompletionButton { +impl EditPredictionButton { pub fn new( fs: Arc, user_store: Entity, @@ -384,9 +383,9 @@ impl InlineCompletionButton { language: None, file: None, edit_prediction_provider: None, + user_store, popover_menu_handle, fs, - user_store, } } @@ -397,15 +396,16 @@ impl InlineCompletionButton { ) -> Entity { let fs = self.fs.clone(); ContextMenu::build(window, cx, |menu, _, _| { - menu.entry("Sign In", None, copilot::initiate_sign_in) + menu.entry("Sign In to Copilot", None, copilot::initiate_sign_in) .entry("Disable Copilot", None, { let fs = fs.clone(); move |_window, cx| hide_copilot(fs.clone(), cx) }) - .entry("Use Supermaven", None, { + .separator() + .entry("Use Zed AI", None, { let fs = fs.clone(); move |_window, cx| { - set_completion_provider(fs.clone(), cx, EditPredictionProvider::Supermaven) + set_completion_provider(fs.clone(), cx, EditPredictionProvider::Zed) } }) }) @@ -433,9 +433,13 @@ impl InlineCompletionButton { if let Some(editor_focus_handle) = self.editor_focus_handle.clone() { let entry = ContextMenuEntry::new("This Buffer") .toggleable(IconPosition::Start, self.editor_show_predictions) - .action(Box::new(ToggleEditPrediction)) + .action(Box::new(editor::actions::ToggleEditPrediction)) .handler(move |window, cx| { - editor_focus_handle.dispatch_action(&ToggleEditPrediction, window, cx); + editor_focus_handle.dispatch_action( + &editor::actions::ToggleEditPrediction, + window, + cx, + ); }); match language_state.clone() { @@ -462,7 +466,7 @@ impl InlineCompletionButton { IconPosition::Start, None, move |_, cx| { - toggle_show_inline_completions_for_language(language.clone(), fs.clone(), cx) + toggle_show_edit_predictions_for_language(language.clone(), fs.clone(), cx) }, ); } @@ -470,17 +474,25 @@ impl InlineCompletionButton { let settings = AllLanguageSettings::get_global(cx); let globally_enabled = settings.show_edit_predictions(None, cx); - menu = menu.toggleable_entry("All Files", globally_enabled, IconPosition::Start, None, { - let fs = fs.clone(); - move |_, cx| toggle_inline_completions_globally(fs.clone(), cx) - }); + let entry = ContextMenuEntry::new("All Files") + .toggleable(IconPosition::Start, globally_enabled) + .action(workspace::ToggleEditPrediction.boxed_clone()) + .handler(|window, cx| { + window.dispatch_action(workspace::ToggleEditPrediction.boxed_clone(), cx) + }); + menu = menu.item(entry); let provider = settings.edit_predictions.provider; let current_mode = settings.edit_predictions_mode(); let subtle_mode = matches!(current_mode, EditPredictionsMode::Subtle); let eager_mode = matches!(current_mode, EditPredictionsMode::Eager); - if matches!(provider, EditPredictionProvider::Zed) { + if matches!( + provider, + EditPredictionProvider::Zed + | EditPredictionProvider::Copilot + | EditPredictionProvider::Supermaven + ) { menu = menu .separator() .header("Display Modes") @@ -512,7 +524,7 @@ impl InlineCompletionButton { ); } - menu = menu.separator().header("Privacy Settings"); + menu = menu.separator().header("Privacy"); if let Some(provider) = &self.edit_prediction_provider { let data_collection = provider.data_collection_state(cx); if data_collection.is_supported() { @@ -563,13 +575,15 @@ impl InlineCompletionButton { .child( Label::new(indoc!{ "Help us improve our open dataset model by sharing data from open source repositories. \ - Zed must detect a license file in your repo for this setting to take effect." + Zed must detect a license file in your repo for this setting to take effect. \ + Files with sensitive data and secrets are excluded by default." }) ) .child( h_flex() .items_start() .pt_2() + .pr_1() .flex_1() .gap_1p5() .border_t_1() @@ -629,6 +643,13 @@ impl InlineCompletionButton { .detach_and_log_err(cx); } }), + ).item( + ContextMenuEntry::new("View Documentation") + .icon(IconName::FileGeneric) + .icon_color(Color::Muted) + .handler(move |_, cx| { + cx.open_url(PRIVACY_DOCS); + }) ); if !self.editor_enabled.unwrap_or(true) { @@ -666,6 +687,13 @@ impl InlineCompletionButton { ) -> Entity { ContextMenu::build(window, cx, |menu, window, cx| { self.build_language_settings_menu(menu, window, cx) + .separator() + .entry("Use Zed AI instead", None, { + let fs = self.fs.clone(); + move |_window, cx| { + set_completion_provider(fs.clone(), cx, EditPredictionProvider::Zed) + } + }) .separator() .link( "Go to Copilot Settings", @@ -744,44 +772,24 @@ impl InlineCompletionButton { menu = menu .custom_entry( |_window, _cx| { - h_flex() - .gap_1() - .child( - Icon::new(IconName::Warning) - .size(IconSize::Small) - .color(Color::Warning), - ) - .child( - Label::new("Your GitHub account is less than 30 days old") - .size(LabelSize::Small) - .color(Color::Warning), - ) + Label::new("Your GitHub account is less than 30 days old.") + .size(LabelSize::Small) + .color(Color::Warning) .into_any_element() }, |_window, cx| cx.open_url(&zed_urls::account_url(cx)), ) - .entry( - "You need to upgrade to Zed Pro or contact us.", - None, - |_window, cx| cx.open_url(&zed_urls::account_url(cx)), - ) + .entry("Upgrade to Zed Pro or contact us.", None, |_window, cx| { + cx.open_url(&zed_urls::account_url(cx)) + }) .separator(); } else if self.user_store.read(cx).has_overdue_invoices() { menu = menu .custom_entry( |_window, _cx| { - h_flex() - .gap_1() - .child( - Icon::new(IconName::Warning) - .size(IconSize::Small) - .color(Color::Warning), - ) - .child( - Label::new("You have an outstanding invoice") - .size(LabelSize::Small) - .color(Color::Warning), - ) + Label::new("You have an outstanding invoice") + .size(LabelSize::Small) + .color(Color::Warning) .into_any_element() }, |_window, cx| { @@ -829,13 +837,9 @@ impl InlineCompletionButton { cx.notify(); } - - pub fn toggle_menu(&mut self, window: &mut Window, cx: &mut Context) { - self.popover_menu_handle.toggle(window, cx); - } } -impl StatusItemView for InlineCompletionButton { +impl StatusItemView for EditPredictionButton { fn set_active_pane_item( &mut self, item: Option<&dyn ItemHandle>, @@ -905,7 +909,7 @@ async fn open_disabled_globs_setting_in_editor( let settings = cx.global::(); - // Ensure that we always have "inline_completions { "disabled_globs": [] }" + // Ensure that we always have "edit_predictions { "disabled_globs": [] }" let edits = settings.edits_for_update::(&text, |file| { file.edit_predictions .get_or_insert_with(Default::default) @@ -929,22 +933,20 @@ async fn open_disabled_globs_setting_in_editor( .map(|inner_match| inner_match.start()..inner_match.end()) }); if let Some(range) = range { - item.change_selections(Some(Autoscroll::newest()), window, cx, |selections| { - selections.select_ranges(vec![range]); - }); + item.change_selections( + SelectionEffects::scroll(Autoscroll::newest()), + window, + cx, + |selections| { + selections.select_ranges(vec![range]); + }, + ); } })?; anyhow::Ok(()) } -fn toggle_inline_completions_globally(fs: Arc, cx: &mut App) { - let show_edit_predictions = all_language_settings(None, cx).show_edit_predictions(None, cx); - update_settings_file::(fs, cx, move |file, _| { - file.defaults.show_edit_predictions = Some(!show_edit_predictions) - }); -} - fn set_completion_provider(fs: Arc, cx: &mut App, provider: EditPredictionProvider) { update_settings_file::(fs, cx, move |file, _| { file.features @@ -953,7 +955,7 @@ fn set_completion_provider(fs: Arc, cx: &mut App, provider: EditPredicti }); } -fn toggle_show_inline_completions_for_language( +fn toggle_show_edit_predictions_for_language( language: Arc, fs: Arc, cx: &mut App, @@ -962,6 +964,7 @@ fn toggle_show_inline_completions_for_language( all_language_settings(None, cx).show_edit_predictions(Some(&language), cx); update_settings_file::(fs, cx, move |file, _| { file.languages + .0 .entry(language.name()) .or_default() .show_edit_predictions = Some(!show_edit_predictions); diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index 4726c280f4..339f98ae8b 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -22,6 +22,7 @@ test-support = [ "theme/test-support", "util/test-support", "workspace/test-support", + "tree-sitter-c", "tree-sitter-rust", "tree-sitter-typescript", "tree-sitter-html", @@ -35,13 +36,11 @@ assets.workspace = true client.workspace = true clock.workspace = true collections.workspace = true -command_palette_hooks.workspace = true convert_case.workspace = true dap.workspace = true db.workspace = true buffer_diff.workspace = true emojis.workspace = true -feature_flags.workspace = true file_icons.workspace = true futures.workspace = true fuzzy.workspace = true @@ -49,7 +48,7 @@ fs.workspace = true git.workspace = true gpui.workspace = true indoc.workspace = true -inline_completion.workspace = true +edit_prediction.workspace = true itertools.workspace = true language.workspace = true linkify.workspace = true @@ -63,6 +62,7 @@ parking_lot.workspace = true pretty_assertions.workspace = true project.workspace = true rand.workspace = true +regex.workspace = true rpc.workspace = true schemars.workspace = true serde.workspace = true @@ -77,6 +77,7 @@ telemetry.workspace = true text.workspace = true time.workspace = true theme.workspace = true +tree-sitter-c = { workspace = true, optional = true } tree-sitter-html = { workspace = true, optional = true } tree-sitter-rust = { workspace = true, optional = true } tree-sitter-typescript = { workspace = true, optional = true } @@ -107,9 +108,12 @@ settings = { workspace = true, features = ["test-support"] } tempfile.workspace = true text = { workspace = true, features = ["test-support"] } theme = { workspace = true, features = ["test-support"] } +tree-sitter-c.workspace = true tree-sitter-html.workspace = true tree-sitter-rust.workspace = true tree-sitter-typescript.workspace = true +tree-sitter-yaml.workspace = true +tree-sitter-bash.workspace = true unindent.workspace = true util = { workspace = true, features = ["test-support"] } workspace = { workspace = true, features = ["test-support"] } diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 79ef7b2733..ce02c4d2bf 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -1,24 +1,31 @@ //! This module contains all actions supported by [`Editor`]. use super::*; -use gpui::{action_as, action_with_deprecated_aliases, actions}; +use gpui::{Action, actions}; +use project::project_settings::GoToDiagnosticSeverityFilter; use schemars::JsonSchema; use util::serde::default_true; -#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)] +/// Selects the next occurrence of the current selection. +#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] +#[action(namespace = editor)] #[serde(deny_unknown_fields)] pub struct SelectNext { #[serde(default)] pub replace_newest: bool, } -#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)] +/// Selects the previous occurrence of the current selection. +#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] +#[action(namespace = editor)] #[serde(deny_unknown_fields)] pub struct SelectPrevious { #[serde(default)] pub replace_newest: bool, } -#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)] +/// Moves the cursor to the beginning of the current line. +#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] +#[action(namespace = editor)] #[serde(deny_unknown_fields)] pub struct MoveToBeginningOfLine { #[serde(default = "default_true")] @@ -27,7 +34,9 @@ pub struct MoveToBeginningOfLine { pub stop_at_indent: bool, } -#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)] +/// Selects from the cursor to the beginning of the current line. +#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] +#[action(namespace = editor)] #[serde(deny_unknown_fields)] pub struct SelectToBeginningOfLine { #[serde(default)] @@ -36,42 +45,54 @@ pub struct SelectToBeginningOfLine { pub stop_at_indent: bool, } -#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)] +/// Deletes from the cursor to the beginning of the current line. +#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] +#[action(namespace = editor)] #[serde(deny_unknown_fields)] pub struct DeleteToBeginningOfLine { #[serde(default)] pub(super) stop_at_indent: bool, } -#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)] +/// Moves the cursor up by one page. +#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] +#[action(namespace = editor)] #[serde(deny_unknown_fields)] pub struct MovePageUp { #[serde(default)] pub(super) center_cursor: bool, } -#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)] +/// Moves the cursor down by one page. +#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] +#[action(namespace = editor)] #[serde(deny_unknown_fields)] pub struct MovePageDown { #[serde(default)] pub(super) center_cursor: bool, } -#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)] +/// Moves the cursor to the end of the current line. +#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] +#[action(namespace = editor)] #[serde(deny_unknown_fields)] pub struct MoveToEndOfLine { #[serde(default = "default_true")] pub stop_at_soft_wraps: bool, } -#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)] +/// Selects from the cursor to the end of the current line. +#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] +#[action(namespace = editor)] #[serde(deny_unknown_fields)] pub struct SelectToEndOfLine { #[serde(default)] pub(super) stop_at_soft_wraps: bool, } -#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)] +/// Toggles the display of available code actions at the cursor position. +#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] +#[action(namespace = editor)] #[serde(deny_unknown_fields)] pub struct ToggleCodeActions { // Source from which the action was deployed. @@ -87,31 +108,40 @@ pub struct ToggleCodeActions { #[derive(PartialEq, Clone, Debug)] pub enum CodeActionSource { Indicator(DisplayRow), + RunMenu(DisplayRow), QuickActionBar, } -#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)] +/// Confirms and accepts the currently selected completion suggestion. +#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] +#[action(namespace = editor)] #[serde(deny_unknown_fields)] pub struct ConfirmCompletion { #[serde(default)] pub item_ix: Option, } -#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)] +/// Composes multiple completion suggestions into a single completion. +#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] +#[action(namespace = editor)] #[serde(deny_unknown_fields)] pub struct ComposeCompletion { #[serde(default)] pub item_ix: Option, } -#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)] +/// Confirms and applies the currently selected code action. +#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] +#[action(namespace = editor)] #[serde(deny_unknown_fields)] pub struct ConfirmCodeAction { #[serde(default)] pub item_ix: Option, } -#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)] +/// Toggles comment markers for the selected lines. +#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] +#[action(namespace = editor)] #[serde(deny_unknown_fields)] pub struct ToggleComments { #[serde(default)] @@ -120,89 +150,122 @@ pub struct ToggleComments { pub ignore_indent: bool, } -#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)] +/// Moves the cursor up by a specified number of lines. +#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] +#[action(namespace = editor)] #[serde(deny_unknown_fields)] pub struct MoveUpByLines { #[serde(default)] pub(super) lines: u32, } -#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)] +/// Moves the cursor down by a specified number of lines. +#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] +#[action(namespace = editor)] #[serde(deny_unknown_fields)] pub struct MoveDownByLines { #[serde(default)] pub(super) lines: u32, } -#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)] +/// Extends selection up by a specified number of lines. +#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] +#[action(namespace = editor)] #[serde(deny_unknown_fields)] pub struct SelectUpByLines { #[serde(default)] pub(super) lines: u32, } -#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)] +/// Extends selection down by a specified number of lines. +#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] +#[action(namespace = editor)] #[serde(deny_unknown_fields)] pub struct SelectDownByLines { #[serde(default)] pub(super) lines: u32, } -#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)] +/// Expands all excerpts in the editor. +#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] +#[action(namespace = editor)] #[serde(deny_unknown_fields)] pub struct ExpandExcerpts { #[serde(default)] pub(super) lines: u32, } -#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)] +/// Expands excerpts above the current position. +#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] +#[action(namespace = editor)] #[serde(deny_unknown_fields)] pub struct ExpandExcerptsUp { #[serde(default)] pub(super) lines: u32, } -#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)] +/// Expands excerpts below the current position. +#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] +#[action(namespace = editor)] #[serde(deny_unknown_fields)] pub struct ExpandExcerptsDown { #[serde(default)] pub(super) lines: u32, } -#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)] +/// Shows code completion suggestions at the cursor position. +#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] +#[action(namespace = editor)] #[serde(deny_unknown_fields)] pub struct ShowCompletions { #[serde(default)] pub(super) trigger: Option, } -#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)] +/// Handles text input in the editor. +#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] +#[action(namespace = editor)] pub struct HandleInput(pub String); -#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)] +/// Deletes from the cursor to the end of the next word. +#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] +#[action(namespace = editor)] #[serde(deny_unknown_fields)] pub struct DeleteToNextWordEnd { #[serde(default)] pub ignore_newlines: bool, } -#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)] +/// Deletes from the cursor to the start of the previous word. +#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] +#[action(namespace = editor)] #[serde(deny_unknown_fields)] pub struct DeleteToPreviousWordStart { #[serde(default)] pub ignore_newlines: bool, } -#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)] +/// Folds all code blocks at the specified indentation level. +#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] +#[action(namespace = editor)] pub struct FoldAtLevel(pub u32); -#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)] +/// Spawns the nearest available task from the current cursor position. +#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] +#[action(namespace = editor)] #[serde(deny_unknown_fields)] pub struct SpawnNearestTask { #[serde(default)] pub reveal: task::RevealStrategy, } +#[derive(Clone, PartialEq, Action)] +#[action(no_json, no_register)] +pub struct DiffClipboardWithSelectionData { + pub clipboard_text: String, + pub editor: Entity, +} + #[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize, Default)] pub enum UuidVersion { #[default] @@ -210,254 +273,486 @@ pub enum UuidVersion { V7, } -impl_actions!( - editor, +/// Splits selection into individual lines. +#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] +#[action(namespace = editor)] +#[serde(deny_unknown_fields)] +pub struct SplitSelectionIntoLines { + /// Keep the text selected after splitting instead of collapsing to cursors. + #[serde(default)] + pub keep_selections: bool, +} + +/// Goes to the next diagnostic in the file. +#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] +#[action(namespace = editor)] +#[serde(deny_unknown_fields)] +pub struct GoToDiagnostic { + #[serde(default)] + pub severity: GoToDiagnosticSeverityFilter, +} + +/// Goes to the previous diagnostic in the file. +#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] +#[action(namespace = editor)] +#[serde(deny_unknown_fields)] +pub struct GoToPreviousDiagnostic { + #[serde(default)] + pub severity: GoToDiagnosticSeverityFilter, +} + +actions!( + debugger, [ - ComposeCompletion, - ConfirmCodeAction, - ConfirmCompletion, - DeleteToBeginningOfLine, - DeleteToNextWordEnd, - DeleteToPreviousWordStart, - ExpandExcerpts, - ExpandExcerptsDown, - ExpandExcerptsUp, - HandleInput, - MoveDownByLines, - MovePageDown, - MovePageUp, - MoveToBeginningOfLine, - MoveToEndOfLine, - MoveUpByLines, - SelectDownByLines, - SelectNext, - SelectPrevious, - SelectToBeginningOfLine, - SelectToEndOfLine, - SelectUpByLines, - SpawnNearestTask, - ShowCompletions, - ToggleCodeActions, - ToggleComments, - FoldAtLevel, + /// Runs program execution to the current cursor position. + RunToCursor, + /// Evaluates the selected text in the debugger context. + EvaluateSelectedText + ] +); + +actions!( + go_to_line, + [ + /// Toggles the go to line dialog. + #[action(name = "Toggle")] + ToggleGoToLine ] ); actions!( editor, [ + /// Accepts the full edit prediction. AcceptEditPrediction, - AcceptPartialCopilotSuggestion, + /// Accepts a partial edit prediction. + #[action(deprecated_aliases = ["editor::AcceptPartialCopilotSuggestion"])] AcceptPartialEditPrediction, + /// Adds a cursor above the current selection. AddSelectionAbove, + /// Adds a cursor below the current selection. AddSelectionBelow, + /// Applies all diff hunks in the editor. ApplyAllDiffHunks, + /// Applies the diff hunk at the current position. ApplyDiffHunk, + /// Deletes the character before the cursor. Backspace, + /// Shows git blame information for the current line. + BlameHover, + /// Cancels the current operation. Cancel, + /// Cancels the running flycheck operation. CancelFlycheck, + /// Cancels pending language server work. CancelLanguageServerWork, + /// Clears flycheck results. ClearFlycheck, + /// Confirms the rename operation. ConfirmRename, + /// Confirms completion by inserting at cursor. ConfirmCompletionInsert, + /// Confirms completion by replacing existing text. ConfirmCompletionReplace, + /// Navigates to the first item in the context menu. ContextMenuFirst, + /// Navigates to the last item in the context menu. ContextMenuLast, + /// Navigates to the next item in the context menu. ContextMenuNext, + /// Navigates to the previous item in the context menu. ContextMenuPrevious, + /// Converts indentation from tabs to spaces. + ConvertIndentationToSpaces, + /// Converts indentation from spaces to tabs. + ConvertIndentationToTabs, + /// Converts selected text to kebab-case. ConvertToKebabCase, + /// Converts selected text to lowerCamelCase. ConvertToLowerCamelCase, + /// Converts selected text to lowercase. ConvertToLowerCase, + /// Toggles the case of selected text. ConvertToOppositeCase, + /// Converts selected text to sentence case. + ConvertToSentenceCase, + /// Converts selected text to snake_case. ConvertToSnakeCase, + /// Converts selected text to Title Case. ConvertToTitleCase, + /// Converts selected text to UpperCamelCase. ConvertToUpperCamelCase, + /// Converts selected text to UPPERCASE. ConvertToUpperCase, + /// Applies ROT13 cipher to selected text. ConvertToRot13, + /// Applies ROT47 cipher to selected text. ConvertToRot47, + /// Copies selected text to the clipboard. Copy, + /// Copies selected text to the clipboard with leading/trailing whitespace trimmed. CopyAndTrim, + /// Copies the current file location to the clipboard. CopyFileLocation, + /// Copies the highlighted text as JSON. CopyHighlightJson, + /// Copies the current file name to the clipboard. CopyFileName, + /// Copies the file name without extension to the clipboard. CopyFileNameWithoutExtension, + /// Copies a permalink to the current line. CopyPermalinkToLine, + /// Cuts selected text to the clipboard. Cut, + /// Cuts from cursor to end of line. CutToEndOfLine, + /// Deletes the character after the cursor. Delete, + /// Deletes the current line. DeleteLine, + /// Deletes from cursor to end of line. DeleteToEndOfLine, + /// Deletes to the end of the next subword. DeleteToNextSubwordEnd, + /// Deletes to the start of the previous subword. DeleteToPreviousSubwordStart, + /// Diffs the text stored in the clipboard against the current selection. + DiffClipboardWithSelection, + /// Displays names of all active cursors. DisplayCursorNames, + /// Duplicates the current line below. DuplicateLineDown, + /// Duplicates the current line above. DuplicateLineUp, + /// Duplicates the current selection. DuplicateSelection, + /// Expands all diff hunks in the editor. + #[action(deprecated_aliases = ["editor::ExpandAllHunkDiffs"])] + ExpandAllDiffHunks, + /// Expands macros recursively at cursor position. ExpandMacroRecursively, + /// Finds all references to the symbol at cursor. FindAllReferences, + /// Finds the next match in the search. FindNextMatch, + /// Finds the previous match in the search. FindPreviousMatch, + /// Folds the current code block. Fold, + /// Folds all foldable regions in the editor. FoldAll, + /// Folds all function bodies in the editor. FoldFunctionBodies, + /// Folds the current code block and all its children. FoldRecursive, + /// Folds the selected ranges. FoldSelectedRanges, + /// Toggles focus back to the last active buffer. + ToggleFocus, + /// Toggles folding at the current position. ToggleFold, + /// Toggles recursive folding at the current position. ToggleFoldRecursive, + /// Toggles all folds in a buffer or all excerpts in multibuffer. + ToggleFoldAll, + /// Formats the entire document. Format, + /// Formats only the selected text. FormatSelections, + /// Goes to the declaration of the symbol at cursor. GoToDeclaration, + /// Goes to declaration in a split pane. GoToDeclarationSplit, + /// Goes to the definition of the symbol at cursor. GoToDefinition, + /// Goes to definition in a split pane. GoToDefinitionSplit, - GoToDiagnostic, + /// Goes to the next diff hunk. GoToHunk, + /// Goes to the previous diff hunk. GoToPreviousHunk, + /// Goes to the implementation of the symbol at cursor. GoToImplementation, + /// Goes to implementation in a split pane. GoToImplementationSplit, + /// Goes to the next change in the file. GoToNextChange, + /// Goes to the parent module of the current file. GoToParentModule, + /// Goes to the previous change in the file. GoToPreviousChange, - GoToPreviousDiagnostic, + /// Goes to the type definition of the symbol at cursor. GoToTypeDefinition, + /// Goes to type definition in a split pane. GoToTypeDefinitionSplit, + /// Scrolls down by half a page. HalfPageDown, + /// Scrolls up by half a page. HalfPageUp, + /// Shows hover information for the symbol at cursor. Hover, + /// Increases indentation of selected lines. Indent, + /// Inserts a UUID v4 at cursor position. InsertUuidV4, + /// Inserts a UUID v7 at cursor position. InsertUuidV7, + /// Joins the current line with the next line. JoinLines, + /// Cuts to kill ring (Emacs-style). KillRingCut, + /// Yanks from kill ring (Emacs-style). KillRingYank, + /// Moves cursor down one line. LineDown, + /// Moves cursor up one line. LineUp, + /// Moves cursor down. MoveDown, + /// Moves cursor left. MoveLeft, + /// Moves the current line down. MoveLineDown, + /// Moves the current line up. MoveLineUp, + /// Moves cursor right. MoveRight, + /// Moves cursor to the beginning of the document. MoveToBeginning, + /// Moves cursor to the enclosing bracket. MoveToEnclosingBracket, + /// Moves cursor to the end of the document. MoveToEnd, + /// Moves cursor to the end of the paragraph. MoveToEndOfParagraph, + /// Moves cursor to the end of the next subword. MoveToNextSubwordEnd, + /// Moves cursor to the end of the next word. MoveToNextWordEnd, + /// Moves cursor to the start of the previous subword. MoveToPreviousSubwordStart, + /// Moves cursor to the start of the previous word. MoveToPreviousWordStart, + /// Moves cursor to the start of the paragraph. MoveToStartOfParagraph, + /// Moves cursor to the start of the current excerpt. MoveToStartOfExcerpt, + /// Moves cursor to the start of the next excerpt. MoveToStartOfNextExcerpt, + /// Moves cursor to the end of the current excerpt. MoveToEndOfExcerpt, + /// Moves cursor to the end of the previous excerpt. MoveToEndOfPreviousExcerpt, + /// Moves cursor up. MoveUp, + /// Inserts a new line and moves cursor to it. Newline, + /// Inserts a new line above the current line. NewlineAbove, + /// Inserts a new line below the current line. NewlineBelow, + /// Navigates to the next edit prediction. NextEditPrediction, + /// Scrolls to the next screen. NextScreen, + /// Opens the context menu at cursor position. OpenContextMenu, + /// Opens excerpts from the current file. OpenExcerpts, + /// Opens excerpts in a split pane. OpenExcerptsSplit, + /// Opens the proposed changes editor. OpenProposedChangesEditor, + /// Opens documentation for the symbol at cursor. OpenDocs, + /// Opens a permalink to the current line. OpenPermalinkToLine, + /// Opens the file whose name is selected in the editor. + #[action(deprecated_aliases = ["editor::OpenFile"])] + OpenSelectedFilename, + /// Opens all selections in a multibuffer. OpenSelectionsInMultibuffer, + /// Opens the URL at cursor position. OpenUrl, + /// Organizes import statements. OrganizeImports, + /// Decreases indentation of selected lines. Outdent, + /// Automatically adjusts indentation based on context. AutoIndent, + /// Scrolls down by one page. PageDown, + /// Scrolls up by one page. PageUp, + /// Pastes from clipboard. Paste, + /// Navigates to the previous edit prediction. PreviousEditPrediction, + /// Redoes the last undone edit. Redo, + /// Redoes the last selection change. RedoSelection, + /// Renames the symbol at cursor. Rename, + /// Restarts the language server for the current file. RestartLanguageServer, + /// Reveals the current file in the system file manager. RevealInFileManager, + /// Reverses the order of selected lines. ReverseLines, - RevertFile, + /// Reloads the file from disk. ReloadFile, + /// Rewraps text to fit within the preferred line length. Rewrap, + /// Runs flycheck diagnostics. RunFlycheck, + /// Scrolls the cursor to the bottom of the viewport. ScrollCursorBottom, + /// Scrolls the cursor to the center of the viewport. ScrollCursorCenter, + /// Cycles cursor position between center, top, and bottom. ScrollCursorCenterTopBottom, + /// Scrolls the cursor to the top of the viewport. ScrollCursorTop, + /// Selects all text in the editor. SelectAll, + /// Selects all matches of the current selection. SelectAllMatches, + /// Selects to the start of the current excerpt. SelectToStartOfExcerpt, + /// Selects to the start of the next excerpt. SelectToStartOfNextExcerpt, + /// Selects to the end of the current excerpt. SelectToEndOfExcerpt, + /// Selects to the end of the previous excerpt. SelectToEndOfPreviousExcerpt, + /// Extends selection down. SelectDown, + /// Selects the enclosing symbol. SelectEnclosingSymbol, + /// Selects the next larger syntax node. SelectLargerSyntaxNode, + /// Extends selection left. SelectLeft, + /// Selects the current line. SelectLine, + /// Extends selection down by one page. SelectPageDown, + /// Extends selection up by one page. SelectPageUp, + /// Extends selection right. SelectRight, + /// Selects the next smaller syntax node. SelectSmallerSyntaxNode, + /// Selects to the beginning of the document. SelectToBeginning, + /// Selects to the end of the document. SelectToEnd, + /// Selects to the end of the paragraph. SelectToEndOfParagraph, + /// Selects to the end of the next subword. SelectToNextSubwordEnd, + /// Selects to the end of the next word. SelectToNextWordEnd, + /// Selects to the start of the previous subword. SelectToPreviousSubwordStart, + /// Selects to the start of the previous word. SelectToPreviousWordStart, + /// Selects to the start of the paragraph. SelectToStartOfParagraph, + /// Extends selection up. SelectUp, + /// Shows the system character palette. ShowCharacterPalette, + /// Shows edit prediction at cursor. ShowEditPrediction, + /// Shows signature help for the current function. ShowSignatureHelp, + /// Shows word completions. ShowWordCompletions, + /// Randomly shuffles selected lines. ShuffleLines, + /// Navigates to the next signature in the signature help popup. + SignatureHelpNext, + /// Navigates to the previous signature in the signature help popup. + SignatureHelpPrevious, + /// Sorts selected lines by length. + SortLinesByLength, + /// Sorts selected lines case-insensitively. SortLinesCaseInsensitive, + /// Sorts selected lines case-sensitively. SortLinesCaseSensitive, - SplitSelectionIntoLines, + /// Stops the language server for the current file. StopLanguageServer, + /// Switches between source and header files. SwitchSourceHeader, + /// Inserts a tab character or indents. Tab, + /// Removes a tab character or outdents. Backtab, + /// Toggles a breakpoint at the current line. ToggleBreakpoint, + /// Toggles the case of selected text. ToggleCase, + /// Disables the breakpoint at the current line. DisableBreakpoint, + /// Enables the breakpoint at the current line. EnableBreakpoint, + /// Edits the log message for a breakpoint. EditLogBreakpoint, - DebuggerRunToCursor, - DebuggerEvaluateSelectedText, + /// Toggles automatic signature help. ToggleAutoSignatureHelp, + /// Toggles inline git blame display. ToggleGitBlameInline, + /// Opens the git commit for the blame at cursor. OpenGitBlameCommit, + /// Toggles the diagnostics panel. ToggleDiagnostics, + /// Toggles indent guides display. ToggleIndentGuides, + /// Toggles inlay hints display. ToggleInlayHints, + /// Toggles inline values display. ToggleInlineValues, + /// Toggles inline diagnostics display. ToggleInlineDiagnostics, + /// Toggles edit prediction feature. ToggleEditPrediction, + /// Toggles line numbers display. ToggleLineNumbers, + /// Toggles the minimap display. ToggleMinimap, + /// Swaps the start and end of the current selection. SwapSelectionEnds, + /// Sets a mark at the current position. SetMark, + /// Toggles relative line numbers display. ToggleRelativeLineNumbers, + /// Toggles diff display for selected hunks. + #[action(deprecated_aliases = ["editor::ToggleHunkDiff"])] + ToggleSelectedDiffHunks, + /// Toggles the selection menu. ToggleSelectionMenu, + /// Toggles soft wrap mode. ToggleSoftWrap, + /// Toggles the tab bar display. ToggleTabBar, + /// Transposes characters around cursor. Transpose, + /// Undoes the last edit. Undo, + /// Undoes the last selection change. UndoSelection, + /// Unfolds all folded regions. UnfoldAll, + /// Unfolds lines at cursor. UnfoldLines, + /// Unfolds recursively at cursor. UnfoldRecursive, + /// Removes duplicate lines (case-insensitive). UniqueLinesCaseInsensitive, + /// Removes duplicate lines (case-sensitive). UniqueLinesCaseSensitive, + UnwrapSyntaxNode ] ); - -action_as!(go_to_line, ToggleGoToLine as Toggle); - -action_with_deprecated_aliases!(editor, OpenSelectedFilename, ["editor::OpenFile"]); -action_with_deprecated_aliases!(editor, ToggleSelectedDiffHunks, ["editor::ToggleHunkDiff"]); -action_with_deprecated_aliases!(editor, ExpandAllDiffHunks, ["editor::ExpandAllHunkDiffs"]); diff --git a/crates/editor/src/clangd_ext.rs b/crates/editor/src/clangd_ext.rs index b745bf8c37..c78d4c83c0 100644 --- a/crates/editor/src/clangd_ext.rs +++ b/crates/editor/src/clangd_ext.rs @@ -13,7 +13,7 @@ use crate::{Editor, SwitchSourceHeader, element::register_action}; use project::lsp_store::clangd_ext::CLANGD_SERVER_NAME; fn is_c_language(language: &Language) -> bool { - return language.name() == "C++".into() || language.name() == "C".into(); + language.name() == "C++".into() || language.name() == "C".into() } pub fn switch_source_header( @@ -29,16 +29,14 @@ pub fn switch_source_header( return; }; - let server_lookup = - find_specific_language_server_in_selection(editor, cx, is_c_language, CLANGD_SERVER_NAME); + let Some((_, _, server_to_query, buffer)) = + find_specific_language_server_in_selection(editor, cx, is_c_language, CLANGD_SERVER_NAME) + else { + return; + }; let project = project.clone(); let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client(); cx.spawn_in(window, async move |_editor, cx| { - let Some((_, _, server_to_query, buffer)) = - server_lookup.await - else { - return Ok(()); - }; let source_file = buffer.read_with(cx, |buffer, _| { buffer.file().map(|file| file.path()).map(|path| path.to_string_lossy().to_string()).unwrap_or_else(|| "Unknown".to_string()) })?; @@ -106,6 +104,6 @@ pub fn apply_related_actions(editor: &Entity, window: &mut Window, cx: & .filter_map(|buffer| buffer.read(cx).language()) .any(|language| is_c_language(language)) { - register_action(&editor, window, switch_source_header); + register_action(editor, window, switch_source_header); } } diff --git a/crates/editor/src/code_completion_tests.rs b/crates/editor/src/code_completion_tests.rs index 1550cd0c4c..a1d9f04a9c 100644 --- a/crates/editor/src/code_completion_tests.rs +++ b/crates/editor/src/code_completion_tests.rs @@ -1,2382 +1,335 @@ -use crate::{ - code_context_menus::{CompletionsMenu, SortableMatch}, - editor_settings::SnippetSortOrder, -}; -use fuzzy::StringMatch; +use crate::{code_context_menus::CompletionsMenu, editor_settings::SnippetSortOrder}; +use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::TestAppContext; +use language::CodeLabel; +use lsp::{CompletionItem, CompletionItemKind, LanguageServerId}; +use project::{Completion, CompletionSource}; +use std::sync::Arc; +use std::sync::atomic::AtomicBool; +use text::Anchor; #[gpui::test] -fn test_sort_matches_local_variable_over_global_variable(_cx: &mut TestAppContext) { - // Case 1: "foo" - let query: Option<&str> = Some("foo"); - let mut matches: Vec> = vec![ - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.2727272727272727, - positions: vec![], - string: "foo_bar_baz".to_string(), - }, - is_snippet: false, - sort_text: Some("7fffffff"), - sort_kind: 2, - sort_label: "foo_bar_baz", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.2727272727272727, - positions: vec![], - string: "foo_bar_qux".to_string(), - }, - is_snippet: false, - sort_text: Some("7ffffffe"), - sort_kind: 1, - sort_label: "foo_bar_qux", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.22499999999999998, - positions: vec![], - string: "floorf64".to_string(), - }, - is_snippet: false, - sort_text: Some("80000000"), - sort_kind: 2, - sort_label: "floorf64", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.22499999999999998, - positions: vec![], - string: "floorf32".to_string(), - }, - is_snippet: false, - sort_text: Some("80000000"), - sort_kind: 2, - sort_label: "floorf32", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.22499999999999998, - positions: vec![], - string: "floorf16".to_string(), - }, - is_snippet: false, - sort_text: Some("80000000"), - sort_kind: 2, - sort_label: "floorf16", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.2, - positions: vec![], - string: "floorf128".to_string(), - }, - is_snippet: false, - sort_text: Some("80000000"), - sort_kind: 2, - sort_label: "floorf128", - }, +async fn test_sort_kind(cx: &mut TestAppContext) { + let completions = vec![ + CompletionBuilder::function("floorf128", None, "80000000"), + CompletionBuilder::constant("foo_bar_baz", None, "80000000"), + CompletionBuilder::variable("foo_bar_qux", None, "80000000"), ]; - CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); - assert_eq!( - matches[0].string_match.string.as_str(), - "foo_bar_qux", - "Match order not expected" - ); - assert_eq!( - matches[1].string_match.string.as_str(), - "foo_bar_baz", - "Match order not expected" - ); - assert_eq!( - matches[2].string_match.string.as_str(), - "floorf16", - "Match order not expected" - ); - assert_eq!( - matches[3].string_match.string.as_str(), - "floorf32", - "Match order not expected" - ); + let matches = + filter_and_sort_matches("foo", &completions, SnippetSortOrder::default(), cx).await; - // Case 2: "foobar" - let query: Option<&str> = Some("foobar"); - let mut matches: Vec> = vec![ - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.4363636363636364, - positions: vec![], - string: "foo_bar_baz".to_string(), - }, - is_snippet: false, - sort_text: Some("7fffffff"), - sort_kind: 2, - sort_label: "foo_bar_baz", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.4363636363636364, - positions: vec![], - string: "foo_bar_qux".to_string(), - }, - is_snippet: false, - sort_text: Some("7ffffffe"), - sort_kind: 1, - sort_label: "foo_bar_qux", - }, - ]; - CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); - assert_eq!( - matches[0].string_match.string.as_str(), - "foo_bar_qux", - "Match order not expected" - ); - assert_eq!( - matches[1].string_match.string.as_str(), - "foo_bar_baz", - "Match order not expected" - ); -} - -#[gpui::test] -fn test_sort_matches_local_variable_over_global_enum(_cx: &mut TestAppContext) { - // Case 1: "ele" - let query: Option<&str> = Some("ele"); - let mut matches: Vec> = vec![ - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.2727272727272727, - positions: vec![], - string: "ElementType".to_string(), - }, - is_snippet: false, - sort_text: Some("7fffffff"), - sort_kind: 2, - sort_label: "ElementType", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.25, - positions: vec![], - string: "element_type".to_string(), - }, - is_snippet: false, - sort_text: Some("7ffffffe"), - sort_kind: 1, - sort_label: "element_type", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.16363636363636364, - positions: vec![], - string: "simd_select".to_string(), - }, - is_snippet: false, - sort_text: Some("80000000"), - sort_kind: 2, - sort_label: "simd_select", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.16, - positions: vec![], - string: "while let".to_string(), - }, - is_snippet: false, - sort_text: Some("7fffffff"), - sort_kind: 0, - sort_label: "while let", - }, - ]; - CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); - assert_eq!( - matches[0].string_match.string.as_str(), - "element_type", - "Match order not expected" - ); - assert_eq!( - matches[1].string_match.string.as_str(), - "ElementType", - "Match order not expected" - ); - - // Case 2: "eleme" - let query: Option<&str> = Some("eleme"); - let mut matches: Vec> = vec![ - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.4545454545454546, - positions: vec![], - string: "ElementType".to_string(), - }, - is_snippet: false, - sort_text: Some("7fffffff"), - sort_kind: 2, - sort_label: "ElementType", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.41666666666666663, - positions: vec![], - string: "element_type".to_string(), - }, - is_snippet: false, - sort_text: Some("7ffffffe"), - sort_kind: 1, - sort_label: "element_type", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.04714285714285713, - positions: vec![], - string: "REPLACEMENT_CHARACTER".to_string(), - }, - is_snippet: false, - sort_text: Some("80000000"), - sort_kind: 2, - sort_label: "REPLACEMENT_CHARACTER", - }, - ]; - CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); - assert_eq!( - matches[0].string_match.string.as_str(), - "element_type", - "Match order not expected" - ); - assert_eq!( - matches[1].string_match.string.as_str(), - "ElementType", - "Match order not expected" - ); - - // Case 3: "Elem" - let query: Option<&str> = Some("Elem"); - let mut matches: Vec> = vec![ - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.36363636363636365, - positions: vec![], - string: "ElementType".to_string(), - }, - is_snippet: false, - sort_text: Some("7fffffff"), - sort_kind: 2, - sort_label: "ElementType", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.0003333333333333333, - positions: vec![], - string: "element_type".to_string(), - }, - is_snippet: false, - sort_text: Some("7ffffffe"), - sort_kind: 1, - sort_label: "element_type", - }, - ]; - CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); - assert_eq!( - matches[0].string_match.string.as_str(), - "ElementType", - "Match order not expected" - ); - assert_eq!( - matches[1].string_match.string.as_str(), - "element_type", - "Match order not expected" - ); -} - -#[gpui::test] -fn test_sort_matches_for_unreachable(_cx: &mut TestAppContext) { - // Case 1: "unre" - let query: Option<&str> = Some("unre"); - let mut matches: Vec> = vec![ - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.36363636363636365, - positions: vec![], - string: "unreachable".to_string(), - }, - is_snippet: false, - sort_text: Some("80000000"), - sort_kind: 2, - sort_label: "unreachable", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.26666666666666666, - positions: vec![], - string: "unreachable!(…)".to_string(), - }, - is_snippet: true, - sort_text: Some("7fffffff"), - sort_kind: 2, - sort_label: "unreachable!(…)", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.24615384615384617, - positions: vec![], - string: "unchecked_rem".to_string(), - }, - is_snippet: false, - sort_text: Some("80000000"), - sort_kind: 2, - sort_label: "unchecked_rem", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.19047619047619047, - positions: vec![], - string: "unreachable_unchecked".to_string(), - }, - is_snippet: false, - sort_text: Some("80000000"), - sort_kind: 2, - sort_label: "unreachable_unchecked", - }, - ]; - CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); - assert_eq!( - matches[0].string_match.string.as_str(), - "unreachable!(…)", - "Match order not expected" - ); - - // Case 2: "unrea" - let query: Option<&str> = Some("unrea"); - let mut matches: Vec> = vec![ - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.4545454545454546, - positions: vec![], - string: "unreachable".to_string(), - }, - is_snippet: true, - sort_text: Some("80000000"), - sort_kind: 3, - sort_label: "unreachable", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.3333333333333333, - positions: vec![], - string: "unreachable!(…)".to_string(), - }, - is_snippet: true, - sort_text: Some("7fffffff"), - sort_kind: 3, - sort_label: "unreachable!(…)", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.23809523809523808, - positions: vec![], - string: "unreachable_unchecked".to_string(), - }, - is_snippet: true, - sort_text: Some("80000000"), - sort_kind: 3, - sort_label: "unreachable_unchecked", - }, - ]; - CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); - assert_eq!( - matches[0].string_match.string.as_str(), - "unreachable!(…)", - "Match order not expected" - ); - - // Case 3: "unreach" - let query: Option<&str> = Some("unreach"); - let mut matches: Vec> = vec![ - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.6363636363636364, - positions: vec![], - string: "unreachable".to_string(), - }, - is_snippet: false, - sort_text: Some("80000000"), - sort_kind: 2, - sort_label: "unreachable", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.4666666666666667, - positions: vec![], - string: "unreachable!(…)".to_string(), - }, - is_snippet: true, - sort_text: Some("7fffffff"), - sort_kind: 2, - sort_label: "unreachable!(…)", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.3333333333333333, - positions: vec![], - string: "unreachable_unchecked".to_string(), - }, - is_snippet: false, - sort_text: Some("80000000"), - sort_kind: 2, - sort_label: "unreachable_unchecked", - }, - ]; - CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); - assert_eq!( - matches[0].string_match.string.as_str(), - "unreachable!(…)", - "Match order not expected" - ); - - // Case 4: "unreachabl" - let query: Option<&str> = Some("unreachable"); - let mut matches: Vec> = vec![ - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.9090909090909092, - positions: vec![], - string: "unreachable".to_string(), - }, - is_snippet: false, - sort_text: Some("80000000"), - sort_kind: 3, - sort_label: "unreachable", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.6666666666666666, - positions: vec![], - string: "unreachable!(…)".to_string(), - }, - is_snippet: false, - sort_text: Some("7fffffff"), - sort_kind: 3, - sort_label: "unreachable!(…)", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.47619047619047616, - positions: vec![], - string: "unreachable_unchecked".to_string(), - }, - is_snippet: false, - sort_text: Some("80000000"), - sort_kind: 3, - sort_label: "unreachable_unchecked", - }, - ]; - CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); - assert_eq!( - matches[0].string_match.string.as_str(), - "unreachable!(…)", - "Match order not expected" - ); - - // Case 5: "unreachable" - let query: Option<&str> = Some("unreachable"); - let mut matches: Vec> = vec![ - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 1.0, - positions: vec![], - string: "unreachable".to_string(), - }, - is_snippet: false, - sort_text: Some("80000000"), - sort_kind: 2, - sort_label: "unreachable", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.7333333333333333, - positions: vec![], - string: "unreachable!(…)".to_string(), - }, - is_snippet: false, - sort_text: Some("7fffffff"), - sort_kind: 2, - sort_label: "unreachable!(…)", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.5238095238095237, - positions: vec![], - string: "unreachable_unchecked".to_string(), - }, - is_snippet: false, - sort_text: Some("80000000"), - sort_kind: 2, - sort_label: "unreachable_unchecked", - }, - ]; - CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); - assert_eq!( - matches[0].string_match.string.as_str(), - "unreachable!(…)", - "LSP should take over even when fuzzy perfect matches" - ); -} - -#[gpui::test] -fn test_sort_matches_variable_and_constants_over_function(_cx: &mut TestAppContext) { - // Case 1: "var" as variable - let query: Option<&str> = Some("var"); - let mut matches: Vec> = vec![ - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 1.0, - positions: vec![], - string: "var".to_string(), - }, - is_snippet: false, - sort_text: Some("7fffffff"), - sort_kind: 3, - sort_label: "var", // function - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 1, - score: 1.0, - positions: vec![], - string: "var".to_string(), - }, - is_snippet: false, - sort_text: Some("7fffffff"), - sort_kind: 1, - sort_label: "var", // variable - }, - ]; - CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); - assert_eq!( - matches[0].string_match.candidate_id, 1, - "Match order not expected" - ); - assert_eq!( - matches[1].string_match.candidate_id, 0, - "Match order not expected" - ); - - // Case 2: "var" as constant - let query: Option<&str> = Some("var"); - let mut matches: Vec> = vec![ - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 1.0, - positions: vec![], - string: "var".to_string(), - }, - is_snippet: false, - sort_text: Some("7fffffff"), - sort_kind: 3, - sort_label: "var", // function - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 1, - score: 1.0, - positions: vec![], - string: "var".to_string(), - }, - is_snippet: false, - sort_text: Some("7fffffff"), - sort_kind: 2, - sort_label: "var", // constant - }, - ]; - CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); - assert_eq!( - matches[0].string_match.candidate_id, 1, - "Match order not expected" - ); - assert_eq!( - matches[1].string_match.candidate_id, 0, - "Match order not expected" - ); -} - -#[gpui::test] -fn test_sort_matches_for_jsx_event_handler(_cx: &mut TestAppContext) { - // Case 1: "on" - let query: Option<&str> = Some("on"); - let mut matches: Vec> = vec![ - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.3333333333333333, - positions: vec![], - string: "onCut?".to_string(), - }, - is_snippet: false, - sort_text: Some("12"), - sort_kind: 3, - sort_label: "onCut?", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.2857142857142857, - positions: vec![], - string: "onPlay?".to_string(), - }, - is_snippet: false, - sort_text: Some("12"), - sort_kind: 3, - sort_label: "onPlay?", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.25, - positions: vec![], - string: "color?".to_string(), - }, - is_snippet: false, - sort_text: Some("12"), - sort_kind: 3, - sort_label: "color?", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.25, - positions: vec![], - string: "defaultValue?".to_string(), - }, - is_snippet: false, - sort_text: Some("12"), - sort_kind: 3, - sort_label: "defaultValue?", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.25, - positions: vec![], - string: "style?".to_string(), - }, - is_snippet: false, - sort_text: Some("12"), - sort_kind: 3, - sort_label: "style?", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.20, - positions: vec![], - string: "className?".to_string(), - }, - is_snippet: false, - sort_text: Some("12"), - sort_kind: 3, - sort_label: "className?", - }, - ]; - CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); - assert_eq!( - matches[0].string_match.string, "onCut?", - "Match order not expected" - ); - assert_eq!( - matches[1].string_match.string, "onPlay?", - "Match order not expected" - ); - - // Case 2: "ona" - let query: Option<&str> = Some("ona"); - let mut matches: Vec> = vec![ - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.375, - positions: vec![], - string: "onAbort?".to_string(), - }, - is_snippet: false, - sort_text: Some("12"), - sort_kind: 3, - sort_label: "onAbort?", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.2727272727272727, - positions: vec![], - string: "onAuxClick?".to_string(), - }, - is_snippet: false, - sort_text: Some("12"), - sort_kind: 3, - sort_label: "onAuxClick?", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.23571428571428565, - positions: vec![], - string: "onPlay?".to_string(), - }, - is_snippet: false, - sort_text: Some("12"), - sort_kind: 3, - sort_label: "onPlay?", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.23571428571428565, - positions: vec![], - string: "onLoad?".to_string(), - }, - is_snippet: false, - sort_text: Some("12"), - sort_kind: 3, - sort_label: "onLoad?", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.23571428571428565, - positions: vec![], - string: "onDrag?".to_string(), - }, - is_snippet: false, - sort_text: Some("12"), - sort_kind: 3, - sort_label: "onDrag?", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.22499999999999998, - positions: vec![], - string: "onPause?".to_string(), - }, - is_snippet: false, - sort_text: Some("12"), - sort_kind: 3, - sort_label: "onPause?", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.22499999999999998, - positions: vec![], - string: "onPaste?".to_string(), - }, - is_snippet: false, - sort_text: Some("12"), - sort_kind: 3, - sort_label: "onPaste?", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.2, - positions: vec![], - string: "onAnimationEnd?".to_string(), - }, - is_snippet: false, - sort_text: Some("12"), - sort_kind: 3, - sort_label: "onAnimationEnd?", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.2, - positions: vec![], - string: "onAbortCapture?".to_string(), - }, - is_snippet: false, - sort_text: Some("12"), - sort_kind: 3, - sort_label: "onAbortCapture?", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.1833333333333333, - positions: vec![], - string: "onChange?".to_string(), - }, - is_snippet: false, - sort_text: Some("12"), - sort_kind: 3, - sort_label: "onChange?", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.18, - positions: vec![], - string: "onWaiting?".to_string(), - }, - is_snippet: false, - sort_text: Some("12"), - sort_kind: 3, - sort_label: "onWaiting?", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.18, - positions: vec![], - string: "onCanPlay?".to_string(), - }, - is_snippet: false, - sort_text: Some("12"), - sort_kind: 3, - sort_label: "onCanPlay?", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.1764705882352941, - positions: vec![], - string: "onAnimationStart?".to_string(), - }, - is_snippet: false, - sort_text: Some("12"), - sort_kind: 3, - sort_label: "onAnimationStart?", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.16666666666666666, - positions: vec![], - string: "onAuxClickCapture?".to_string(), - }, - is_snippet: false, - sort_text: Some("12"), - sort_kind: 3, - sort_label: "onAuxClickCapture?", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.16499999999999998, - positions: vec![], - string: "onStalled?".to_string(), - }, - is_snippet: false, - sort_text: Some("12"), - sort_kind: 3, - sort_label: "onStalled?", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.16499999999999998, - positions: vec![], - string: "onPlaying?".to_string(), - }, - is_snippet: false, - sort_text: Some("12"), - sort_kind: 3, - sort_label: "onPlaying?", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.16499999999999998, - positions: vec![], - string: "onDragEnd?".to_string(), - }, - is_snippet: false, - sort_text: Some("12"), - sort_kind: 3, - sort_label: "onDragEnd?", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.15000000000000002, - positions: vec![], - string: "onInvalid?".to_string(), - }, - is_snippet: false, - sort_text: Some("12"), - sort_kind: 3, - sort_label: "onInvalid?", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.15, - positions: vec![], - string: "onDragOver?".to_string(), - }, - is_snippet: false, - sort_text: Some("12"), - sort_kind: 3, - sort_label: "onDragOver?", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.15, - positions: vec![], - string: "onDragExit?".to_string(), - }, - is_snippet: false, - sort_text: Some("12"), - sort_kind: 3, - sort_label: "onDragExit?", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.14285714285714285, - positions: vec![], - string: "onAnimationIteration?".to_string(), - }, - is_snippet: false, - sort_text: Some("12"), - sort_kind: 3, - sort_label: "onAnimationIteration?", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.13846153846153847, - positions: vec![], - string: "onRateChange?".to_string(), - }, - is_snippet: false, - sort_text: Some("12"), - sort_kind: 3, - sort_label: "onRateChange?", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.13749999999999996, - positions: vec![], - string: "onLoadStart?".to_string(), - }, - is_snippet: false, - sort_text: Some("12"), - sort_kind: 3, - sort_label: "onLoadStart?", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.13749999999999996, - positions: vec![], - string: "onDragStart?".to_string(), - }, - is_snippet: false, - sort_text: Some("12"), - sort_kind: 3, - sort_label: "onDragStart?", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.13749999999999996, - positions: vec![], - string: "onDragLeave?".to_string(), - }, - is_snippet: false, - sort_text: Some("12"), - sort_kind: 3, - sort_label: "onDragLeave?", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.13749999999999996, - positions: vec![], - string: "onDragEnter?".to_string(), - }, - is_snippet: false, - sort_text: Some("12"), - sort_kind: 3, - sort_label: "onDragEnter?", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.13636363636363635, - positions: vec![], - string: "onAnimationEndCapture?".to_string(), - }, - is_snippet: false, - sort_text: Some("12"), - sort_kind: 3, - sort_label: "onAnimationEndCapture?", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.12692307692307692, - positions: vec![], - string: "onLoadedData?".to_string(), - }, - is_snippet: false, - sort_text: Some("12"), - sort_kind: 3, - sort_label: "onLoadedData?", - }, - ]; - CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); + // variable takes precedence over constant + // constant take precedence over function assert_eq!( matches .iter() - .take(12) - .map(|m| m.string_match.string.as_str()) - .collect::>(), - vec![ - "onAbort?", - "onAuxClick?", - "onAbortCapture?", - "onAnimationEnd?", - "onAnimationStart?", - "onAuxClickCapture?", - "onAnimationIteration?", - "onAnimationEndCapture?", - "onDrag?", - "onLoad?", - "onPlay?", - "onPaste?", - ] + .map(|m| m.string.as_str()) + .collect::>(), + vec!["foo_bar_qux", "foo_bar_baz", "floorf128"] ); + + // fuzzy score should match for first two items as query is common prefix + assert_eq!(matches[0].score, matches[1].score); } #[gpui::test] -fn test_sort_matches_for_snippets(_cx: &mut TestAppContext) { - // Case 1: "prin" - let query: Option<&str> = Some("prin"); - let mut matches: Vec> = vec![ - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.2, - positions: vec![], - string: "println".to_string(), - }, - is_snippet: false, - sort_text: Some("80000000"), - sort_kind: 2, - sort_label: "println", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.2, - positions: vec![], - string: "println!(…)".to_string(), - }, - is_snippet: true, - sort_text: Some("80000000"), - sort_kind: 2, - sort_label: "println!(…)", - }, - ]; - CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top); - assert_eq!( - matches[0].string_match.string.as_str(), - "println!(…)", - "Match order not expected" - ); +async fn test_fuzzy_score(cx: &mut TestAppContext) { + // first character sensitive over sort_text and sort_kind + { + let completions = vec![ + CompletionBuilder::variable("element_type", None, "7ffffffe"), + CompletionBuilder::constant("ElementType", None, "7fffffff"), + ]; + let matches = + filter_and_sort_matches("Elem", &completions, SnippetSortOrder::default(), cx).await; + assert_eq!( + matches + .iter() + .map(|m| m.string.as_str()) + .collect::>(), + vec!["ElementType", "element_type"] + ); + assert!(matches[0].score > matches[1].score); + } + + // fuzzy takes over sort_text and sort_kind + { + let completions = vec![ + CompletionBuilder::function("onAbort?", None, "12"), + CompletionBuilder::function("onAuxClick?", None, "12"), + CompletionBuilder::variable("onPlay?", None, "12"), + CompletionBuilder::variable("onLoad?", None, "12"), + CompletionBuilder::variable("onDrag?", None, "12"), + CompletionBuilder::function("onPause?", None, "10"), + CompletionBuilder::function("onPaste?", None, "10"), + CompletionBuilder::function("onAnimationEnd?", None, "12"), + CompletionBuilder::function("onAbortCapture?", None, "12"), + CompletionBuilder::constant("onChange?", None, "12"), + CompletionBuilder::constant("onWaiting?", None, "12"), + CompletionBuilder::function("onCanPlay?", None, "12"), + ]; + let matches = + filter_and_sort_matches("ona", &completions, SnippetSortOrder::default(), cx).await; + for i in 0..4 { + assert!(matches[i].string.to_lowercase().starts_with("ona")); + } + } + + // plain fuzzy prefix match + { + let completions = vec![ + CompletionBuilder::function("set_text", None, "7fffffff"), + CompletionBuilder::function("set_placeholder_text", None, "7fffffff"), + CompletionBuilder::function("set_text_style_refinement", None, "7fffffff"), + CompletionBuilder::function("set_context_menu_options", None, "7fffffff"), + CompletionBuilder::function("select_to_next_word_end", None, "7fffffff"), + CompletionBuilder::function("select_to_next_subword_end", None, "7fffffff"), + CompletionBuilder::function("set_custom_context_menu", None, "7fffffff"), + CompletionBuilder::function("select_to_end_of_excerpt", None, "7fffffff"), + CompletionBuilder::function("select_to_start_of_excerpt", None, "7fffffff"), + CompletionBuilder::function("select_to_start_of_next_excerpt", None, "7fffffff"), + CompletionBuilder::function("select_to_end_of_previous_excerpt", None, "7fffffff"), + ]; + let matches = + filter_and_sort_matches("set_text", &completions, SnippetSortOrder::Top, cx).await; + assert_eq!(matches[0].string, "set_text"); + assert_eq!(matches[1].string, "set_text_style_refinement"); + assert_eq!(matches[2].string, "set_placeholder_text"); + } + + // fuzzy filter text over label, sort_text and sort_kind + { + // Case 1: "awa" + let completions = vec![ + CompletionBuilder::method("await", Some("await"), "7fffffff"), + CompletionBuilder::method("await.ne", Some("ne"), "80000010"), + CompletionBuilder::method("await.eq", Some("eq"), "80000010"), + CompletionBuilder::method("await.or", Some("or"), "7ffffff8"), + CompletionBuilder::method("await.zip", Some("zip"), "80000006"), + CompletionBuilder::method("await.xor", Some("xor"), "7ffffff8"), + CompletionBuilder::method("await.and", Some("and"), "80000006"), + CompletionBuilder::method("await.map", Some("map"), "80000006"), + ]; + + test_for_each_prefix("await", &completions, cx, |matches| { + // for each prefix, first item should always be one with lower sort_text + assert_eq!(matches[0].string, "await"); + }) + .await; + } } #[gpui::test] -fn test_sort_matches_for_exact_match(_cx: &mut TestAppContext) { - // Case 1: "set_text" - let query: Option<&str> = Some("set_text"); - let mut matches: Vec> = vec![ - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 1.0, - positions: vec![], - string: "set_text".to_string(), - }, - is_snippet: false, - sort_text: Some("7fffffff"), - sort_kind: 3, - sort_label: "set_text", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.32000000000000006, - positions: vec![], - string: "set_placeholder_text".to_string(), - }, - is_snippet: false, - sort_text: Some("7fffffff"), - sort_kind: 3, - sort_label: "set_placeholder_text", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.32, - positions: vec![], - string: "set_text_style_refinement".to_string(), - }, - is_snippet: false, - sort_text: Some("7fffffff"), - sort_kind: 3, - sort_label: "set_text_style_refinement", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.16666666666666666, - positions: vec![], - string: "set_context_menu_options".to_string(), - }, - is_snippet: false, - sort_text: Some("7fffffff"), - sort_kind: 3, - sort_label: "set_context_menu_options", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.08695652173913043, - positions: vec![], - string: "select_to_next_word_end".to_string(), - }, - is_snippet: false, - sort_text: Some("7fffffff"), - sort_kind: 3, - sort_label: "select_to_next_word_end", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.07692307692307693, - positions: vec![], - string: "select_to_next_subword_end".to_string(), - }, - is_snippet: false, - sort_text: Some("7fffffff"), - sort_kind: 3, - sort_label: "select_to_next_subword_end", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.06956521739130435, - positions: vec![], - string: "set_custom_context_menu".to_string(), - }, - is_snippet: false, - sort_text: Some("7fffffff"), - sort_kind: 3, - sort_label: "set_custom_context_menu", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.06, - positions: vec![], - string: "select_to_end_of_excerpt".to_string(), - }, - is_snippet: false, - sort_text: Some("7fffffff"), - sort_kind: 3, - sort_label: "select_to_end_of_excerpt", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.055384615384615386, - positions: vec![], - string: "select_to_start_of_excerpt".to_string(), - }, - is_snippet: false, - sort_text: Some("7fffffff"), - sort_kind: 3, - sort_label: "select_to_start_of_excerpt", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.0464516129032258, - positions: vec![], - string: "select_to_start_of_next_excerpt".to_string(), - }, - is_snippet: false, - sort_text: Some("7fffffff"), - sort_kind: 3, - sort_label: "select_to_start_of_next_excerpt", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.04363636363636363, - positions: vec![], - string: "select_to_end_of_previous_excerpt".to_string(), - }, - is_snippet: false, - sort_text: Some("7fffffff"), - sort_kind: 3, - sort_label: "select_to_end_of_previous_excerpt", - }, - ]; - CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top); - assert_eq!( - matches - .iter() - .map(|m| m.string_match.string.as_str()) - .collect::>(), - vec![ - "set_text", - "set_text_style_refinement", - "set_placeholder_text", - "set_context_menu_options", - "set_custom_context_menu", - "select_to_next_word_end", - "select_to_next_subword_end", - "select_to_end_of_excerpt", - "select_to_start_of_excerpt", - "select_to_start_of_next_excerpt", - "select_to_end_of_previous_excerpt", - ] - ); +async fn test_sort_text(cx: &mut TestAppContext) { + // sort text takes precedance over sort_kind, when fuzzy is same + { + let completions = vec![ + CompletionBuilder::variable("unreachable", None, "80000000"), + CompletionBuilder::function("unreachable!(…)", None, "7fffffff"), + CompletionBuilder::function("unchecked_rem", None, "80000010"), + CompletionBuilder::function("unreachable_unchecked", None, "80000020"), + ]; + + test_for_each_prefix("unreachabl", &completions, cx, |matches| { + // for each prefix, first item should always be one with lower sort_text + assert_eq!(matches[0].string, "unreachable!(…)"); + assert_eq!(matches[1].string, "unreachable"); + + // fuzzy score should match for first two items as query is common prefix + assert_eq!(matches[0].score, matches[1].score); + }) + .await; + + let matches = + filter_and_sort_matches("unreachable", &completions, SnippetSortOrder::Top, cx).await; + // exact match comes first + assert_eq!(matches[0].string, "unreachable"); + assert_eq!(matches[1].string, "unreachable!(…)"); + + // fuzzy score should match for first two items as query is common prefix + assert_eq!(matches[0].score, matches[1].score); + } } #[gpui::test] -fn test_sort_matches_for_prefix_matches(_cx: &mut TestAppContext) { - // Case 1: "set" - let query: Option<&str> = Some("set"); - let mut matches: Vec> = vec![ - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.12631578947368421, - positions: vec![], - string: "select_to_beginning".to_string(), - }, - is_snippet: false, - sort_text: Some("7fffffff"), - sort_kind: 3, - sort_label: "select_to_beginning", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.15000000000000002, - positions: vec![], - string: "set_collapse_matches".to_string(), - }, - is_snippet: false, - sort_text: Some("7fffffff"), - sort_kind: 3, - sort_label: "set_collapse_matches", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.21428571428571427, - positions: vec![], - string: "set_autoindent".to_string(), - }, - is_snippet: false, - sort_text: Some("7fffffff"), - sort_kind: 3, - sort_label: "set_autoindent", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.11538461538461539, - positions: vec![], - string: "set_all_diagnostics_active".to_string(), - }, - is_snippet: false, - sort_text: Some("7fffffff"), - sort_kind: 3, - sort_label: "set_all_diagnostics_active", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.1142857142857143, - positions: vec![], - string: "select_to_end_of_line".to_string(), - }, - is_snippet: false, - sort_text: Some("7fffffff"), - sort_kind: 3, - sort_label: "select_to_end_of_line", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.15000000000000002, - positions: vec![], - string: "select_all".to_string(), - }, - is_snippet: false, - sort_text: Some("7fffffff"), - sort_kind: 3, - sort_label: "select_all", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.13636363636363635, - positions: vec![], - string: "select_line".to_string(), - }, - is_snippet: false, - sort_text: Some("7fffffff"), - sort_kind: 3, - sort_label: "select_line", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.13636363636363635, - positions: vec![], - string: "select_left".to_string(), - }, - is_snippet: false, - sort_text: Some("7fffffff"), - sort_kind: 3, - sort_label: "select_left", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.13636363636363635, - positions: vec![], - string: "select_down".to_string(), - }, - is_snippet: false, - sort_text: Some("7fffffff"), - sort_kind: 3, - sort_label: "select_down", - }, +async fn test_sort_snippet(cx: &mut TestAppContext) { + let completions = vec![ + CompletionBuilder::constant("println", None, "7fffffff"), + CompletionBuilder::snippet("println!(…)", None, "80000000"), ]; - CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top); - assert_eq!( - matches - .iter() - .map(|m| m.string_match.string.as_str()) - .collect::>(), - vec![ - "set_autoindent", - "set_collapse_matches", - "set_all_diagnostics_active", - "select_all", - "select_down", - "select_left", - "select_line", - "select_to_beginning", - "select_to_end_of_line", - ] - ); + let matches = filter_and_sort_matches("prin", &completions, SnippetSortOrder::Top, cx).await; + + // snippet take precedence over sort_text and sort_kind + assert_eq!(matches[0].string, "println!(…)"); } #[gpui::test] -fn test_sort_matches_for_await(_cx: &mut TestAppContext) { - // Case 1: "awa" - let query: Option<&str> = Some("awa"); - let mut matches: Vec> = vec![ - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.6000000000000001, - positions: vec![], - string: "await".to_string(), - }, - is_snippet: false, - sort_text: Some("7fffffff"), - sort_kind: 0, - sort_label: "await", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 35, - score: 0.375, - positions: vec![], - string: "await.ne".to_string(), - }, - is_snippet: false, - sort_text: Some("80000010"), - sort_kind: 3, - sort_label: "await.ne", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 34, - score: 0.375, - positions: vec![], - string: "await.eq".to_string(), - }, - is_snippet: false, - sort_text: Some("80000010"), - sort_kind: 3, - sort_label: "await.eq", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 18, - score: 0.375, - positions: vec![], - string: "await.or".to_string(), - }, - is_snippet: false, - sort_text: Some("7ffffff8"), - sort_kind: 3, - sort_label: "await.or", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 21, - score: 0.3333333333333333, - positions: vec![], - string: "await.zip".to_string(), - }, - is_snippet: false, - sort_text: Some("80000006"), - sort_kind: 3, - sort_label: "await.zip", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 20, - score: 0.3333333333333333, - positions: vec![], - string: "await.xor".to_string(), - }, - is_snippet: false, - sort_text: Some("7ffffff8"), - sort_kind: 3, - sort_label: "await.xor", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 15, - score: 0.3333333333333333, - positions: vec![], - string: "await.and".to_string(), - }, - is_snippet: false, - sort_text: Some("80000006"), - sort_kind: 3, - sort_label: "await.and", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 9, - score: 0.3333333333333333, - positions: vec![], - string: "await.map".to_string(), - }, - is_snippet: false, - sort_text: Some("80000006"), - sort_kind: 3, - sort_label: "await.map", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 47, - score: 0.30000000000000004, - positions: vec![], - string: "await.take".to_string(), - }, - is_snippet: false, - sort_text: Some("7ffffff8"), - sort_kind: 3, - sort_label: "await.take", - }, +async fn test_sort_exact(cx: &mut TestAppContext) { + // sort_text takes over if no exact match + let completions = vec![ + CompletionBuilder::function("into", None, "80000004"), + CompletionBuilder::function("try_into", None, "80000004"), + CompletionBuilder::snippet("println", None, "80000004"), + CompletionBuilder::function("clone_into", None, "80000004"), + CompletionBuilder::function("into_searcher", None, "80000000"), + CompletionBuilder::snippet("eprintln", None, "80000004"), ]; - CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top); - assert_eq!( - matches - .iter() - .map(|m| m.string_match.string.as_str()) - .collect::>(), - vec![ - "await", - "await.or", - "await.eq", - "await.ne", - "await.xor", - "await.take", - "await.and", - "await.map", - "await.zip" - ] - ); - // Case 2: "await" - let query: Option<&str> = Some("await"); - let mut matches: Vec> = vec![ - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 1.0, - positions: vec![], - string: "await".to_string(), - }, - is_snippet: false, - sort_text: Some("7fffffff"), - sort_kind: 0, - sort_label: "await", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 35, - score: 0.625, - positions: vec![], - string: "await.ne".to_string(), - }, - is_snippet: false, - sort_text: Some("80000010"), - sort_kind: 3, - sort_label: "await.ne", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 34, - score: 0.625, - positions: vec![], - string: "await.eq".to_string(), - }, - is_snippet: false, - sort_text: Some("80000010"), - sort_kind: 3, - sort_label: "await.eq", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 18, - score: 0.625, - positions: vec![], - string: "await.or".to_string(), - }, - is_snippet: false, - sort_text: Some("7ffffff8"), - sort_kind: 3, - sort_label: "await.or", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 21, - score: 0.5555555555555556, - positions: vec![], - string: "await.zip".to_string(), - }, - is_snippet: false, - sort_text: Some("80000006"), - sort_kind: 3, - sort_label: "await.zip", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 20, - score: 0.5555555555555556, - positions: vec![], - string: "await.xor".to_string(), - }, - is_snippet: false, - sort_text: Some("7ffffff8"), - sort_kind: 3, - sort_label: "await.xor", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 15, - score: 0.5555555555555556, - positions: vec![], - string: "await.and".to_string(), - }, - is_snippet: false, - sort_text: Some("80000006"), - sort_kind: 3, - sort_label: "await.and", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 9, - score: 0.5555555555555556, - positions: vec![], - string: "await.map".to_string(), - }, - is_snippet: false, - sort_text: Some("80000006"), - sort_kind: 3, - sort_label: "await.map", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 47, - score: 0.5, - positions: vec![], - string: "await.take".to_string(), - }, - is_snippet: false, - sort_text: Some("7ffffff8"), - sort_kind: 3, - sort_label: "await.take", - }, + let matches = + filter_and_sort_matches("int", &completions, SnippetSortOrder::default(), cx).await; + assert_eq!(matches[0].string, "into_searcher"); + + // exact match takes over sort_text + let completions = vec![ + CompletionBuilder::function("into", None, "80000004"), + CompletionBuilder::function("try_into", None, "80000004"), + CompletionBuilder::function("clone_into", None, "80000004"), + CompletionBuilder::function("into_searcher", None, "80000000"), + CompletionBuilder::function("split_terminator", None, "7fffffff"), + CompletionBuilder::function("rsplit_terminator", None, "7fffffff"), ]; - CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top); - assert_eq!( - matches - .iter() - .map(|m| m.string_match.string.as_str()) - .collect::>(), - vec![ - "await", - "await.or", - "await.eq", - "await.ne", - "await.xor", - "await.take", - "await.and", - "await.map", - "await.zip" - ] - ); + let matches = + filter_and_sort_matches("into", &completions, SnippetSortOrder::default(), cx).await; + assert_eq!(matches[0].string, "into"); } #[gpui::test] -fn test_sort_matches_for_python_init(_cx: &mut TestAppContext) { - // Case 1: "__in" - let query: Option<&str> = Some("__in"); - let mut matches: Vec> = vec![ - SortableMatch { - string_match: StringMatch { - candidate_id: 211, - score: 0.5, - positions: vec![], - string: "__init__".to_string(), - }, - is_snippet: false, - sort_text: Some("05.0003.__init__"), - sort_kind: 3, - sort_label: "__init__", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.5, - positions: vec![], - string: "__init__".to_string(), - }, - is_snippet: false, - sort_text: Some("05.0003"), - sort_kind: 3, - sort_label: "__init__", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 215, - score: 0.23529411764705882, - positions: vec![], - string: "__instancecheck__".to_string(), - }, - is_snippet: false, - sort_text: Some("05.0005.__instancecheck__"), - sort_kind: 3, - sort_label: "__instancecheck__", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 213, - score: 0.23529411764705882, - positions: vec![], - string: "__init_subclass__".to_string(), - }, - is_snippet: false, - sort_text: Some("05.0004.__init_subclass__"), - sort_kind: 3, - sort_label: "__init_subclass__", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 4, - score: 0.23529411764705882, - positions: vec![], - string: "__instancecheck__".to_string(), - }, - is_snippet: false, - sort_text: Some("05.0005"), - sort_kind: 3, - sort_label: "__instancecheck__", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 2, - score: 0.23529411764705882, - positions: vec![], - string: "__init_subclass__".to_string(), - }, - is_snippet: false, - sort_text: Some("05.0004"), - sort_kind: 3, - sort_label: "__init_subclass__", - }, +async fn test_sort_positions(cx: &mut TestAppContext) { + // positions take precedence over fuzzy score and sort_text + let completions = vec![ + CompletionBuilder::function("rounded-full", None, "15788"), + CompletionBuilder::variable("rounded-t-full", None, "15846"), + CompletionBuilder::variable("rounded-b-full", None, "15731"), + CompletionBuilder::function("rounded-tr-full", None, "15866"), ]; - CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top); - assert_eq!( - matches - .iter() - .map(|m| m.string_match.string.as_str()) - .collect::>(), - vec![ - "__init__", - "__init__", - "__init_subclass__", - "__init_subclass__", - "__instancecheck__", - "__instancecheck__", - ] - ); - // Case 2: "__ini" - let query: Option<&str> = Some("__ini"); - let mut matches: Vec> = vec![ - SortableMatch { - string_match: StringMatch { - candidate_id: 9, - score: 0.625, - positions: vec![], - string: "__init__".to_string(), - }, - is_snippet: false, - sort_text: Some("05.0004.__init__"), - sort_kind: 3, - sort_label: "__init__", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.625, - positions: vec![], - string: "__init__".to_string(), - }, - is_snippet: false, - sort_text: Some("05.0004"), - sort_kind: 3, - sort_label: "__init__", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 10, - score: 0.29411764705882354, - positions: vec![], - string: "__init_subclass__".to_string(), - }, - is_snippet: false, - sort_text: Some("05.0003.__init_subclass__"), - sort_kind: 3, - sort_label: "__init_subclass__", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 1, - score: 0.29411764705882354, - positions: vec![], - string: "__init_subclass__".to_string(), - }, - is_snippet: false, - sort_text: Some("05.0003"), - sort_kind: 3, - sort_label: "__init_subclass__", - }, - ]; - CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top); - assert_eq!( - matches - .iter() - .map(|m| m.string_match.string.as_str()) - .collect::>(), - vec![ - "__init__", - "__init__", - "__init_subclass__", - "__init_subclass__", - ] - ); - // Case 3: "__init" - let query: Option<&str> = Some("__init"); - let mut matches: Vec> = vec![ - SortableMatch { - string_match: StringMatch { - candidate_id: 7, - score: 0.75, - positions: vec![], - string: "__init__".to_string(), - }, - is_snippet: false, - sort_text: Some("05.0000.__init__"), - sort_kind: 3, - sort_label: "__init__", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.75, - positions: vec![], - string: "__init__".to_string(), - }, - is_snippet: false, - sort_text: Some("05.0000"), - sort_kind: 3, - sort_label: "__init__", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 8, - score: 0.3529411764705882, - positions: vec![], - string: "__init_subclass__".to_string(), - }, - is_snippet: false, - sort_text: Some("05.0001.__init_subclass__"), - sort_kind: 3, - sort_label: "__init_subclass__", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 1, - score: 0.3529411764705882, - positions: vec![], - string: "__init_subclass__".to_string(), - }, - is_snippet: false, - sort_text: Some("05.0001"), - sort_kind: 3, - sort_label: "__init_subclass__", - }, - ]; - CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top); - assert_eq!( - matches - .iter() - .map(|m| m.string_match.string.as_str()) - .collect::>(), - vec![ - "__init__", - "__init__", - "__init_subclass__", - "__init_subclass__", - ] - ); - // Case 4: "__init_" - let query: Option<&str> = Some("__init_"); - let mut matches: Vec> = vec![ - SortableMatch { - string_match: StringMatch { - candidate_id: 4, - score: 0.875, - positions: vec![], - string: "__init__".to_string(), - }, - is_snippet: false, - sort_text: Some("11.9999.__init__"), - sort_kind: 3, - sort_label: "__init__", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.875, - positions: vec![], - string: "__init__".to_string(), - }, - is_snippet: false, - sort_text: Some("11.9999"), - sort_kind: 3, - sort_label: "__init__", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 5, - score: 0.4117647058823529, - positions: vec![], - string: "__init_subclass__".to_string(), - }, - is_snippet: false, - sort_text: Some("05.0000.__init_subclass__"), - sort_kind: 3, - sort_label: "__init_subclass__", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 1, - score: 0.4117647058823529, - positions: vec![], - string: "__init_subclass__".to_string(), - }, - is_snippet: false, - sort_text: Some("05.0000"), - sort_kind: 3, - sort_label: "__init_subclass__", - }, - ]; - CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top); - assert_eq!( - matches - .iter() - .map(|m| m.string_match.string.as_str()) - .collect::>(), - vec![ - "__init__", - "__init__", - "__init_subclass__", - "__init_subclass__", - ] - ); + + let matches = filter_and_sort_matches( + "rounded-full", + &completions, + SnippetSortOrder::default(), + cx, + ) + .await; + assert_eq!(matches[0].string, "rounded-full"); + + let matches = + filter_and_sort_matches("roundedfull", &completions, SnippetSortOrder::default(), cx).await; + assert_eq!(matches[0].string, "rounded-full"); } #[gpui::test] -fn test_sort_matches_for_rust_into(_cx: &mut TestAppContext) { - // Case 1: "int" - let query: Option<&str> = Some("int"); - let mut matches: Vec> = vec![ - SortableMatch { - string_match: StringMatch { - candidate_id: 67, - score: 0.75, - positions: vec![], - string: "into".to_string(), - }, - is_snippet: false, - sort_text: Some("80000004"), - sort_kind: 3, - sort_label: "into", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 68, - score: 0.30000000000000004, - positions: vec![], - string: "try_into".to_string(), - }, - is_snippet: false, - sort_text: Some("80000004"), - sort_kind: 3, - sort_label: "try_into", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 108, - score: 0.2571428571428571, - positions: vec![], - string: "println".to_string(), - }, - is_snippet: true, - sort_text: Some("80000004"), - sort_kind: 3, - sort_label: "println", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 73, - score: 0.24, - positions: vec![], - string: "clone_into".to_string(), - }, - is_snippet: false, - sort_text: Some("80000004"), - sort_kind: 3, - sort_label: "clone_into", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 1, - score: 0.23076923076923078, - positions: vec![], - string: "into_searcher".to_string(), - }, - is_snippet: false, - sort_text: Some("80000000"), - sort_kind: 3, - sort_label: "into_searcher", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 109, - score: 0.22499999999999998, - positions: vec![], - string: "eprintln".to_string(), - }, - is_snippet: true, - sort_text: Some("80000004"), - sort_kind: 3, - sort_label: "eprintln", - }, +async fn test_fuzzy_over_sort_positions(cx: &mut TestAppContext) { + let completions = vec![ + CompletionBuilder::variable("lsp_document_colors", None, "7fffffff"), // 0.29 fuzzy score + CompletionBuilder::function( + "language_servers_running_disk_based_diagnostics", + None, + "7fffffff", + ), // 0.168 fuzzy score + CompletionBuilder::function("code_lens", None, "7fffffff"), // 3.2 fuzzy score + CompletionBuilder::variable("lsp_code_lens", None, "7fffffff"), // 3.2 fuzzy score + CompletionBuilder::function("fetch_code_lens", None, "7fffffff"), // 3.2 fuzzy score ]; - CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); - assert_eq!( - matches[0].string_match.string.as_str(), - "into", - "Match order not expected" - ); - // Case 2: "into" - let query: Option<&str> = Some("into"); - let mut matches: Vec> = vec![ - SortableMatch { - string_match: StringMatch { - candidate_id: 65, - score: 1.0, - positions: vec![], - string: "into".to_string(), - }, - is_snippet: false, - sort_text: Some("80000004"), - sort_kind: 3, - sort_label: "into", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 66, - score: 0.4, - positions: vec![], - string: "try_into".to_string(), - }, - is_snippet: false, - sort_text: Some("80000004"), - sort_kind: 3, - sort_label: "try_into", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 71, - score: 0.32, - positions: vec![], - string: "clone_into".to_string(), - }, - is_snippet: false, - sort_text: Some("80000004"), - sort_kind: 3, - sort_label: "clone_into", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 0, - score: 0.3076923076923077, - positions: vec![], - string: "into_searcher".to_string(), - }, - is_snippet: false, - sort_text: Some("80000000"), - sort_kind: 3, - sort_label: "into_searcher", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 27, - score: 0.09, - positions: vec![], - string: "split_terminator".to_string(), - }, - is_snippet: false, - sort_text: Some("7fffffff"), - sort_kind: 3, - sort_label: "split_terminator", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 28, - score: 0.08470588235294117, - positions: vec![], - string: "rsplit_terminator".to_string(), - }, - is_snippet: false, - sort_text: Some("7fffffff"), - sort_kind: 3, - sort_label: "rsplit_terminator", - }, - ]; - CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); - assert_eq!( - matches[0].string_match.string.as_str(), - "into", - "Match order not expected" - ); + + let matches = + filter_and_sort_matches("lens", &completions, SnippetSortOrder::default(), cx).await; + + assert_eq!(matches[0].string, "code_lens"); + assert_eq!(matches[1].string, "lsp_code_lens"); + assert_eq!(matches[2].string, "fetch_code_lens"); } -#[gpui::test] -fn test_sort_matches_for_variable_over_function(_cx: &mut TestAppContext) { - // Case 1: "serial" - let query: Option<&str> = Some("serial"); - let mut matches: Vec> = vec![ - SortableMatch { - string_match: StringMatch { - candidate_id: 33, - score: 0.6666666666666666, - positions: vec![], - string: "serialize".to_string(), - }, - is_snippet: false, - sort_text: Some("80000000"), - sort_kind: 3, - sort_label: "serialize", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 32, - score: 0.6666666666666666, - positions: vec![], - string: "serialize".to_string(), - }, - is_snippet: false, - sort_text: Some("80000000"), - sort_kind: 3, - sort_label: "serialize", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 103, - score: 0.3529411764705882, - positions: vec![], - string: "serialization_key".to_string(), - }, - is_snippet: false, - sort_text: Some("7ffffffe"), - sort_kind: 1, - sort_label: "serialization_key", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 18, - score: 0.3529411764705882, - positions: vec![], - string: "serialize_version".to_string(), - }, - is_snippet: false, - sort_text: Some("80000000"), - sort_kind: 3, - sort_label: "serialize_version", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 65, - score: 0.32727272727272727, - positions: vec![], - string: "deserialize".to_string(), - }, - is_snippet: false, - sort_text: Some("80000000"), - sort_kind: 3, - sort_label: "deserialize", - }, - ]; - CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); - assert_eq!( - matches - .iter() - .map(|m| m.string_match.string.as_str()) - .collect::>(), - vec![ - "serialization_key", - "serialize", - "serialize", - "serialize_version", - "deserialize" - ] - ); +async fn test_for_each_prefix( + target: &str, + completions: &Vec, + cx: &mut TestAppContext, + mut test_fn: F, +) where + F: FnMut(Vec), +{ + for i in 1..=target.len() { + let prefix = &target[..i]; + let matches = + filter_and_sort_matches(prefix, completions, SnippetSortOrder::default(), cx).await; + test_fn(matches); + } } -#[gpui::test] -fn test_sort_matches_for_local_methods_over_library(_cx: &mut TestAppContext) { - // Case 1: "setis" - let query: Option<&str> = Some("setis"); - let mut matches: Vec> = vec![ - SortableMatch { - string_match: StringMatch { - candidate_id: 1200, - score: 0.5555555555555556, - positions: vec![], - string: "setISODay".to_string(), +struct CompletionBuilder; + +impl CompletionBuilder { + fn constant(label: &str, filter_text: Option<&str>, sort_text: &str) -> Completion { + Self::new(label, filter_text, sort_text, CompletionItemKind::CONSTANT) + } + + fn function(label: &str, filter_text: Option<&str>, sort_text: &str) -> Completion { + Self::new(label, filter_text, sort_text, CompletionItemKind::FUNCTION) + } + + fn method(label: &str, filter_text: Option<&str>, sort_text: &str) -> Completion { + Self::new(label, filter_text, sort_text, CompletionItemKind::METHOD) + } + + fn variable(label: &str, filter_text: Option<&str>, sort_text: &str) -> Completion { + Self::new(label, filter_text, sort_text, CompletionItemKind::VARIABLE) + } + + fn snippet(label: &str, filter_text: Option<&str>, sort_text: &str) -> Completion { + Self::new(label, filter_text, sort_text, CompletionItemKind::SNIPPET) + } + + fn new( + label: &str, + filter_text: Option<&str>, + sort_text: &str, + kind: CompletionItemKind, + ) -> Completion { + Completion { + replace_range: Anchor::MIN..Anchor::MAX, + new_text: label.to_string(), + label: CodeLabel::plain(label.to_string(), filter_text), + documentation: None, + source: CompletionSource::Lsp { + insert_range: None, + server_id: LanguageServerId(0), + lsp_completion: Box::new(CompletionItem { + label: label.to_string(), + kind: Some(kind), + sort_text: Some(sort_text.to_string()), + filter_text: filter_text.map(|text| text.to_string()), + ..Default::default() + }), + lsp_defaults: None, + resolved: false, }, - is_snippet: false, - sort_text: Some("16"), - sort_kind: 1, - sort_label: "setISODay", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 1216, - score: 0.5, - positions: vec![], - string: "setISOWeek".to_string(), - }, - is_snippet: false, - sort_text: Some("16"), - sort_kind: 1, - sort_label: "setISOWeek", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 1232, - score: 0.3571428571428571, - positions: vec![], - string: "setISOWeekYear".to_string(), - }, - is_snippet: false, - sort_text: Some("16"), - sort_kind: 1, - sort_label: "setISOWeekYear", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 1217, - score: 0.3571428571428571, - positions: vec![], - string: "setISOWeekYear".to_string(), - }, - is_snippet: false, - sort_text: Some("16"), - sort_kind: 3, - sort_label: "setISOWeekYear", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 53, - score: 0.3333333333333333, - positions: vec![], - string: "setIsRefreshing".to_string(), - }, - is_snippet: false, - sort_text: Some("11"), - sort_kind: 1, - sort_label: "setIsRefreshing", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 1180, - score: 0.2571428571428571, - positions: vec![], - string: "setFips".to_string(), - }, - is_snippet: false, - sort_text: Some("16"), - sort_kind: 3, - sort_label: "setFips", - }, - ]; - CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); - assert_eq!( - matches - .iter() - .map(|m| m.string_match.string.as_str()) - .collect::>(), - vec![ - "setIsRefreshing", - "setISODay", - "setISOWeek", - "setISOWeekYear", - "setISOWeekYear", - "setFips" - ] - ); + icon_path: None, + insert_text_mode: None, + confirm: None, + } + } } -#[gpui::test] -fn test_sort_matches_for_priotize_not_exact_match(_cx: &mut TestAppContext) { - // Case 1: "item" - let query: Option<&str> = Some("item"); - let mut matches: Vec> = vec![ - SortableMatch { - string_match: StringMatch { - candidate_id: 1115, - score: 1.0, - positions: vec![], - string: "Item".to_string(), - }, - is_snippet: false, - sort_text: Some("16"), - sort_kind: 3, - sort_label: "Item", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 1108, - score: 1.0, - positions: vec![], - string: "Item".to_string(), - }, - is_snippet: false, - sort_text: Some("16"), - sort_kind: 1, - sort_label: "Item", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 26, - score: 0.8, - positions: vec![], - string: "items".to_string(), - }, - is_snippet: false, - sort_text: Some("11"), - sort_kind: 1, - sort_label: "items", - }, - SortableMatch { - string_match: StringMatch { - candidate_id: 1138, - score: 0.5, - positions: vec![], - string: "ItemText".to_string(), - }, - is_snippet: false, - sort_text: Some("16"), - sort_kind: 3, - sort_label: "ItemText", - }, - ]; - CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default()); - assert_eq!( - matches - .iter() - .map(|m| m.string_match.string.as_str()) - .collect::>(), - vec!["items", "Item", "Item", "ItemText"] - ); +async fn filter_and_sort_matches( + query: &str, + completions: &Vec, + snippet_sort_order: SnippetSortOrder, + cx: &mut TestAppContext, +) -> Vec { + let candidates: Arc<[StringMatchCandidate]> = completions + .iter() + .enumerate() + .map(|(id, completion)| StringMatchCandidate::new(id, completion.label.filter_text())) + .collect(); + let cancel_flag = Arc::new(AtomicBool::new(false)); + let background_executor = cx.executor(); + let matches = fuzzy::match_strings( + &candidates, + query, + query.chars().any(|c| c.is_uppercase()), + false, + 100, + &cancel_flag, + background_executor, + ) + .await; + CompletionsMenu::sort_string_matches(matches, Some(query), snippet_sort_order, completions) } diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs index 4ec90a204e..96809d6877 100644 --- a/crates/editor/src/code_context_menus.rs +++ b/crates/editor/src/code_context_menus.rs @@ -1,9 +1,8 @@ use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ AnyElement, Entity, Focusable, FontWeight, ListSizingBehavior, ScrollStrategy, SharedString, - Size, StrikethroughStyle, StyledText, UniformListScrollHandle, div, px, uniform_list, + Size, StrikethroughStyle, StyledText, Task, UniformListScrollHandle, div, px, uniform_list, }; -use gpui::{AsyncWindowContext, WeakEntity}; use itertools::Itertools; use language::CodeLabel; use language::{Buffer, LanguageName, LanguageRegistry}; @@ -18,6 +17,7 @@ use task::TaskContext; use std::collections::VecDeque; use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; use std::{ cell::RefCell, cmp::{Reverse, min}, @@ -47,15 +47,10 @@ pub const MENU_ASIDE_MAX_WIDTH: Pixels = px(500.); // Constants for the markdown cache. The purpose of this cache is to reduce flickering due to // documentation not yet being parsed. // -// The size of the cache is set to the number of items fetched around the current selection plus one -// for the current selection and another to avoid cases where and adjacent selection exits the -// cache. The only current benefit of a larger cache would be doing less markdown parsing when the -// selection revisits items. -// -// One future benefit of a larger cache would be reducing flicker on backspace. This would require -// not recreating the menu on every change, by not re-querying the language server when -// `is_incomplete = false`. -const MARKDOWN_CACHE_MAX_SIZE: usize = MARKDOWN_CACHE_BEFORE_ITEMS + MARKDOWN_CACHE_AFTER_ITEMS + 2; +// The size of the cache is set to 16, which is roughly 3 times more than the number of items +// fetched around the current selection. This way documentation is more often ready for render when +// revisiting previous entries, such as when pressing backspace. +const MARKDOWN_CACHE_MAX_SIZE: usize = 16; const MARKDOWN_CACHE_BEFORE_ITEMS: usize = 2; const MARKDOWN_CACHE_AFTER_ITEMS: usize = 2; @@ -197,34 +192,64 @@ pub enum ContextMenuOrigin { QuickActionBar, } -#[derive(Clone)] pub struct CompletionsMenu { pub id: CompletionId, + pub source: CompletionsMenuSource, sort_completions: bool, pub initial_position: Anchor, + pub initial_query: Option>, + pub is_incomplete: bool, pub buffer: Entity, pub completions: Rc>>, - match_candidates: Rc<[StringMatchCandidate]>, - pub entries: Rc>>, + match_candidates: Arc<[StringMatchCandidate]>, + pub entries: Rc>>, pub selected_item: usize, + filter_task: Task<()>, + cancel_filter: Arc, scroll_handle: UniformListScrollHandle, resolve_completions: bool, show_completion_documentation: bool, - pub(super) ignore_completion_provider: bool, last_rendered_range: Rc>>>, - markdown_cache: Rc)>>>, + markdown_cache: Rc)>>>, language_registry: Option>, language: Option, snippet_sort_order: SnippetSortOrder, } +#[derive(Clone, Debug, PartialEq)] +enum MarkdownCacheKey { + ForCandidate { + candidate_id: usize, + }, + ForCompletionMatch { + new_text: String, + markdown_source: SharedString, + }, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum CompletionsMenuSource { + Normal, + SnippetChoices, + Words, +} + +// TODO: There should really be a wrapper around fuzzy match tasks that does this. +impl Drop for CompletionsMenu { + fn drop(&mut self) { + self.cancel_filter.store(true, Ordering::Relaxed); + } +} + impl CompletionsMenu { pub fn new( id: CompletionId, + source: CompletionsMenuSource, sort_completions: bool, show_completion_documentation: bool, - ignore_completion_provider: bool, initial_position: Anchor, + initial_query: Option>, + is_incomplete: bool, buffer: Entity, completions: Box<[Completion]>, snippet_sort_order: SnippetSortOrder, @@ -235,24 +260,28 @@ impl CompletionsMenu { let match_candidates = completions .iter() .enumerate() - .map(|(id, completion)| StringMatchCandidate::new(id, &completion.label.filter_text())) + .map(|(id, completion)| StringMatchCandidate::new(id, completion.label.filter_text())) .collect(); let completions_menu = Self { id, + source, sort_completions, initial_position, + initial_query, + is_incomplete, buffer, show_completion_documentation, - ignore_completion_provider, completions: RefCell::new(completions).into(), match_candidates, - entries: RefCell::new(Vec::new()).into(), + entries: Rc::new(RefCell::new(Box::new([]))), selected_item: 0, + filter_task: Task::ready(()), + cancel_filter: Arc::new(AtomicBool::new(false)), scroll_handle: UniformListScrollHandle::new(), resolve_completions: true, last_rendered_range: RefCell::new(None).into(), - markdown_cache: RefCell::new(VecDeque::with_capacity(MARKDOWN_CACHE_MAX_SIZE)).into(), + markdown_cache: RefCell::new(VecDeque::new()).into(), language_registry, language, snippet_sort_order, @@ -292,7 +321,7 @@ impl CompletionsMenu { let match_candidates = choices .iter() .enumerate() - .map(|(id, completion)| StringMatchCandidate::new(id, &completion)) + .map(|(id, completion)| StringMatchCandidate::new(id, completion)) .collect(); let entries = choices .iter() @@ -303,20 +332,24 @@ impl CompletionsMenu { positions: vec![], string: completion.clone(), }) - .collect::>(); + .collect(); Self { id, + source: CompletionsMenuSource::SnippetChoices, sort_completions, initial_position: selection.start, + initial_query: None, + is_incomplete: false, buffer, completions: RefCell::new(completions).into(), match_candidates, entries: RefCell::new(entries).into(), selected_item: 0, + filter_task: Task::ready(()), + cancel_filter: Arc::new(AtomicBool::new(false)), scroll_handle: UniformListScrollHandle::new(), resolve_completions: false, show_completion_documentation: false, - ignore_completion_provider: false, last_rendered_range: RefCell::new(None).into(), markdown_cache: RefCell::new(VecDeque::new()).into(), language_registry: None, @@ -390,14 +423,7 @@ impl CompletionsMenu { ) { if self.selected_item != match_index { self.selected_item = match_index; - self.scroll_handle - .scroll_to_item(self.selected_item, ScrollStrategy::Top); - self.resolve_visible_completions(provider, cx); - self.start_markdown_parse_for_nearby_entries(cx); - if let Some(provider) = provider { - self.handle_selection_changed(provider, window, cx); - } - cx.notify(); + self.handle_selection_changed(provider, window, cx); } } @@ -418,18 +444,25 @@ impl CompletionsMenu { } fn handle_selection_changed( - &self, - provider: &dyn CompletionProvider, + &mut self, + provider: Option<&dyn CompletionProvider>, window: &mut Window, - cx: &mut App, + cx: &mut Context, ) { - let entries = self.entries.borrow(); - let entry = if self.selected_item < entries.len() { - Some(&entries[self.selected_item]) - } else { - None - }; - provider.selection_changed(entry, window, cx); + self.scroll_handle + .scroll_to_item(self.selected_item, ScrollStrategy::Top); + if let Some(provider) = provider { + let entries = self.entries.borrow(); + let entry = if self.selected_item < entries.len() { + Some(&entries[self.selected_item]) + } else { + None + }; + provider.selection_changed(entry, window, cx); + } + self.resolve_visible_completions(provider, cx); + self.start_markdown_parse_for_nearby_entries(cx); + cx.notify(); } pub fn resolve_visible_completions( @@ -444,6 +477,19 @@ impl CompletionsMenu { return; }; + let entries = self.entries.borrow(); + if entries.is_empty() { + return; + } + if self.selected_item >= entries.len() { + log::error!( + "bug: completion selected_item >= entries.len(): {} >= {}", + self.selected_item, + entries.len() + ); + self.selected_item = entries.len() - 1; + } + // Attempt to resolve completions for every item that will be displayed. This matters // because single line documentation may be displayed inline with the completion. // @@ -455,7 +501,6 @@ impl CompletionsMenu { let visible_count = last_rendered_range .clone() .map_or(APPROXIMATE_VISIBLE_COUNT, |range| range.count()); - let entries = self.entries.borrow(); let entry_range = if self.selected_item == 0 { 0..min(visible_count, entries.len()) } else if self.selected_item == entries.len() - 1 { @@ -469,7 +514,7 @@ impl CompletionsMenu { // Expand the range to resolve more completions than are predicted to be visible, to reduce // jank on navigation. let entry_indices = util::expanded_and_wrapped_usize_range( - entry_range.clone(), + entry_range, RESOLVE_BEFORE_ITEMS, RESOLVE_AFTER_ITEMS, entries.len(), @@ -508,11 +553,11 @@ impl CompletionsMenu { .update(cx, |editor, cx| { // `resolve_completions` modified state affecting display. cx.notify(); - editor.with_completions_menu_matching_id( - completion_id, - || (), - |this| this.start_markdown_parse_for_nearby_entries(cx), - ); + editor.with_completions_menu_matching_id(completion_id, |menu| { + if let Some(menu) = menu { + menu.start_markdown_parse_for_nearby_entries(cx) + } + }); }) .ok(); } @@ -548,11 +593,11 @@ impl CompletionsMenu { return None; } let candidate_id = entries[index].candidate_id; - match &self.completions.borrow()[candidate_id].documentation { - Some(CompletionDocumentation::MultiLineMarkdown(source)) if !source.is_empty() => Some( - self.get_or_create_markdown(candidate_id, source.clone(), false, cx) - .1, - ), + let completions = self.completions.borrow(); + match &completions[candidate_id].documentation { + Some(CompletionDocumentation::MultiLineMarkdown(source)) if !source.is_empty() => self + .get_or_create_markdown(candidate_id, Some(source), false, &completions, cx) + .map(|(_, markdown)| markdown), Some(_) => None, _ => None, } @@ -561,38 +606,75 @@ impl CompletionsMenu { fn get_or_create_markdown( &self, candidate_id: usize, - source: SharedString, + source: Option<&SharedString>, is_render: bool, + completions: &[Completion], cx: &mut Context, - ) -> (bool, Entity) { + ) -> Option<(bool, Entity)> { let mut markdown_cache = self.markdown_cache.borrow_mut(); - if let Some((cache_index, (_, markdown))) = markdown_cache - .iter() - .find_position(|(id, _)| *id == candidate_id) - { - let markdown = if is_render && cache_index != 0 { + + let mut has_completion_match_cache_entry = false; + let mut matching_entry = markdown_cache.iter().find_position(|(key, _)| match key { + MarkdownCacheKey::ForCandidate { candidate_id: id } => *id == candidate_id, + MarkdownCacheKey::ForCompletionMatch { .. } => { + has_completion_match_cache_entry = true; + false + } + }); + + if has_completion_match_cache_entry && matching_entry.is_none() { + if let Some(source) = source { + matching_entry = markdown_cache.iter().find_position(|(key, _)| { + matches!(key, MarkdownCacheKey::ForCompletionMatch { markdown_source, .. } + if markdown_source == source) + }); + } else { + // Heuristic guess that documentation can be reused when new_text matches. This is + // to mitigate documentation flicker while typing. If this is wrong, then resolution + // should cause the correct documentation to be displayed soon. + let completion = &completions[candidate_id]; + matching_entry = markdown_cache.iter().find_position(|(key, _)| { + matches!(key, MarkdownCacheKey::ForCompletionMatch { new_text, .. } + if new_text == &completion.new_text) + }); + } + } + + if let Some((cache_index, (key, markdown))) = matching_entry { + let markdown = markdown.clone(); + + // Since the markdown source matches, the key can now be ForCandidate. + if source.is_some() && matches!(key, MarkdownCacheKey::ForCompletionMatch { .. }) { + markdown_cache[cache_index].0 = MarkdownCacheKey::ForCandidate { candidate_id }; + } + + if is_render && cache_index != 0 { // Move the current selection's cache entry to the front. markdown_cache.rotate_right(1); let cache_len = markdown_cache.len(); markdown_cache.swap(0, (cache_index + 1) % cache_len); - &markdown_cache[0].1 - } else { - markdown - }; + } let is_parsing = markdown.update(cx, |markdown, cx| { - // `reset` is called as it's possible for documentation to change due to resolve - // requests. It does nothing if `source` is unchanged. - markdown.reset(source, cx); + if let Some(source) = source { + // `reset` is called as it's possible for documentation to change due to resolve + // requests. It does nothing if `source` is unchanged. + markdown.reset(source.clone(), cx); + } markdown.is_parsing() }); - return (is_parsing, markdown.clone()); + return Some((is_parsing, markdown)); } + let Some(source) = source else { + // Can't create markdown as there is no source. + return None; + }; + if markdown_cache.len() < MARKDOWN_CACHE_MAX_SIZE { let markdown = cx.new(|cx| { Markdown::new( - source, + source.clone(), self.language_registry.clone(), self.language.clone(), cx, @@ -601,17 +683,20 @@ impl CompletionsMenu { // Handles redraw when the markdown is done parsing. The current render is for a // deferred draw, and so without this did not redraw when `markdown` notified. cx.observe(&markdown, |_, _, cx| cx.notify()).detach(); - markdown_cache.push_front((candidate_id, markdown.clone())); - (true, markdown) + markdown_cache.push_front(( + MarkdownCacheKey::ForCandidate { candidate_id }, + markdown.clone(), + )); + Some((true, markdown)) } else { debug_assert_eq!(markdown_cache.capacity(), MARKDOWN_CACHE_MAX_SIZE); // Moves the last cache entry to the start. The ring buffer is full, so this does no // copying and just shifts indexes. markdown_cache.rotate_right(1); - markdown_cache[0].0 = candidate_id; + markdown_cache[0].0 = MarkdownCacheKey::ForCandidate { candidate_id }; let markdown = &markdown_cache[0].1; - markdown.update(cx, |markdown, cx| markdown.reset(source, cx)); - (true, markdown.clone()) + markdown.update(cx, |markdown, cx| markdown.reset(source.clone(), cx)); + Some((true, markdown.clone())) } } @@ -637,10 +722,9 @@ impl CompletionsMenu { let last_rendered_range = self.last_rendered_range.clone(); let style = style.clone(); let list = uniform_list( - cx.entity().clone(), "completions", self.entries.borrow().len(), - move |_editor, range, _window, cx| { + cx.processor(move |_editor, range: Range, _window, cx| { last_rendered_range.borrow_mut().replace(range.clone()); let start_ix = range.start; let completions_guard = completions.borrow_mut(); @@ -752,7 +836,7 @@ impl CompletionsMenu { ) }) .collect() - }, + }), ) .occlude() .max_h(max_height_in_lines as f32 * window.line_height()) @@ -774,37 +858,46 @@ impl CompletionsMenu { } let mat = &self.entries.borrow()[self.selected_item]; - let multiline_docs = match self.completions.borrow_mut()[mat.candidate_id] - .documentation - .as_ref()? - { - CompletionDocumentation::MultiLinePlainText(text) => div().child(text.clone()), - CompletionDocumentation::SingleLineAndMultiLinePlainText { + let completions = self.completions.borrow_mut(); + let multiline_docs = match completions[mat.candidate_id].documentation.as_ref() { + Some(CompletionDocumentation::MultiLinePlainText(text)) => div().child(text.clone()), + Some(CompletionDocumentation::SingleLineAndMultiLinePlainText { plain_text: Some(text), .. - } => div().child(text.clone()), - CompletionDocumentation::MultiLineMarkdown(source) if !source.is_empty() => { - let (is_parsing, markdown) = - self.get_or_create_markdown(mat.candidate_id, source.clone(), true, cx); - if is_parsing { + }) => div().child(text.clone()), + Some(CompletionDocumentation::MultiLineMarkdown(source)) if !source.is_empty() => { + let Some((false, markdown)) = self.get_or_create_markdown( + mat.candidate_id, + Some(source), + true, + &completions, + cx, + ) else { return None; - } - div().child( - MarkdownElement::new(markdown, hover_markdown_style(window, cx)) - .code_block_renderer(markdown::CodeBlockRenderer::Default { - copy_button: false, - copy_button_on_hover: false, - border: false, - }) - .on_url_click(open_markdown_url), - ) + }; + Self::render_markdown(markdown, window, cx) } - CompletionDocumentation::MultiLineMarkdown(_) => return None, - CompletionDocumentation::SingleLine(_) => return None, - CompletionDocumentation::Undocumented => return None, - CompletionDocumentation::SingleLineAndMultiLinePlainText { - plain_text: None, .. - } => { + None => { + // Handle the case where documentation hasn't yet been resolved but there's a + // `new_text` match in the cache. + // + // TODO: It's inconsistent that documentation caching based on matching `new_text` + // only works for markdown. Consider generally caching the results of resolving + // completions. + let Some((false, markdown)) = + self.get_or_create_markdown(mat.candidate_id, None, true, &completions, cx) + else { + return None; + }; + Self::render_markdown(markdown, window, cx) + } + Some(CompletionDocumentation::MultiLineMarkdown(_)) => return None, + Some(CompletionDocumentation::SingleLine(_)) => return None, + Some(CompletionDocumentation::Undocumented) => return None, + Some(CompletionDocumentation::SingleLineAndMultiLinePlainText { + plain_text: None, + .. + }) => { return None; } }; @@ -824,20 +917,151 @@ impl CompletionsMenu { ) } - pub fn sort_matches( - matches: &mut Vec>, + fn render_markdown( + markdown: Entity, + window: &mut Window, + cx: &mut Context, + ) -> Div { + div().child( + MarkdownElement::new(markdown, hover_markdown_style(window, cx)) + .code_block_renderer(markdown::CodeBlockRenderer::Default { + copy_button: false, + copy_button_on_hover: false, + border: false, + }) + .on_url_click(open_markdown_url), + ) + } + + pub fn filter( + &mut self, + query: Option>, + provider: Option>, + window: &mut Window, + cx: &mut Context, + ) { + self.cancel_filter.store(true, Ordering::Relaxed); + if let Some(query) = query { + self.cancel_filter = Arc::new(AtomicBool::new(false)); + let matches = self.do_async_filtering(query, cx); + let id = self.id; + self.filter_task = cx.spawn_in(window, async move |editor, cx| { + let matches = matches.await; + editor + .update_in(cx, |editor, window, cx| { + editor.with_completions_menu_matching_id(id, |this| { + if let Some(this) = this { + this.set_filter_results(matches, provider, window, cx); + } + }); + }) + .ok(); + }); + } else { + self.filter_task = Task::ready(()); + let matches = self.unfiltered_matches(); + self.set_filter_results(matches, provider, window, cx); + } + } + + pub fn do_async_filtering( + &self, + query: Arc, + cx: &Context, + ) -> Task> { + let matches_task = cx.background_spawn({ + let query = query.clone(); + let match_candidates = self.match_candidates.clone(); + let cancel_filter = self.cancel_filter.clone(); + let background_executor = cx.background_executor().clone(); + async move { + fuzzy::match_strings( + &match_candidates, + &query, + query.chars().any(|c| c.is_uppercase()), + false, + 1000, + &cancel_filter, + background_executor, + ) + .await + } + }); + + let completions = self.completions.clone(); + let sort_completions = self.sort_completions; + let snippet_sort_order = self.snippet_sort_order; + cx.foreground_executor().spawn(async move { + let mut matches = matches_task.await; + + if sort_completions { + matches = Self::sort_string_matches( + matches, + Some(&query), + snippet_sort_order, + completions.borrow().as_ref(), + ); + } + + matches + }) + } + + /// Like `do_async_filtering` but there is no filter query, so no need to spawn tasks. + pub fn unfiltered_matches(&self) -> Vec { + let mut matches = self + .match_candidates + .iter() + .enumerate() + .map(|(candidate_id, candidate)| StringMatch { + candidate_id, + score: Default::default(), + positions: Default::default(), + string: candidate.string.clone(), + }) + .collect(); + + if self.sort_completions { + matches = Self::sort_string_matches( + matches, + None, + self.snippet_sort_order, + self.completions.borrow().as_ref(), + ); + } + + matches + } + + pub fn set_filter_results( + &mut self, + matches: Vec, + provider: Option>, + window: &mut Window, + cx: &mut Context, + ) { + *self.entries.borrow_mut() = matches.into_boxed_slice(); + self.selected_item = 0; + self.handle_selection_changed(provider.as_deref(), window, cx); + } + + pub fn sort_string_matches( + matches: Vec, query: Option<&str>, snippet_sort_order: SnippetSortOrder, - ) { + completions: &[Completion], + ) -> Vec { + let mut matches = matches; + #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] enum MatchTier<'a> { WordStartMatch { - sort_mixed_case_prefix_length: Reverse, + sort_exact: Reverse, sort_snippet: Reverse, - sort_kind: usize, - sort_fuzzy_bracket: Reverse, - sort_text: Option<&'a str>, sort_score: Reverse>, + sort_positions: Vec, + sort_text: Option<&'a str>, + sort_kind: usize, sort_label: &'a str, }, OtherMatch { @@ -845,32 +1069,50 @@ impl CompletionsMenu { }, } - // Our goal here is to intelligently sort completion suggestions. We want to - // balance the raw fuzzy match score with hints from the language server - - // In a fuzzy bracket, matches with a score of 1.0 are prioritized. - // The remaining matches are partitioned into two groups at 3/5 of the max_score. - let max_score = matches - .iter() - .map(|mat| mat.string_match.score) - .fold(0.0, f64::max); - let fuzzy_bracket_threshold = max_score * (3.0 / 5.0); - let query_start_lower = query + .as_ref() .and_then(|q| q.chars().next()) .and_then(|c| c.to_lowercase().next()); - matches.sort_unstable_by_key(|mat| { - let score = mat.string_match.score; + if snippet_sort_order == SnippetSortOrder::None { + matches.retain(|string_match| { + let completion = &completions[string_match.candidate_id]; + + let is_snippet = matches!( + &completion.source, + CompletionSource::Lsp { lsp_completion, .. } + if lsp_completion.kind == Some(CompletionItemKind::SNIPPET) + ); + + !is_snippet + }); + } + + matches.sort_unstable_by_key(|string_match| { + let completion = &completions[string_match.candidate_id]; + + let is_snippet = matches!( + &completion.source, + CompletionSource::Lsp { lsp_completion, .. } + if lsp_completion.kind == Some(CompletionItemKind::SNIPPET) + ); + + let sort_text = match &completion.source { + CompletionSource::Lsp { lsp_completion, .. } => lsp_completion.sort_text.as_deref(), + CompletionSource::Dap { sort_text } => Some(sort_text.as_str()), + _ => None, + }; + + let (sort_kind, sort_label) = completion.sort_key(); + + let score = string_match.score; let sort_score = Reverse(OrderedFloat(score)); let query_start_doesnt_match_split_words = query_start_lower .map(|query_char| { - !split_words(&mat.string_match.string).any(|word| { - word.chars() - .next() - .and_then(|c| c.to_lowercase().next()) - .map_or(false, |word_char| word_char == query_char) + !split_words(&string_match.string).any(|word| { + word.chars().next().and_then(|c| c.to_lowercase().next()) + == Some(query_char) }) }) .unwrap_or(false); @@ -878,151 +1120,63 @@ impl CompletionsMenu { if query_start_doesnt_match_split_words { MatchTier::OtherMatch { sort_score } } else { - let sort_fuzzy_bracket = Reverse(if score >= fuzzy_bracket_threshold { + let sort_snippet = match snippet_sort_order { + SnippetSortOrder::Top => Reverse(if is_snippet { 1 } else { 0 }), + SnippetSortOrder::Bottom => Reverse(if is_snippet { 0 } else { 1 }), + SnippetSortOrder::Inline => Reverse(0), + SnippetSortOrder::None => Reverse(0), + }; + let sort_positions = string_match.positions.clone(); + let sort_exact = Reverse(if Some(completion.label.filter_text()) == query { 1 } else { 0 }); - let sort_snippet = match snippet_sort_order { - SnippetSortOrder::Top => Reverse(if mat.is_snippet { 1 } else { 0 }), - SnippetSortOrder::Bottom => Reverse(if mat.is_snippet { 0 } else { 1 }), - SnippetSortOrder::Inline => Reverse(0), - }; - let sort_mixed_case_prefix_length = Reverse( - query - .map(|q| { - q.chars() - .zip(mat.string_match.string.chars()) - .enumerate() - .take_while(|(i, (q_char, match_char))| { - if *i == 0 { - // Case-sensitive comparison for first character - q_char == match_char - } else { - // Case-insensitive comparison for other characters - q_char.to_lowercase().eq(match_char.to_lowercase()) - } - }) - .count() - }) - .unwrap_or(0), - ); + MatchTier::WordStartMatch { - sort_mixed_case_prefix_length, + sort_exact, sort_snippet, - sort_kind: mat.sort_kind, - sort_fuzzy_bracket, - sort_text: mat.sort_text, sort_score, - sort_label: mat.sort_label, + sort_positions, + sort_text, + sort_kind, + sort_label, } } }); + + matches } - pub async fn filter( - &mut self, - query: Option<&str>, - provider: Option>, - editor: WeakEntity, - cx: &mut AsyncWindowContext, - ) { - let mut matches = if let Some(query) = query { - fuzzy::match_strings( - &self.match_candidates, - query, - query.chars().any(|c| c.is_uppercase()), - 100, - &Default::default(), - cx.background_executor().clone(), - ) - .await - } else { - self.match_candidates - .iter() - .enumerate() - .map(|(candidate_id, candidate)| StringMatch { - candidate_id, - score: Default::default(), - positions: Default::default(), - string: candidate.string.clone(), - }) - .collect() - }; + pub fn preserve_markdown_cache(&mut self, prev_menu: CompletionsMenu) { + self.markdown_cache = prev_menu.markdown_cache.clone(); - if self.sort_completions { - let completions = self.completions.borrow(); - - let mut sortable_items: Vec> = matches - .into_iter() - .map(|string_match| { - let completion = &completions[string_match.candidate_id]; - - let is_snippet = matches!( - &completion.source, - CompletionSource::Lsp { lsp_completion, .. } - if lsp_completion.kind == Some(CompletionItemKind::SNIPPET) - ); - - let sort_text = - if let CompletionSource::Lsp { lsp_completion, .. } = &completion.source { - lsp_completion.sort_text.as_deref() - } else { - None - }; - - let (sort_kind, sort_label) = completion.sort_key(); - - SortableMatch { - string_match, - is_snippet, - sort_text, - sort_kind, - sort_label, + // Convert ForCandidate cache keys to ForCompletionMatch keys. + let prev_completions = prev_menu.completions.borrow(); + self.markdown_cache + .borrow_mut() + .retain_mut(|(key, _markdown)| match key { + MarkdownCacheKey::ForCompletionMatch { .. } => true, + MarkdownCacheKey::ForCandidate { candidate_id } => { + if let Some(completion) = prev_completions.get(*candidate_id) { + match &completion.documentation { + Some(CompletionDocumentation::MultiLineMarkdown(source)) => { + *key = MarkdownCacheKey::ForCompletionMatch { + new_text: completion.new_text.clone(), + markdown_source: source.clone(), + }; + true + } + _ => false, + } + } else { + false } - }) - .collect(); - - Self::sort_matches(&mut sortable_items, query, self.snippet_sort_order); - - matches = sortable_items - .into_iter() - .map(|sortable| sortable.string_match) - .collect(); - } - - *self.entries.borrow_mut() = matches; - self.selected_item = 0; - // This keeps the display consistent when y_flipped. - self.scroll_handle.scroll_to_item(0, ScrollStrategy::Top); - - if let Some(provider) = provider { - cx.update(|window, cx| { - // Since this is async, it's possible the menu has been closed and possibly even - // another opened. `provider.selection_changed` should not be called in this case. - let this_menu_still_active = editor - .read_with(cx, |editor, _cx| { - editor.with_completions_menu_matching_id(self.id, || false, |_| true) - }) - .unwrap_or(false); - if this_menu_still_active { - self.handle_selection_changed(&*provider, window, cx); } - }) - .ok(); - } + }); } } -#[derive(Debug)] -pub struct SortableMatch<'a> { - pub string_match: StringMatch, - pub is_snippet: bool, - pub sort_text: Option<&'a str>, - pub sort_kind: usize, - pub sort_label: &'a str, -} - #[derive(Clone)] pub struct AvailableCodeAction { pub excerpt_id: ExcerptId, @@ -1063,7 +1217,7 @@ impl CodeActionContents { tasks_len + code_actions_len + self.debug_scenarios.len() } - fn is_empty(&self) -> bool { + pub fn is_empty(&self) -> bool { self.len() == 0 } @@ -1228,13 +1382,15 @@ impl CodeActionsMenu { } } - fn visible(&self) -> bool { + pub fn visible(&self) -> bool { !self.actions.is_empty() } fn origin(&self) -> ContextMenuOrigin { match &self.deployed_from { - Some(CodeActionSource::Indicator(row)) => ContextMenuOrigin::GutterIndicator(*row), + Some(CodeActionSource::Indicator(row)) | Some(CodeActionSource::RunMenu(row)) => { + ContextMenuOrigin::GutterIndicator(*row) + } Some(CodeActionSource::QuickActionBar) => ContextMenuOrigin::QuickActionBar, None => ContextMenuOrigin::Cursor, } @@ -1250,10 +1406,9 @@ impl CodeActionsMenu { let actions = self.actions.clone(); let selected_item = self.selected_item; let list = uniform_list( - cx.entity().clone(), "code_actions_menu", self.actions.len(), - move |_this, range, _, cx| { + cx.processor(move |_this, range: Range, _, cx| { actions .iter() .skip(range.start) @@ -1316,7 +1471,7 @@ impl CodeActionsMenu { ) }) .collect() - }, + }), ) .occlude() .max_h(max_height_in_lines as f32 * window.line_height()) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 374f9ed0ba..c16e4a6ddb 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -37,7 +37,9 @@ pub use block_map::{ use block_map::{BlockRow, BlockSnapshot}; use collections::{HashMap, HashSet}; pub use crease_map::*; -pub use fold_map::{ChunkRenderer, ChunkRendererContext, Fold, FoldId, FoldPlaceholder, FoldPoint}; +pub use fold_map::{ + ChunkRenderer, ChunkRendererContext, ChunkRendererId, Fold, FoldId, FoldPlaceholder, FoldPoint, +}; use fold_map::{FoldMap, FoldSnapshot}; use gpui::{App, Context, Entity, Font, HighlightStyle, LineLayout, Pixels, UnderlineStyle}; pub use inlay_map::Inlay; @@ -76,11 +78,17 @@ pub enum FoldStatus { Foldable, } +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum HighlightKey { + Type(TypeId), + TypePlus(TypeId, usize), +} + pub trait ToDisplayPoint { fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint; } -type TextHighlights = TreeMap>)>>; +type TextHighlights = TreeMap>)>>; type InlayHighlights = TreeMap>; /// Decides how text in a [`MultiBuffer`] should be displayed in a buffer, handling inlay hints, @@ -263,7 +271,6 @@ impl DisplayMap { height: Some(height), style, priority, - render_in_minimap: true, } }), ); @@ -473,12 +480,11 @@ impl DisplayMap { pub fn highlight_text( &mut self, - type_id: TypeId, + key: HighlightKey, ranges: Vec>, style: HighlightStyle, ) { - self.text_highlights - .insert(type_id, Arc::new((style, ranges))); + self.text_highlights.insert(key, Arc::new((style, ranges))); } pub(crate) fn highlight_inlays( @@ -501,11 +507,22 @@ impl DisplayMap { } pub fn text_highlights(&self, type_id: TypeId) -> Option<(HighlightStyle, &[Range])> { - let highlights = self.text_highlights.get(&type_id)?; + let highlights = self.text_highlights.get(&HighlightKey::Type(type_id))?; Some((highlights.0, &highlights.1)) } + + #[cfg(feature = "test-support")] + pub fn all_text_highlights( + &self, + ) -> impl Iterator>)>> { + self.text_highlights.values() + } + pub fn clear_highlights(&mut self, type_id: TypeId) -> bool { - let mut cleared = self.text_highlights.remove(&type_id).is_some(); + let mut cleared = self + .text_highlights + .remove(&HighlightKey::Type(type_id)) + .is_some(); cleared |= self.inlay_highlights.remove(&type_id).is_some(); cleared } @@ -522,7 +539,7 @@ impl DisplayMap { pub fn update_fold_widths( &mut self, - widths: impl IntoIterator, + widths: impl IntoIterator, cx: &mut Context, ) -> bool { let snapshot = self.buffer.read(cx).snapshot(cx); @@ -618,7 +635,7 @@ pub(crate) struct Highlights<'a> { } #[derive(Clone, Copy, Debug)] -pub struct InlineCompletionStyles { +pub struct EditPredictionStyles { pub insertion: HighlightStyle, pub whitespace: HighlightStyle, } @@ -626,7 +643,7 @@ pub struct InlineCompletionStyles { #[derive(Default, Debug, Clone, Copy)] pub struct HighlightStyles { pub inlay_hint: Option, - pub inline_completion: Option, + pub edit_prediction: Option, } #[derive(Clone)] @@ -639,6 +656,7 @@ pub struct HighlightedChunk<'a> { pub text: &'a str, pub style: Option, pub is_tab: bool, + pub is_inlay: bool, pub replacement: Option, } @@ -652,6 +670,7 @@ impl<'a> HighlightedChunk<'a> { let style = self.style; let is_tab = self.is_tab; let renderer = self.replacement; + let is_inlay = self.is_inlay; iter::from_fn(move || { let mut prefix_len = 0; while let Some(&ch) = chars.peek() { @@ -667,6 +686,7 @@ impl<'a> HighlightedChunk<'a> { text: prefix, style, is_tab, + is_inlay, replacement: renderer.clone(), }); } @@ -693,6 +713,7 @@ impl<'a> HighlightedChunk<'a> { text: prefix, style: Some(invisible_style), is_tab: false, + is_inlay, replacement: Some(ChunkReplacement::Str(replacement.into())), }); } else { @@ -716,6 +737,7 @@ impl<'a> HighlightedChunk<'a> { text: prefix, style: Some(invisible_style), is_tab: false, + is_inlay, replacement: renderer.clone(), }); } @@ -728,6 +750,7 @@ impl<'a> HighlightedChunk<'a> { text: remainder, style, is_tab, + is_inlay, replacement: renderer.clone(), }) } else { @@ -935,7 +958,7 @@ impl DisplaySnapshot { language_aware, HighlightStyles { inlay_hint: Some(editor_style.inlay_hints_style), - inline_completion: Some(editor_style.inline_completion_styles), + edit_prediction: Some(editor_style.edit_prediction_styles), }, ) .flat_map(|chunk| { @@ -944,10 +967,22 @@ impl DisplaySnapshot { .and_then(|id| id.style(&editor_style.syntax)); if let Some(chunk_highlight) = chunk.highlight_style { + // For color inlays, blend the color with the editor background + let mut processed_highlight = chunk_highlight; + if chunk.is_inlay + && let Some(inlay_color) = chunk_highlight.color + { + // Only blend if the color has transparency (alpha < 1.0) + if inlay_color.a < 1.0 { + let blended_color = editor_style.background.blend(inlay_color); + processed_highlight.color = Some(blended_color); + } + } + if let Some(highlight_style) = highlight_style.as_mut() { - highlight_style.highlight(chunk_highlight); + highlight_style.highlight(processed_highlight); } else { - highlight_style = Some(chunk_highlight); + highlight_style = Some(processed_highlight); } } @@ -956,12 +991,15 @@ impl DisplaySnapshot { if let Some(severity) = chunk.diagnostic_severity.filter(|severity| { self.diagnostics_max_severity .into_lsp() - .map_or(false, |max_severity| severity <= &max_severity) + .is_some_and(|max_severity| severity <= &max_severity) }) { if chunk.is_unnecessary { diagnostic_highlight.fade_out = Some(editor_style.unnecessary_code_fade); } - if chunk.underline && editor_style.show_underlines { + if chunk.underline + && editor_style.show_underlines + && !(chunk.is_unnecessary && severity > lsp::DiagnosticSeverity::WARNING) + { let diagnostic_color = super::diagnostic_style(severity, &editor_style.status); diagnostic_highlight.underline = Some(UnderlineStyle { color: Some(diagnostic_color), @@ -981,6 +1019,7 @@ impl DisplaySnapshot { text: chunk.text, style: highlight_style, is_tab: chunk.is_tab, + is_inlay: chunk.is_inlay, replacement: chunk.renderer.map(ChunkReplacement::Renderer), } .highlight_invisibles(editor_style) @@ -1026,7 +1065,7 @@ impl DisplaySnapshot { } let font_size = editor_style.text.font_size.to_pixels(*rem_size); - text_system.layout_line(&line, font_size, &runs) + text_system.layout_line(&line, font_size, &runs, None) } pub fn x_for_display_point( @@ -1323,7 +1362,9 @@ impl DisplaySnapshot { &self, ) -> Option>)>> { let type_id = TypeId::of::(); - self.text_highlights.get(&type_id).cloned() + self.text_highlights + .get(&HighlightKey::Type(type_id)) + .cloned() } #[allow(unused)] @@ -1621,7 +1662,6 @@ pub mod tests { height: Some(height), render: Arc::new(|_| div().into_any()), priority, - render_in_minimap: true, } }) .collect::>(); @@ -1987,7 +2027,6 @@ pub mod tests { style: BlockStyle::Sticky, render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }], cx, ); @@ -1997,11 +2036,11 @@ pub mod tests { map.update(cx, |map, cx| { map.splice_inlays( &[], - vec![Inlay { - id: InlayId::InlineCompletion(0), - position: buffer_snapshot.anchor_after(0), - text: "\n".into(), - }], + vec![Inlay::edit_prediction( + 0, + buffer_snapshot.anchor_after(0), + "\n", + )], cx, ); }); @@ -2185,7 +2224,6 @@ pub mod tests { style: BlockStyle::Sticky, render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }, BlockProperties { placement: BlockPlacement::Below( @@ -2195,7 +2233,6 @@ pub mod tests { style: BlockStyle::Sticky, render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }, ], cx, @@ -2284,7 +2321,7 @@ pub mod tests { // Insert a block in the middle of a multi-line diagnostic. map.update(cx, |map, cx| { map.highlight_text( - TypeId::of::(), + HighlightKey::Type(TypeId::of::()), vec![ buffer_snapshot.anchor_before(Point::new(3, 9)) ..buffer_snapshot.anchor_after(Point::new(3, 14)), @@ -2302,7 +2339,6 @@ pub mod tests { style: BlockStyle::Sticky, render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }], cx, ) @@ -2315,11 +2351,12 @@ pub mod tests { .highlight_style .and_then(|style| style.color) .map_or(black, |color| color.to_rgb()); - if let Some((last_chunk, last_severity, last_color)) = chunks.last_mut() { - if *last_severity == chunk.diagnostic_severity && *last_color == color { - last_chunk.push_str(chunk.text); - continue; - } + if let Some((last_chunk, last_severity, last_color)) = chunks.last_mut() + && *last_severity == chunk.diagnostic_severity + && *last_color == color + { + last_chunk.push_str(chunk.text); + continue; } chunks.push((chunk.text.to_string(), chunk.diagnostic_severity, color)); @@ -2378,7 +2415,6 @@ pub mod tests { style: BlockStyle::Fixed, render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }], cx, ); @@ -2512,7 +2548,9 @@ pub mod tests { cx.update(|cx| syntax_chunks(DisplayRow(0)..DisplayRow(5), &map, &theme, cx)), [ ("fn \n".to_string(), None), - ("oute\nr".to_string(), Some(Hsla::blue())), + ("oute".to_string(), Some(Hsla::blue())), + ("\n".to_string(), None), + ("r".to_string(), Some(Hsla::blue())), ("() \n{}\n\n".to_string(), None), ] ); @@ -2535,8 +2573,11 @@ pub mod tests { [ ("out".to_string(), Some(Hsla::blue())), ("⋯\n".to_string(), None), - (" \nfn ".to_string(), Some(Hsla::red())), - ("i\n".to_string(), Some(Hsla::blue())) + (" ".to_string(), Some(Hsla::red())), + ("\n".to_string(), None), + ("fn ".to_string(), Some(Hsla::red())), + ("i".to_string(), Some(Hsla::blue())), + ("\n".to_string(), None) ] ); } @@ -2601,7 +2642,7 @@ pub mod tests { map.update(cx, |map, _cx| { map.highlight_text( - TypeId::of::(), + HighlightKey::Type(TypeId::of::()), highlighted_ranges .into_iter() .map(|range| { @@ -2861,11 +2902,12 @@ pub mod tests { .syntax_highlight_id .and_then(|id| id.style(theme)?.color); let highlight_color = chunk.highlight_style.and_then(|style| style.color); - if let Some((last_chunk, last_syntax_color, last_highlight_color)) = chunks.last_mut() { - if syntax_color == *last_syntax_color && highlight_color == *last_highlight_color { - last_chunk.push_str(chunk.text); - continue; - } + if let Some((last_chunk, last_syntax_color, last_highlight_color)) = chunks.last_mut() + && syntax_color == *last_syntax_color + && highlight_color == *last_highlight_color + { + last_chunk.push_str(chunk.text); + continue; } chunks.push((chunk.text.to_string(), syntax_color, highlight_color)); } diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 8214ab7a8c..b073fe7be7 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -22,7 +22,7 @@ use std::{ atomic::{AtomicUsize, Ordering::SeqCst}, }, }; -use sum_tree::{Bias, SumTree, Summary, TreeMap}; +use sum_tree::{Bias, Dimensions, SumTree, Summary, TreeMap}; use text::{BufferId, Edit}; use ui::ElementId; @@ -193,7 +193,6 @@ pub struct CustomBlock { style: BlockStyle, render: Arc>, priority: usize, - pub(crate) render_in_minimap: bool, } #[derive(Clone)] @@ -205,7 +204,6 @@ pub struct BlockProperties

{ pub style: BlockStyle, pub render: RenderBlock, pub priority: usize, - pub render_in_minimap: bool, } impl Debug for BlockProperties

{ @@ -292,7 +290,10 @@ pub enum Block { ExcerptBoundary { excerpt: ExcerptInfo, height: u32, - starts_new_buffer: bool, + }, + BufferHeader { + excerpt: ExcerptInfo, + height: u32, }, } @@ -305,27 +306,37 @@ impl Block { .. } => BlockId::ExcerptBoundary(next_excerpt.id), Block::FoldedBuffer { first_excerpt, .. } => BlockId::FoldedBuffer(first_excerpt.id), + Block::BufferHeader { + excerpt: next_excerpt, + .. + } => BlockId::ExcerptBoundary(next_excerpt.id), } } pub fn has_height(&self) -> bool { match self { Block::Custom(block) => block.height.is_some(), - Block::ExcerptBoundary { .. } | Block::FoldedBuffer { .. } => true, + Block::ExcerptBoundary { .. } + | Block::FoldedBuffer { .. } + | Block::BufferHeader { .. } => true, } } pub fn height(&self) -> u32 { match self { Block::Custom(block) => block.height.unwrap_or(0), - Block::ExcerptBoundary { height, .. } | Block::FoldedBuffer { height, .. } => *height, + Block::ExcerptBoundary { height, .. } + | Block::FoldedBuffer { height, .. } + | Block::BufferHeader { height, .. } => *height, } } pub fn style(&self) -> BlockStyle { match self { Block::Custom(block) => block.style, - Block::ExcerptBoundary { .. } | Block::FoldedBuffer { .. } => BlockStyle::Sticky, + Block::ExcerptBoundary { .. } + | Block::FoldedBuffer { .. } + | Block::BufferHeader { .. } => BlockStyle::Sticky, } } @@ -334,6 +345,7 @@ impl Block { Block::Custom(block) => matches!(block.placement, BlockPlacement::Above(_)), Block::FoldedBuffer { .. } => false, Block::ExcerptBoundary { .. } => true, + Block::BufferHeader { .. } => true, } } @@ -342,6 +354,7 @@ impl Block { Block::Custom(block) => matches!(block.placement, BlockPlacement::Near(_)), Block::FoldedBuffer { .. } => false, Block::ExcerptBoundary { .. } => false, + Block::BufferHeader { .. } => false, } } @@ -353,6 +366,7 @@ impl Block { ), Block::FoldedBuffer { .. } => false, Block::ExcerptBoundary { .. } => false, + Block::BufferHeader { .. } => false, } } @@ -361,6 +375,7 @@ impl Block { Block::Custom(block) => matches!(block.placement, BlockPlacement::Replace(_)), Block::FoldedBuffer { .. } => true, Block::ExcerptBoundary { .. } => false, + Block::BufferHeader { .. } => false, } } @@ -369,6 +384,7 @@ impl Block { Block::Custom(_) => false, Block::FoldedBuffer { .. } => true, Block::ExcerptBoundary { .. } => true, + Block::BufferHeader { .. } => true, } } @@ -376,9 +392,8 @@ impl Block { match self { Block::Custom(_) => false, Block::FoldedBuffer { .. } => true, - Block::ExcerptBoundary { - starts_new_buffer, .. - } => *starts_new_buffer, + Block::ExcerptBoundary { .. } => false, + Block::BufferHeader { .. } => true, } } } @@ -395,14 +410,14 @@ impl Debug for Block { .field("first_excerpt", &first_excerpt) .field("height", height) .finish(), - Self::ExcerptBoundary { - starts_new_buffer, - excerpt, - height, - } => f + Self::ExcerptBoundary { excerpt, height } => f .debug_struct("ExcerptBoundary") .field("excerpt", excerpt) - .field("starts_new_buffer", starts_new_buffer) + .field("height", height) + .finish(), + Self::BufferHeader { excerpt, height } => f + .debug_struct("BufferHeader") + .field("excerpt", excerpt) .field("height", height) .finish(), } @@ -418,7 +433,7 @@ struct TransformSummary { } pub struct BlockChunks<'a> { - transforms: sum_tree::Cursor<'a, Transform, (BlockRow, WrapRow)>, + transforms: sum_tree::Cursor<'a, Transform, Dimensions>, input_chunks: wrap_map::WrapChunks<'a>, input_chunk: Chunk<'a>, output_row: u32, @@ -428,7 +443,7 @@ pub struct BlockChunks<'a> { #[derive(Clone)] pub struct BlockRows<'a> { - transforms: sum_tree::Cursor<'a, Transform, (BlockRow, WrapRow)>, + transforms: sum_tree::Cursor<'a, Transform, Dimensions>, input_rows: wrap_map::WrapRows<'a>, output_row: BlockRow, started: bool, @@ -464,7 +479,7 @@ impl BlockMap { map } - pub fn read(&self, wrap_snapshot: WrapSnapshot, edits: Patch) -> BlockMapReader { + pub fn read(&self, wrap_snapshot: WrapSnapshot, edits: Patch) -> BlockMapReader<'_> { self.sync(&wrap_snapshot, edits); *self.wrap_snapshot.borrow_mut() = wrap_snapshot.clone(); BlockMapReader { @@ -479,7 +494,7 @@ impl BlockMap { } } - pub fn write(&mut self, wrap_snapshot: WrapSnapshot, edits: Patch) -> BlockMapWriter { + pub fn write(&mut self, wrap_snapshot: WrapSnapshot, edits: Patch) -> BlockMapWriter<'_> { self.sync(&wrap_snapshot, edits); *self.wrap_snapshot.borrow_mut() = wrap_snapshot; BlockMapWriter(self) @@ -526,27 +541,23 @@ impl BlockMap { // * Isomorphic transforms that end *at* the start of the edit // * Below blocks that end at the start of the edit // However, if we hit a replace block that ends at the start of the edit we want to reconstruct it. - new_transforms.append(cursor.slice(&old_start, Bias::Left, &()), &()); - if let Some(transform) = cursor.item() { - if transform.summary.input_rows > 0 - && cursor.end(&()) == old_start - && transform - .block - .as_ref() - .map_or(true, |b| !b.is_replacement()) - { - // Preserve the transform (push and next) - new_transforms.push(transform.clone(), &()); - cursor.next(&()); + new_transforms.append(cursor.slice(&old_start, Bias::Left), &()); + if let Some(transform) = cursor.item() + && transform.summary.input_rows > 0 + && cursor.end() == old_start + && transform.block.as_ref().is_none_or(|b| !b.is_replacement()) + { + // Preserve the transform (push and next) + new_transforms.push(transform.clone(), &()); + cursor.next(); - // Preserve below blocks at end of edit - while let Some(transform) = cursor.item() { - if transform.block.as_ref().map_or(false, |b| b.place_below()) { - new_transforms.push(transform.clone(), &()); - cursor.next(&()); - } else { - break; - } + // Preserve below blocks at end of edit + while let Some(transform) = cursor.item() { + if transform.block.as_ref().is_some_and(|b| b.place_below()) { + new_transforms.push(transform.clone(), &()); + cursor.next(); + } else { + break; } } } @@ -581,8 +592,8 @@ impl BlockMap { let mut new_end = WrapRow(edit.new.end); loop { // Seek to the transform starting at or after the end of the edit - cursor.seek(&old_end, Bias::Left, &()); - cursor.next(&()); + cursor.seek(&old_end, Bias::Left); + cursor.next(); // Extend edit to the end of the discarded transform so it is reconstructed in full let transform_rows_after_edit = cursor.start().0 - old_end.0; @@ -594,8 +605,8 @@ impl BlockMap { if next_edit.old.start <= cursor.start().0 { old_end = WrapRow(next_edit.old.end); new_end = WrapRow(next_edit.new.end); - cursor.seek(&old_end, Bias::Left, &()); - cursor.next(&()); + cursor.seek(&old_end, Bias::Left); + cursor.next(); edits.next(); } else { break; @@ -609,8 +620,8 @@ impl BlockMap { // Discard below blocks at the end of the edit. They'll be reconstructed. while let Some(transform) = cursor.item() { - if transform.block.as_ref().map_or(false, |b| b.place_below()) { - cursor.next(&()); + if transform.block.as_ref().is_some_and(|b| b.place_below()) { + cursor.next(); } else { break; } @@ -659,22 +670,20 @@ impl BlockMap { .iter() .filter_map(|block| { let placement = block.placement.to_wrap_row(wrap_snapshot)?; - if let BlockPlacement::Above(row) = placement { - if row < new_start { - return None; - } + if let BlockPlacement::Above(row) = placement + && row < new_start + { + return None; } Some((placement, Block::Custom(block.clone()))) }), ); - if buffer.show_headers() { - blocks_in_edit.extend(self.header_and_footer_blocks( - buffer, - (start_bound, end_bound), - wrap_snapshot, - )); - } + blocks_in_edit.extend(self.header_and_footer_blocks( + buffer, + (start_bound, end_bound), + wrap_snapshot, + )); BlockMap::sort_blocks(&mut blocks_in_edit); @@ -722,7 +731,7 @@ impl BlockMap { push_isomorphic(&mut new_transforms, rows_after_last_block, wrap_snapshot); } - new_transforms.append(cursor.suffix(&()), &()); + new_transforms.append(cursor.suffix(), &()); debug_assert_eq!( new_transforms.summary().input_rows, wrap_snapshot.max_point().row() + 1 @@ -777,7 +786,7 @@ impl BlockMap { if self.buffers_with_disabled_headers.contains(&new_buffer_id) { continue; } - if self.folded_buffers.contains(&new_buffer_id) { + if self.folded_buffers.contains(&new_buffer_id) && buffer.show_headers() { let mut last_excerpt_end_row = first_excerpt.end_row; while let Some(next_boundary) = boundaries.peek() { @@ -810,20 +819,24 @@ impl BlockMap { } } - if new_buffer_id.is_some() { + let starts_new_buffer = new_buffer_id.is_some(); + let block = if starts_new_buffer && buffer.show_headers() { height += self.buffer_header_height; - } else { + Block::BufferHeader { + excerpt: excerpt_boundary.next, + height, + } + } else if excerpt_boundary.prev.is_some() { height += self.excerpt_header_height; - } - - return Some(( - BlockPlacement::Above(WrapRow(wrap_row)), Block::ExcerptBoundary { excerpt: excerpt_boundary.next, height, - starts_new_buffer: new_buffer_id.is_some(), - }, - )); + } + } else { + continue; + }; + + return Some((BlockPlacement::Above(WrapRow(wrap_row)), block)); } }) } @@ -848,13 +861,25 @@ impl BlockMap { ( Block::ExcerptBoundary { excerpt: excerpt_a, .. + } + | Block::BufferHeader { + excerpt: excerpt_a, .. }, Block::ExcerptBoundary { excerpt: excerpt_b, .. + } + | Block::BufferHeader { + excerpt: excerpt_b, .. }, ) => Some(excerpt_a.id).cmp(&Some(excerpt_b.id)), - (Block::ExcerptBoundary { .. }, Block::Custom(_)) => Ordering::Less, - (Block::Custom(_), Block::ExcerptBoundary { .. }) => Ordering::Greater, + ( + Block::ExcerptBoundary { .. } | Block::BufferHeader { .. }, + Block::Custom(_), + ) => Ordering::Less, + ( + Block::Custom(_), + Block::ExcerptBoundary { .. } | Block::BufferHeader { .. }, + ) => Ordering::Greater, (Block::Custom(block_a), Block::Custom(block_b)) => block_a .priority .cmp(&block_b.priority) @@ -972,19 +997,19 @@ impl BlockMapReader<'_> { .unwrap_or(self.wrap_snapshot.max_point().row() + 1), ); - let mut cursor = self.transforms.cursor::<(WrapRow, BlockRow)>(&()); - cursor.seek(&start_wrap_row, Bias::Left, &()); + let mut cursor = self.transforms.cursor::>(&()); + cursor.seek(&start_wrap_row, Bias::Left); while let Some(transform) = cursor.item() { if cursor.start().0 > end_wrap_row { break; } - if let Some(BlockId::Custom(id)) = transform.block.as_ref().map(|block| block.id()) { - if id == block_id { - return Some(cursor.start().1); - } + if let Some(BlockId::Custom(id)) = transform.block.as_ref().map(|block| block.id()) + && id == block_id + { + return Some(cursor.start().1); } - cursor.next(&()); + cursor.next(); } None @@ -1044,7 +1069,6 @@ impl BlockMapWriter<'_> { render: Arc::new(Mutex::new(block.render)), style: block.style, priority: block.priority, - render_in_minimap: block.render_in_minimap, }); self.0.custom_blocks.insert(block_ix, new_block.clone()); self.0.custom_blocks_by_id.insert(id, new_block); @@ -1079,7 +1103,6 @@ impl BlockMapWriter<'_> { style: block.style, render: block.render.clone(), priority: block.priority, - render_in_minimap: block.render_in_minimap, }; let new_block = Arc::new(new_block); *block = new_block.clone(); @@ -1296,21 +1319,21 @@ impl BlockSnapshot { ) -> BlockChunks<'a> { let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows); - let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); - cursor.seek(&BlockRow(rows.start), Bias::Right, &()); + let mut cursor = self.transforms.cursor::>(&()); + cursor.seek(&BlockRow(rows.start), Bias::Right); let transform_output_start = cursor.start().0.0; let transform_input_start = cursor.start().1.0; let mut input_start = transform_input_start; let mut input_end = transform_input_start; - if let Some(transform) = cursor.item() { - if transform.block.is_none() { - input_start += rows.start - transform_output_start; - input_end += cmp::min( - rows.end - transform_output_start, - transform.summary.input_rows, - ); - } + if let Some(transform) = cursor.item() + && transform.block.is_none() + { + input_start += rows.start - transform_output_start; + input_end += cmp::min( + rows.end - transform_output_start, + transform.summary.input_rows, + ); } BlockChunks { @@ -1327,13 +1350,13 @@ impl BlockSnapshot { } } - pub(super) fn row_infos(&self, start_row: BlockRow) -> BlockRows { - let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); - cursor.seek(&start_row, Bias::Right, &()); - let (output_start, input_start) = cursor.start(); + pub(super) fn row_infos(&self, start_row: BlockRow) -> BlockRows<'_> { + let mut cursor = self.transforms.cursor::>(&()); + cursor.seek(&start_row, Bias::Right); + let Dimensions(output_start, input_start, _) = cursor.start(); let overshoot = if cursor .item() - .map_or(false, |transform| transform.block.is_none()) + .is_some_and(|transform| transform.block.is_none()) { start_row.0 - output_start.0 } else { @@ -1350,9 +1373,9 @@ impl BlockSnapshot { pub fn blocks_in_range(&self, rows: Range) -> impl Iterator { let mut cursor = self.transforms.cursor::(&()); - cursor.seek(&BlockRow(rows.start), Bias::Left, &()); - while cursor.start().0 < rows.start && cursor.end(&()).0 <= rows.start { - cursor.next(&()); + cursor.seek(&BlockRow(rows.start), Bias::Left); + while cursor.start().0 < rows.start && cursor.end().0 <= rows.start { + cursor.next(); } std::iter::from_fn(move || { @@ -1363,15 +1386,15 @@ impl BlockSnapshot { && transform .block .as_ref() - .map_or(false, |block| block.height() > 0)) + .is_some_and(|block| block.height() > 0)) { break; } if let Some(block) = &transform.block { - cursor.next(&()); + cursor.next(); return Some((start_row, block)); } else { - cursor.next(&()); + cursor.next(); } } None @@ -1381,16 +1404,18 @@ impl BlockSnapshot { pub fn sticky_header_excerpt(&self, position: f32) -> Option> { let top_row = position as u32; let mut cursor = self.transforms.cursor::(&()); - cursor.seek(&BlockRow(top_row), Bias::Right, &()); + cursor.seek(&BlockRow(top_row), Bias::Right); while let Some(transform) = cursor.item() { match &transform.block { - Some(Block::ExcerptBoundary { excerpt, .. }) => { + Some( + Block::ExcerptBoundary { excerpt, .. } | Block::BufferHeader { excerpt, .. }, + ) => { return Some(StickyHeaderExcerpt { excerpt }); } Some(block) if block.is_buffer_header() => return None, _ => { - cursor.prev(&()); + cursor.prev(); continue; } } @@ -1418,7 +1443,7 @@ impl BlockSnapshot { let wrap_row = WrapRow(wrap_point.row()); let mut cursor = self.transforms.cursor::(&()); - cursor.seek(&wrap_row, Bias::Left, &()); + cursor.seek(&wrap_row, Bias::Left); while let Some(transform) = cursor.item() { if let Some(block) = transform.block.as_ref() { @@ -1429,7 +1454,7 @@ impl BlockSnapshot { break; } - cursor.next(&()); + cursor.next(); } None @@ -1445,19 +1470,19 @@ impl BlockSnapshot { } pub fn longest_row_in_range(&self, range: Range) -> BlockRow { - let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); - cursor.seek(&range.start, Bias::Right, &()); + let mut cursor = self.transforms.cursor::>(&()); + cursor.seek(&range.start, Bias::Right); let mut longest_row = range.start; let mut longest_row_chars = 0; if let Some(transform) = cursor.item() { if transform.block.is_none() { - let (output_start, input_start) = cursor.start(); + let Dimensions(output_start, input_start, _) = cursor.start(); let overshoot = range.start.0 - output_start.0; let wrap_start_row = input_start.0 + overshoot; let wrap_end_row = cmp::min( input_start.0 + (range.end.0 - output_start.0), - cursor.end(&()).1.0, + cursor.end().1.0, ); let summary = self .wrap_snapshot @@ -1465,29 +1490,29 @@ impl BlockSnapshot { longest_row = BlockRow(range.start.0 + summary.longest_row); longest_row_chars = summary.longest_row_chars; } - cursor.next(&()); + cursor.next(); } let cursor_start_row = cursor.start().0; if range.end > cursor_start_row { - let summary = cursor.summary::<_, TransformSummary>(&range.end, Bias::Right, &()); + let summary = cursor.summary::<_, TransformSummary>(&range.end, Bias::Right); if summary.longest_row_chars > longest_row_chars { longest_row = BlockRow(cursor_start_row.0 + summary.longest_row); longest_row_chars = summary.longest_row_chars; } - if let Some(transform) = cursor.item() { - if transform.block.is_none() { - let (output_start, input_start) = cursor.start(); - let overshoot = range.end.0 - output_start.0; - let wrap_start_row = input_start.0; - let wrap_end_row = input_start.0 + overshoot; - let summary = self - .wrap_snapshot - .text_summary_for_range(wrap_start_row..wrap_end_row); - if summary.longest_row_chars > longest_row_chars { - longest_row = BlockRow(output_start.0 + summary.longest_row); - } + if let Some(transform) = cursor.item() + && transform.block.is_none() + { + let Dimensions(output_start, input_start, _) = cursor.start(); + let overshoot = range.end.0 - output_start.0; + let wrap_start_row = input_start.0; + let wrap_end_row = input_start.0 + overshoot; + let summary = self + .wrap_snapshot + .text_summary_for_range(wrap_start_row..wrap_end_row); + if summary.longest_row_chars > longest_row_chars { + longest_row = BlockRow(output_start.0 + summary.longest_row); } } } @@ -1496,10 +1521,10 @@ impl BlockSnapshot { } pub(super) fn line_len(&self, row: BlockRow) -> u32 { - let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); - cursor.seek(&BlockRow(row.0), Bias::Right, &()); + let mut cursor = self.transforms.cursor::>(&()); + cursor.seek(&BlockRow(row.0), Bias::Right); if let Some(transform) = cursor.item() { - let (output_start, input_start) = cursor.start(); + let Dimensions(output_start, input_start, _) = cursor.start(); let overshoot = row.0 - output_start.0; if transform.block.is_some() { 0 @@ -1514,14 +1539,14 @@ impl BlockSnapshot { } pub(super) fn is_block_line(&self, row: BlockRow) -> bool { - let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); - cursor.seek(&row, Bias::Right, &()); - cursor.item().map_or(false, |t| t.block.is_some()) + let mut cursor = self.transforms.cursor::>(&()); + cursor.seek(&row, Bias::Right); + cursor.item().is_some_and(|t| t.block.is_some()) } pub(super) fn is_folded_buffer_header(&self, row: BlockRow) -> bool { - let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); - cursor.seek(&row, Bias::Right, &()); + let mut cursor = self.transforms.cursor::>(&()); + cursor.seek(&row, Bias::Right); let Some(transform) = cursor.item() else { return false; }; @@ -1532,41 +1557,40 @@ impl BlockSnapshot { let wrap_point = self .wrap_snapshot .make_wrap_point(Point::new(row.0, 0), Bias::Left); - let mut cursor = self.transforms.cursor::<(WrapRow, BlockRow)>(&()); - cursor.seek(&WrapRow(wrap_point.row()), Bias::Right, &()); - cursor.item().map_or(false, |transform| { + let mut cursor = self.transforms.cursor::>(&()); + cursor.seek(&WrapRow(wrap_point.row()), Bias::Right); + cursor.item().is_some_and(|transform| { transform .block .as_ref() - .map_or(false, |block| block.is_replacement()) + .is_some_and(|block| block.is_replacement()) }) } pub fn clip_point(&self, point: BlockPoint, bias: Bias) -> BlockPoint { - let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); - cursor.seek(&BlockRow(point.row), Bias::Right, &()); + let mut cursor = self.transforms.cursor::>(&()); + cursor.seek(&BlockRow(point.row), Bias::Right); let max_input_row = WrapRow(self.transforms.summary().input_rows); let mut search_left = - (bias == Bias::Left && cursor.start().1.0 > 0) || cursor.end(&()).1 == max_input_row; + (bias == Bias::Left && cursor.start().1.0 > 0) || cursor.end().1 == max_input_row; let mut reversed = false; loop { if let Some(transform) = cursor.item() { - let (output_start_row, input_start_row) = cursor.start(); - let (output_end_row, input_end_row) = cursor.end(&()); + let Dimensions(output_start_row, input_start_row, _) = cursor.start(); + let Dimensions(output_end_row, input_end_row, _) = cursor.end(); let output_start = Point::new(output_start_row.0, 0); let input_start = Point::new(input_start_row.0, 0); let input_end = Point::new(input_end_row.0, 0); match transform.block.as_ref() { Some(block) => { - if block.is_replacement() { - if ((bias == Bias::Left || search_left) && output_start <= point.0) - || (!search_left && output_start >= point.0) - { - return BlockPoint(output_start); - } + if block.is_replacement() + && (((bias == Bias::Left || search_left) && output_start <= point.0) + || (!search_left && output_start >= point.0)) + { + return BlockPoint(output_start); } } None => { @@ -1588,28 +1612,28 @@ impl BlockSnapshot { } if search_left { - cursor.prev(&()); + cursor.prev(); } else { - cursor.next(&()); + cursor.next(); } } else if reversed { return self.max_point(); } else { reversed = true; search_left = !search_left; - cursor.seek(&BlockRow(point.row), Bias::Right, &()); + cursor.seek(&BlockRow(point.row), Bias::Right); } } } pub fn to_block_point(&self, wrap_point: WrapPoint) -> BlockPoint { - let mut cursor = self.transforms.cursor::<(WrapRow, BlockRow)>(&()); - cursor.seek(&WrapRow(wrap_point.row()), Bias::Right, &()); + let mut cursor = self.transforms.cursor::>(&()); + cursor.seek(&WrapRow(wrap_point.row()), Bias::Right); if let Some(transform) = cursor.item() { if transform.block.is_some() { BlockPoint::new(cursor.start().1.0, 0) } else { - let (input_start_row, output_start_row) = cursor.start(); + let Dimensions(input_start_row, output_start_row, _) = cursor.start(); let input_start = Point::new(input_start_row.0, 0); let output_start = Point::new(output_start_row.0, 0); let input_overshoot = wrap_point.0 - input_start; @@ -1621,8 +1645,8 @@ impl BlockSnapshot { } pub fn to_wrap_point(&self, block_point: BlockPoint, bias: Bias) -> WrapPoint { - let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); - cursor.seek(&BlockRow(block_point.row), Bias::Right, &()); + let mut cursor = self.transforms.cursor::>(&()); + cursor.seek(&BlockRow(block_point.row), Bias::Right); if let Some(transform) = cursor.item() { match transform.block.as_ref() { Some(block) => { @@ -1634,7 +1658,7 @@ impl BlockSnapshot { } else if bias == Bias::Left { WrapPoint::new(cursor.start().1.0, 0) } else { - let wrap_row = cursor.end(&()).1.0 - 1; + let wrap_row = cursor.end().1.0 - 1; WrapPoint::new(wrap_row, self.wrap_snapshot.line_len(wrap_row)) } } @@ -1654,14 +1678,14 @@ impl BlockChunks<'_> { /// Go to the next transform fn advance(&mut self) { self.input_chunk = Chunk::default(); - self.transforms.next(&()); + self.transforms.next(); while let Some(transform) = self.transforms.item() { if transform .block .as_ref() - .map_or(false, |block| block.height() == 0) + .is_some_and(|block| block.height() == 0) { - self.transforms.next(&()); + self.transforms.next(); } else { break; } @@ -1670,13 +1694,13 @@ impl BlockChunks<'_> { if self .transforms .item() - .map_or(false, |transform| transform.block.is_none()) + .is_some_and(|transform| transform.block.is_none()) { let start_input_row = self.transforms.start().1.0; let start_output_row = self.transforms.start().0.0; if start_output_row < self.max_output_row { let end_input_row = cmp::min( - self.transforms.end(&()).1.0, + self.transforms.end().1.0, start_input_row + (self.max_output_row - start_output_row), ); self.input_chunks.seek(start_input_row..end_input_row); @@ -1700,7 +1724,7 @@ impl<'a> Iterator for BlockChunks<'a> { let transform = self.transforms.item()?; if transform.block.is_some() { let block_start = self.transforms.start().0.0; - let mut block_end = self.transforms.end(&()).0.0; + let mut block_end = self.transforms.end().0.0; self.advance(); if self.transforms.item().is_none() { block_end -= 1; @@ -1735,7 +1759,7 @@ impl<'a> Iterator for BlockChunks<'a> { } } - let transform_end = self.transforms.end(&()).0.0; + let transform_end = self.transforms.end().0.0; let (prefix_rows, prefix_bytes) = offset_for_row(self.input_chunk.text, transform_end - self.output_row); self.output_row += prefix_rows; @@ -1774,15 +1798,15 @@ impl Iterator for BlockRows<'_> { self.started = true; } - if self.output_row.0 >= self.transforms.end(&()).0.0 { - self.transforms.next(&()); + if self.output_row.0 >= self.transforms.end().0.0 { + self.transforms.next(); while let Some(transform) = self.transforms.item() { if transform .block .as_ref() - .map_or(false, |block| block.height() == 0) + .is_some_and(|block| block.height() == 0) { - self.transforms.next(&()); + self.transforms.next(); } else { break; } @@ -1792,7 +1816,7 @@ impl Iterator for BlockRows<'_> { if transform .block .as_ref() - .map_or(true, |block| block.is_replacement()) + .is_none_or(|block| block.is_replacement()) { self.input_rows.seek(self.transforms.start().1.0); } @@ -1976,7 +2000,6 @@ mod tests { height: Some(1), render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }, BlockProperties { style: BlockStyle::Fixed, @@ -1984,7 +2007,6 @@ mod tests { height: Some(2), render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }, BlockProperties { style: BlockStyle::Fixed, @@ -1992,7 +2014,6 @@ mod tests { height: Some(3), render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }, ]); @@ -2168,7 +2189,7 @@ mod tests { } let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx); - let (_, inlay_snapshot) = InlayMap::new(multi_buffer_snapshot.clone()); + let (_, inlay_snapshot) = InlayMap::new(multi_buffer_snapshot); let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); let (_, wraps_snapshot) = WrapMap::new(tab_snapshot, font, font_size, Some(wrap_width), cx); @@ -2217,7 +2238,6 @@ mod tests { height: Some(1), render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }, BlockProperties { style: BlockStyle::Fixed, @@ -2225,7 +2245,6 @@ mod tests { height: Some(2), render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }, BlockProperties { style: BlockStyle::Fixed, @@ -2233,7 +2252,6 @@ mod tests { height: Some(3), render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }, ]); @@ -2290,7 +2308,7 @@ mod tests { new_heights.insert(block_ids[0], 3); block_map_writer.resize(new_heights); - let snapshot = block_map.read(wraps_snapshot.clone(), Default::default()); + let snapshot = block_map.read(wraps_snapshot, Default::default()); // Same height as before, should remain the same assert_eq!(snapshot.text(), "aaa\n\n\n\n\n\nbbb\nccc\nddd\n\n\n"); } @@ -2300,8 +2318,6 @@ mod tests { fn test_blocks_on_wrapped_lines(cx: &mut gpui::TestAppContext) { cx.update(init_test); - let _font_id = cx.text_system().font_id(&font("Helvetica")).unwrap(); - let text = "one two three\nfour five six\nseven eight"; let buffer = cx.update(|cx| MultiBuffer::build_simple(text, cx)); @@ -2322,7 +2338,6 @@ mod tests { render: Arc::new(|_| div().into_any()), height: Some(1), priority: 0, - render_in_minimap: true, }, BlockProperties { style: BlockStyle::Fixed, @@ -2330,7 +2345,6 @@ mod tests { render: Arc::new(|_| div().into_any()), height: Some(1), priority: 0, - render_in_minimap: true, }, ]); @@ -2370,7 +2384,6 @@ mod tests { height: Some(4), render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }])[0]; let blocks_snapshot = block_map.read(wraps_snapshot, Default::default()); @@ -2380,16 +2393,14 @@ mod tests { buffer.edit([(Point::new(2, 0)..Point::new(3, 0), "")], None, cx); buffer.snapshot(cx) }); - let (inlay_snapshot, inlay_edits) = inlay_map.sync( - buffer_snapshot.clone(), - buffer_subscription.consume().into_inner(), - ); + let (inlay_snapshot, inlay_edits) = + inlay_map.sync(buffer_snapshot, buffer_subscription.consume().into_inner()); let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); let (tab_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size); let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| { wrap_map.sync(tab_snapshot, tab_edits, cx) }); - let blocks_snapshot = block_map.read(wraps_snapshot.clone(), wrap_edits); + let blocks_snapshot = block_map.read(wraps_snapshot, wrap_edits); assert_eq!(blocks_snapshot.text(), "line1\n\n\n\n\nline5"); let buffer_snapshot = buffer.update(cx, |buffer, cx| { @@ -2424,7 +2435,6 @@ mod tests { height: Some(1), render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }, BlockProperties { style: BlockStyle::Fixed, @@ -2432,7 +2442,6 @@ mod tests { height: Some(1), render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }, BlockProperties { style: BlockStyle::Fixed, @@ -2440,7 +2449,6 @@ mod tests { height: Some(1), render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }, ]); let blocks_snapshot = block_map.read(wraps_snapshot.clone(), Default::default()); @@ -2455,7 +2463,6 @@ mod tests { height: Some(1), render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }, BlockProperties { style: BlockStyle::Fixed, @@ -2463,7 +2470,6 @@ mod tests { height: Some(1), render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }, BlockProperties { style: BlockStyle::Fixed, @@ -2471,7 +2477,6 @@ mod tests { height: Some(1), render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }, ]); let blocks_snapshot = block_map.read(wraps_snapshot.clone(), Default::default()); @@ -2480,7 +2485,7 @@ mod tests { // Removing the replace block shows all the hidden blocks again. let mut writer = block_map.write(wraps_snapshot.clone(), Default::default()); writer.remove(HashSet::from_iter([replace_block_id])); - let blocks_snapshot = block_map.read(wraps_snapshot.clone(), Default::default()); + let blocks_snapshot = block_map.read(wraps_snapshot, Default::default()); assert_eq!( blocks_snapshot.text(), "\nline1\n\nline2\n\n\nline 2.1\nline2.2\nline 2.3\nline 2.4\n\nline4\n\nline5" @@ -2571,7 +2576,6 @@ mod tests { height: Some(1), render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }, BlockProperties { style: BlockStyle::Fixed, @@ -2579,7 +2583,6 @@ mod tests { height: Some(1), render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }, BlockProperties { style: BlockStyle::Fixed, @@ -2587,7 +2590,6 @@ mod tests { height: Some(1), render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }, ]); let excerpt_blocks_3 = writer.insert(vec![ @@ -2597,7 +2599,6 @@ mod tests { height: Some(1), render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }, BlockProperties { style: BlockStyle::Fixed, @@ -2605,7 +2606,6 @@ mod tests { height: Some(1), render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }, ]); @@ -2653,7 +2653,6 @@ mod tests { height: Some(1), render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }]); let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default()); let blocks = blocks_snapshot @@ -2825,7 +2824,7 @@ mod tests { buffer.read_with(cx, |buffer, cx| { writer.fold_buffers([buffer_id_3], buffer, cx); }); - let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default()); + let blocks_snapshot = block_map.read(wrap_snapshot, Patch::default()); let blocks = blocks_snapshot .blocks_in_range(0..u32::MAX) .collect::>(); @@ -2878,7 +2877,7 @@ mod tests { assert_eq!(buffer_ids.len(), 1); let buffer_id = buffer_ids[0]; - let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot); let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); let (_, wrap_snapshot) = @@ -2892,7 +2891,7 @@ mod tests { buffer.read_with(cx, |buffer, cx| { writer.fold_buffers([buffer_id], buffer, cx); }); - let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default()); + let blocks_snapshot = block_map.read(wrap_snapshot, Patch::default()); let blocks = blocks_snapshot .blocks_in_range(0..u32::MAX) .collect::>(); @@ -2900,12 +2899,7 @@ mod tests { 1, blocks .iter() - .filter(|(_, block)| { - match block { - Block::FoldedBuffer { .. } => true, - _ => false, - } - }) + .filter(|(_, block)| { matches!(block, Block::FoldedBuffer { .. }) }) .count(), "Should have one folded block, producing a header of the second buffer" ); @@ -3011,7 +3005,6 @@ mod tests { height: Some(height), render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, } }) .collect::>(); @@ -3032,7 +3025,6 @@ mod tests { style: props.style, render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, })); for (block_properties, block_id) in block_properties.iter().zip(block_ids) { @@ -3224,9 +3216,9 @@ mod tests { // so we special case row 0 to assume a leading '\n'. // // Linehood is the birthright of strings. - let mut input_text_lines = input_text.split('\n').enumerate().peekable(); + let input_text_lines = input_text.split('\n').enumerate().peekable(); let mut block_row = 0; - while let Some((wrap_row, input_line)) = input_text_lines.next() { + for (wrap_row, input_line) in input_text_lines { let wrap_row = wrap_row as u32; let multibuffer_row = wraps_snapshot .to_point(WrapPoint::new(wrap_row, 0), Bias::Left) @@ -3257,34 +3249,32 @@ mod tests { let mut is_in_replace_block = false; if let Some((BlockPlacement::Replace(replace_range), block)) = sorted_blocks_iter.peek() + && wrap_row >= replace_range.start().0 { - if wrap_row >= replace_range.start().0 { - is_in_replace_block = true; + is_in_replace_block = true; - if wrap_row == replace_range.start().0 { - if matches!(block, Block::FoldedBuffer { .. }) { - expected_buffer_rows.push(None); - } else { - expected_buffer_rows - .push(input_buffer_rows[multibuffer_row as usize]); - } + if wrap_row == replace_range.start().0 { + if matches!(block, Block::FoldedBuffer { .. }) { + expected_buffer_rows.push(None); + } else { + expected_buffer_rows.push(input_buffer_rows[multibuffer_row as usize]); } + } - if wrap_row == replace_range.end().0 { - expected_block_positions.push((block_row, block.id())); - let text = "\n".repeat((block.height() - 1) as usize); - if block_row > 0 { - expected_text.push('\n'); - } - expected_text.push_str(&text); - - for _ in 1..block.height() { - expected_buffer_rows.push(None); - } - block_row += block.height(); - - sorted_blocks_iter.next(); + if wrap_row == replace_range.end().0 { + expected_block_positions.push((block_row, block.id())); + let text = "\n".repeat((block.height() - 1) as usize); + if block_row > 0 { + expected_text.push('\n'); } + expected_text.push_str(&text); + + for _ in 1..block.height() { + expected_buffer_rows.push(None); + } + block_row += block.height(); + + sorted_blocks_iter.next(); } } @@ -3557,7 +3547,6 @@ mod tests { height: Some(1), render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }])[0]; let blocks_snapshot = block_map.read(wraps_snapshot.clone(), Default::default()); @@ -3569,7 +3558,7 @@ mod tests { ..buffer_snapshot.anchor_after(Point::new(1, 0))], false, ); - let blocks_snapshot = block_map.read(wraps_snapshot.clone(), Default::default()); + let blocks_snapshot = block_map.read(wraps_snapshot, Default::default()); assert_eq!(blocks_snapshot.text(), "abc\n\ndef\nghi\njkl\nmno"); } diff --git a/crates/editor/src/display_map/crease_map.rs b/crates/editor/src/display_map/crease_map.rs index e6fe4270ec..bdac982fa7 100644 --- a/crates/editor/src/display_map/crease_map.rs +++ b/crates/editor/src/display_map/crease_map.rs @@ -52,15 +52,15 @@ impl CreaseSnapshot { ) -> Option<&'a Crease> { let start = snapshot.anchor_before(Point::new(row.0, 0)); let mut cursor = self.creases.cursor::(snapshot); - cursor.seek(&start, Bias::Left, snapshot); + cursor.seek(&start, Bias::Left); while let Some(item) = cursor.item() { match Ord::cmp(&item.crease.range().start.to_point(snapshot).row, &row.0) { - Ordering::Less => cursor.next(snapshot), + Ordering::Less => cursor.next(), Ordering::Equal => { if item.crease.range().start.is_valid(snapshot) { return Some(&item.crease); } else { - cursor.next(snapshot); + cursor.next(); } } Ordering::Greater => break, @@ -76,11 +76,11 @@ impl CreaseSnapshot { ) -> impl 'a + Iterator> { let start = snapshot.anchor_before(Point::new(range.start.0, 0)); let mut cursor = self.creases.cursor::(snapshot); - cursor.seek(&start, Bias::Left, snapshot); + cursor.seek(&start, Bias::Left); std::iter::from_fn(move || { while let Some(item) = cursor.item() { - cursor.next(snapshot); + cursor.next(); let crease_range = item.crease.range(); let crease_start = crease_range.start.to_point(snapshot); let crease_end = crease_range.end.to_point(snapshot); @@ -102,13 +102,13 @@ impl CreaseSnapshot { let mut cursor = self.creases.cursor::(snapshot); let mut results = Vec::new(); - cursor.next(snapshot); + cursor.next(); while let Some(item) = cursor.item() { let crease_range = item.crease.range(); let start_point = crease_range.start.to_point(snapshot); let end_point = crease_range.end.to_point(snapshot); results.push((item.id, start_point..end_point)); - cursor.next(snapshot); + cursor.next(); } results @@ -298,7 +298,7 @@ impl CreaseMap { let mut cursor = self.snapshot.creases.cursor::(snapshot); for crease in creases { let crease_range = crease.range().clone(); - new_creases.append(cursor.slice(&crease_range, Bias::Left, snapshot), snapshot); + new_creases.append(cursor.slice(&crease_range, Bias::Left), snapshot); let id = self.next_id; self.next_id.0 += 1; @@ -306,7 +306,7 @@ impl CreaseMap { new_creases.push(CreaseItem { crease, id }, snapshot); new_ids.push(id); } - new_creases.append(cursor.suffix(snapshot), snapshot); + new_creases.append(cursor.suffix(), snapshot); new_creases }; new_ids @@ -332,9 +332,9 @@ impl CreaseMap { let mut cursor = self.snapshot.creases.cursor::(snapshot); for (id, range) in &removals { - new_creases.append(cursor.slice(range, Bias::Left, snapshot), snapshot); + new_creases.append(cursor.slice(range, Bias::Left), snapshot); while let Some(item) = cursor.item() { - cursor.next(snapshot); + cursor.next(); if item.id == *id { break; } else { @@ -343,7 +343,7 @@ impl CreaseMap { } } - new_creases.append(cursor.suffix(snapshot), snapshot); + new_creases.append(cursor.suffix(), snapshot); new_creases }; diff --git a/crates/editor/src/display_map/custom_highlights.rs b/crates/editor/src/display_map/custom_highlights.rs index 11356586eb..f3737ea4b7 100644 --- a/crates/editor/src/display_map/custom_highlights.rs +++ b/crates/editor/src/display_map/custom_highlights.rs @@ -1,16 +1,15 @@ use collections::BTreeMap; use gpui::HighlightStyle; use language::Chunk; -use multi_buffer::{Anchor, MultiBufferChunks, MultiBufferSnapshot, ToOffset as _}; +use multi_buffer::{MultiBufferChunks, MultiBufferSnapshot, ToOffset as _}; use std::{ - any::TypeId, cmp, iter::{self, Peekable}, ops::Range, - sync::Arc, vec, }; -use sum_tree::TreeMap; + +use crate::display_map::{HighlightKey, TextHighlights}; pub struct CustomHighlightsChunks<'a> { buffer_chunks: MultiBufferChunks<'a>, @@ -19,15 +18,15 @@ pub struct CustomHighlightsChunks<'a> { multibuffer_snapshot: &'a MultiBufferSnapshot, highlight_endpoints: Peekable>, - active_highlights: BTreeMap, - text_highlights: Option<&'a TreeMap>)>>>, + active_highlights: BTreeMap, + text_highlights: Option<&'a TextHighlights>, } #[derive(Debug, Copy, Clone, Eq, PartialEq)] struct HighlightEndpoint { offset: usize, is_start: bool, - tag: TypeId, + tag: HighlightKey, style: HighlightStyle, } @@ -35,7 +34,7 @@ impl<'a> CustomHighlightsChunks<'a> { pub fn new( range: Range, language_aware: bool, - text_highlights: Option<&'a TreeMap>)>>>, + text_highlights: Option<&'a TextHighlights>, multibuffer_snapshot: &'a MultiBufferSnapshot, ) -> Self { Self { @@ -66,7 +65,7 @@ impl<'a> CustomHighlightsChunks<'a> { fn create_highlight_endpoints( range: &Range, - text_highlights: Option<&TreeMap>)>>>, + text_highlights: Option<&TextHighlights>, buffer: &MultiBufferSnapshot, ) -> iter::Peekable> { let mut highlight_endpoints = Vec::new(); @@ -78,7 +77,7 @@ fn create_highlight_endpoints( let ranges = &text_highlights.1; let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe.end.cmp(&start, &buffer); + let cmp = probe.end.cmp(&start, buffer); if cmp.is_gt() { cmp::Ordering::Greater } else { @@ -89,18 +88,18 @@ fn create_highlight_endpoints( }; for range in &ranges[start_ix..] { - if range.start.cmp(&end, &buffer).is_ge() { + if range.start.cmp(&end, buffer).is_ge() { break; } highlight_endpoints.push(HighlightEndpoint { - offset: range.start.to_offset(&buffer), + offset: range.start.to_offset(buffer), is_start: true, tag, style, }); highlight_endpoints.push(HighlightEndpoint { - offset: range.end.to_offset(&buffer), + offset: range.end.to_offset(buffer), is_start: false, tag, style, diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index e9a611d390..42f46fb749 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -1,3 +1,5 @@ +use crate::{InlayId, display_map::inlay_map::InlayChunk}; + use super::{ Highlights, inlay_map::{InlayBufferRows, InlayChunks, InlayEdit, InlayOffset, InlayPoint, InlaySnapshot}, @@ -15,7 +17,7 @@ use std::{ sync::Arc, usize, }; -use sum_tree::{Bias, Cursor, FilterCursor, SumTree, Summary, TreeMap}; +use sum_tree::{Bias, Cursor, Dimensions, FilterCursor, SumTree, Summary, TreeMap}; use ui::IntoElement as _; use util::post_inc; @@ -96,8 +98,10 @@ impl FoldPoint { } pub fn to_inlay_point(self, snapshot: &FoldSnapshot) -> InlayPoint { - let mut cursor = snapshot.transforms.cursor::<(FoldPoint, InlayPoint)>(&()); - cursor.seek(&self, Bias::Right, &()); + let mut cursor = snapshot + .transforms + .cursor::>(&()); + cursor.seek(&self, Bias::Right); let overshoot = self.0 - cursor.start().0.0; InlayPoint(cursor.start().1.0 + overshoot) } @@ -105,8 +109,8 @@ impl FoldPoint { pub fn to_offset(self, snapshot: &FoldSnapshot) -> FoldOffset { let mut cursor = snapshot .transforms - .cursor::<(FoldPoint, TransformSummary)>(&()); - cursor.seek(&self, Bias::Right, &()); + .cursor::>(&()); + cursor.seek(&self, Bias::Right); let overshoot = self.0 - cursor.start().1.output.lines; let mut offset = cursor.start().1.output.len; if !overshoot.is_zero() { @@ -185,10 +189,10 @@ impl FoldMapWriter<'_> { width: None, }, ); - new_tree.append(cursor.slice(&fold.range, Bias::Right, buffer), buffer); + new_tree.append(cursor.slice(&fold.range, Bias::Right), buffer); new_tree.push(fold, buffer); } - new_tree.append(cursor.suffix(buffer), buffer); + new_tree.append(cursor.suffix(), buffer); new_tree }; @@ -250,7 +254,7 @@ impl FoldMapWriter<'_> { fold_ixs_to_delete.push(*folds_cursor.start()); self.0.snapshot.fold_metadata_by_id.remove(&fold.id); } - folds_cursor.next(buffer); + folds_cursor.next(); } } @@ -261,10 +265,10 @@ impl FoldMapWriter<'_> { let mut cursor = self.0.snapshot.folds.cursor::(buffer); let mut folds = SumTree::new(buffer); for fold_ix in fold_ixs_to_delete { - folds.append(cursor.slice(&fold_ix, Bias::Right, buffer), buffer); - cursor.next(buffer); + folds.append(cursor.slice(&fold_ix, Bias::Right), buffer); + cursor.next(); } - folds.append(cursor.suffix(buffer), buffer); + folds.append(cursor.suffix(), buffer); folds }; @@ -275,32 +279,35 @@ impl FoldMapWriter<'_> { pub(crate) fn update_fold_widths( &mut self, - new_widths: impl IntoIterator, + new_widths: impl IntoIterator, ) -> (FoldSnapshot, Vec) { let mut edits = Vec::new(); let inlay_snapshot = self.0.snapshot.inlay_snapshot.clone(); let buffer = &inlay_snapshot.buffer; for (id, new_width) in new_widths { - if let Some(metadata) = self.0.snapshot.fold_metadata_by_id.get(&id).cloned() { - if Some(new_width) != metadata.width { - let buffer_start = metadata.range.start.to_offset(buffer); - let buffer_end = metadata.range.end.to_offset(buffer); - let inlay_range = inlay_snapshot.to_inlay_offset(buffer_start) - ..inlay_snapshot.to_inlay_offset(buffer_end); - edits.push(InlayEdit { - old: inlay_range.clone(), - new: inlay_range.clone(), - }); + let ChunkRendererId::Fold(id) = id else { + continue; + }; + if let Some(metadata) = self.0.snapshot.fold_metadata_by_id.get(&id).cloned() + && Some(new_width) != metadata.width + { + let buffer_start = metadata.range.start.to_offset(buffer); + let buffer_end = metadata.range.end.to_offset(buffer); + let inlay_range = inlay_snapshot.to_inlay_offset(buffer_start) + ..inlay_snapshot.to_inlay_offset(buffer_end); + edits.push(InlayEdit { + old: inlay_range.clone(), + new: inlay_range.clone(), + }); - self.0.snapshot.fold_metadata_by_id.insert( - id, - FoldMetadata { - range: metadata.range, - width: Some(new_width), - }, - ); - } + self.0.snapshot.fold_metadata_by_id.insert( + id, + FoldMetadata { + range: metadata.range, + width: Some(new_width), + }, + ); } } @@ -357,7 +364,7 @@ impl FoldMap { &mut self, inlay_snapshot: InlaySnapshot, edits: Vec, - ) -> (FoldMapWriter, FoldSnapshot, Vec) { + ) -> (FoldMapWriter<'_>, FoldSnapshot, Vec) { let (snapshot, edits) = self.read(inlay_snapshot, edits); (FoldMapWriter(self), snapshot, edits) } @@ -407,28 +414,28 @@ impl FoldMap { let mut new_transforms = SumTree::::default(); let mut cursor = self.snapshot.transforms.cursor::(&()); - cursor.seek(&InlayOffset(0), Bias::Right, &()); + cursor.seek(&InlayOffset(0), Bias::Right); while let Some(mut edit) = inlay_edits_iter.next() { - if let Some(item) = cursor.item() { - if !item.is_fold() { - new_transforms.update_last( - |transform| { - if !transform.is_fold() { - transform.summary.add_summary(&item.summary, &()); - cursor.next(&()); - } - }, - &(), - ); - } + if let Some(item) = cursor.item() + && !item.is_fold() + { + new_transforms.update_last( + |transform| { + if !transform.is_fold() { + transform.summary.add_summary(&item.summary, &()); + cursor.next(); + } + }, + &(), + ); } - new_transforms.append(cursor.slice(&edit.old.start, Bias::Left, &()), &()); + new_transforms.append(cursor.slice(&edit.old.start, Bias::Left), &()); edit.new.start -= edit.old.start - *cursor.start(); edit.old.start = *cursor.start(); - cursor.seek(&edit.old.end, Bias::Right, &()); - cursor.next(&()); + cursor.seek(&edit.old.end, Bias::Right); + cursor.next(); let mut delta = edit.new_len().0 as isize - edit.old_len().0 as isize; loop { @@ -444,8 +451,8 @@ impl FoldMap { if next_edit.old.end >= edit.old.end { edit.old.end = next_edit.old.end; - cursor.seek(&edit.old.end, Bias::Right, &()); - cursor.next(&()); + cursor.seek(&edit.old.end, Bias::Right); + cursor.next(); } } else { break; @@ -462,11 +469,7 @@ impl FoldMap { .snapshot .folds .cursor::(&inlay_snapshot.buffer); - folds_cursor.seek( - &FoldRange(anchor..Anchor::max()), - Bias::Left, - &inlay_snapshot.buffer, - ); + folds_cursor.seek(&FoldRange(anchor..Anchor::max()), Bias::Left); let mut folds = iter::from_fn({ let inlay_snapshot = &inlay_snapshot; @@ -480,7 +483,7 @@ impl FoldMap { ..inlay_snapshot.to_inlay_offset(buffer_end), ) }); - folds_cursor.next(&inlay_snapshot.buffer); + folds_cursor.next(); item } }) @@ -488,14 +491,14 @@ impl FoldMap { while folds .peek() - .map_or(false, |(_, fold_range)| fold_range.start < edit.new.end) + .is_some_and(|(_, fold_range)| fold_range.start < edit.new.end) { let (fold, mut fold_range) = folds.next().unwrap(); let sum = new_transforms.summary(); assert!(fold_range.start.0 >= sum.input.len); - while folds.peek().map_or(false, |(next_fold, next_fold_range)| { + while folds.peek().is_some_and(|(next_fold, next_fold_range)| { next_fold_range.start < fold_range.end || (next_fold_range.start == fold_range.end && fold.placeholder.merge_adjacent @@ -527,7 +530,7 @@ impl FoldMap { placeholder: Some(TransformPlaceholder { text: ELLIPSIS, renderer: ChunkRenderer { - id: fold.id, + id: ChunkRendererId::Fold(fold.id), render: Arc::new(move |cx| { (fold.placeholder.render)( fold_id, @@ -553,7 +556,7 @@ impl FoldMap { } } - new_transforms.append(cursor.suffix(&()), &()); + new_transforms.append(cursor.suffix(), &()); if new_transforms.is_empty() { let text_summary = inlay_snapshot.text_summary(); push_isomorphic(&mut new_transforms, text_summary); @@ -566,35 +569,36 @@ impl FoldMap { let mut old_transforms = self .snapshot .transforms - .cursor::<(InlayOffset, FoldOffset)>(&()); - let mut new_transforms = new_transforms.cursor::<(InlayOffset, FoldOffset)>(&()); + .cursor::>(&()); + let mut new_transforms = + new_transforms.cursor::>(&()); for mut edit in inlay_edits { - old_transforms.seek(&edit.old.start, Bias::Left, &()); - if old_transforms.item().map_or(false, |t| t.is_fold()) { + old_transforms.seek(&edit.old.start, Bias::Left); + if old_transforms.item().is_some_and(|t| t.is_fold()) { edit.old.start = old_transforms.start().0; } let old_start = old_transforms.start().1.0 + (edit.old.start - old_transforms.start().0).0; - old_transforms.seek_forward(&edit.old.end, Bias::Right, &()); - if old_transforms.item().map_or(false, |t| t.is_fold()) { - old_transforms.next(&()); + old_transforms.seek_forward(&edit.old.end, Bias::Right); + if old_transforms.item().is_some_and(|t| t.is_fold()) { + old_transforms.next(); edit.old.end = old_transforms.start().0; } let old_end = old_transforms.start().1.0 + (edit.old.end - old_transforms.start().0).0; - new_transforms.seek(&edit.new.start, Bias::Left, &()); - if new_transforms.item().map_or(false, |t| t.is_fold()) { + new_transforms.seek(&edit.new.start, Bias::Left); + if new_transforms.item().is_some_and(|t| t.is_fold()) { edit.new.start = new_transforms.start().0; } let new_start = new_transforms.start().1.0 + (edit.new.start - new_transforms.start().0).0; - new_transforms.seek_forward(&edit.new.end, Bias::Right, &()); - if new_transforms.item().map_or(false, |t| t.is_fold()) { - new_transforms.next(&()); + new_transforms.seek_forward(&edit.new.end, Bias::Right); + if new_transforms.item().is_some_and(|t| t.is_fold()) { + new_transforms.next(); edit.new.end = new_transforms.start().0; } let new_end = @@ -650,11 +654,13 @@ impl FoldSnapshot { pub fn text_summary_for_range(&self, range: Range) -> TextSummary { let mut summary = TextSummary::default(); - let mut cursor = self.transforms.cursor::<(FoldPoint, InlayPoint)>(&()); - cursor.seek(&range.start, Bias::Right, &()); + let mut cursor = self + .transforms + .cursor::>(&()); + cursor.seek(&range.start, Bias::Right); if let Some(transform) = cursor.item() { let start_in_transform = range.start.0 - cursor.start().0.0; - let end_in_transform = cmp::min(range.end, cursor.end(&()).0).0 - cursor.start().0.0; + let end_in_transform = cmp::min(range.end, cursor.end().0).0 - cursor.start().0.0; if let Some(placeholder) = transform.placeholder.as_ref() { summary = TextSummary::from( &placeholder.text @@ -673,10 +679,10 @@ impl FoldSnapshot { } } - if range.end > cursor.end(&()).0 { - cursor.next(&()); + if range.end > cursor.end().0 { + cursor.next(); summary += &cursor - .summary::<_, TransformSummary>(&range.end, Bias::Right, &()) + .summary::<_, TransformSummary>(&range.end, Bias::Right) .output; if let Some(transform) = cursor.item() { let end_in_transform = range.end.0 - cursor.start().0.0; @@ -699,20 +705,19 @@ impl FoldSnapshot { } pub fn to_fold_point(&self, point: InlayPoint, bias: Bias) -> FoldPoint { - let mut cursor = self.transforms.cursor::<(InlayPoint, FoldPoint)>(&()); - cursor.seek(&point, Bias::Right, &()); - if cursor.item().map_or(false, |t| t.is_fold()) { + let mut cursor = self + .transforms + .cursor::>(&()); + cursor.seek(&point, Bias::Right); + if cursor.item().is_some_and(|t| t.is_fold()) { if bias == Bias::Left || point == cursor.start().0 { cursor.start().1 } else { - cursor.end(&()).1 + cursor.end().1 } } else { let overshoot = point.0 - cursor.start().0.0; - FoldPoint(cmp::min( - cursor.start().1.0 + overshoot, - cursor.end(&()).1.0, - )) + FoldPoint(cmp::min(cursor.start().1.0 + overshoot, cursor.end().1.0)) } } @@ -730,14 +735,16 @@ impl FoldSnapshot { (line_end - line_start) as u32 } - pub fn row_infos(&self, start_row: u32) -> FoldRows { + pub fn row_infos(&self, start_row: u32) -> FoldRows<'_> { if start_row > self.transforms.summary().output.lines.row { panic!("invalid display row {}", start_row); } let fold_point = FoldPoint::new(start_row, 0); - let mut cursor = self.transforms.cursor::<(FoldPoint, InlayPoint)>(&()); - cursor.seek(&fold_point, Bias::Left, &()); + let mut cursor = self + .transforms + .cursor::>(&()); + cursor.seek(&fold_point, Bias::Left); let overshoot = fold_point.0 - cursor.start().0.0; let inlay_point = InlayPoint(cursor.start().1.0 + overshoot); @@ -768,7 +775,7 @@ impl FoldSnapshot { let mut folds = intersecting_folds(&self.inlay_snapshot, &self.folds, range, false); iter::from_fn(move || { let item = folds.item(); - folds.next(&self.inlay_snapshot.buffer); + folds.next(); item }) } @@ -780,8 +787,8 @@ impl FoldSnapshot { let buffer_offset = offset.to_offset(&self.inlay_snapshot.buffer); let inlay_offset = self.inlay_snapshot.to_inlay_offset(buffer_offset); let mut cursor = self.transforms.cursor::(&()); - cursor.seek(&inlay_offset, Bias::Right, &()); - cursor.item().map_or(false, |t| t.placeholder.is_some()) + cursor.seek(&inlay_offset, Bias::Right); + cursor.item().is_some_and(|t| t.placeholder.is_some()) } pub fn is_line_folded(&self, buffer_row: MultiBufferRow) -> bool { @@ -789,7 +796,7 @@ impl FoldSnapshot { .inlay_snapshot .to_inlay_point(Point::new(buffer_row.0, 0)); let mut cursor = self.transforms.cursor::(&()); - cursor.seek(&inlay_point, Bias::Right, &()); + cursor.seek(&inlay_point, Bias::Right); loop { match cursor.item() { Some(transform) => { @@ -803,11 +810,11 @@ impl FoldSnapshot { None => return false, } - if cursor.end(&()).row() == inlay_point.row() { - cursor.next(&()); + if cursor.end().row() == inlay_point.row() { + cursor.next(); } else { inlay_point.0 += Point::new(1, 0); - cursor.seek(&inlay_point, Bias::Right, &()); + cursor.seek(&inlay_point, Bias::Right); } } } @@ -818,19 +825,21 @@ impl FoldSnapshot { language_aware: bool, highlights: Highlights<'a>, ) -> FoldChunks<'a> { - let mut transform_cursor = self.transforms.cursor::<(FoldOffset, InlayOffset)>(&()); - transform_cursor.seek(&range.start, Bias::Right, &()); + let mut transform_cursor = self + .transforms + .cursor::>(&()); + transform_cursor.seek(&range.start, Bias::Right); let inlay_start = { let overshoot = range.start.0 - transform_cursor.start().0.0; transform_cursor.start().1 + InlayOffset(overshoot) }; - let transform_end = transform_cursor.end(&()); + let transform_end = transform_cursor.end(); let inlay_end = if transform_cursor .item() - .map_or(true, |transform| transform.is_fold()) + .is_none_or(|transform| transform.is_fold()) { inlay_start } else if range.end < transform_end.0 { @@ -873,15 +882,17 @@ impl FoldSnapshot { } pub fn clip_point(&self, point: FoldPoint, bias: Bias) -> FoldPoint { - let mut cursor = self.transforms.cursor::<(FoldPoint, InlayPoint)>(&()); - cursor.seek(&point, Bias::Right, &()); + let mut cursor = self + .transforms + .cursor::>(&()); + cursor.seek(&point, Bias::Right); if let Some(transform) = cursor.item() { let transform_start = cursor.start().0.0; if transform.placeholder.is_some() { if point.0 == transform_start || matches!(bias, Bias::Left) { FoldPoint(transform_start) } else { - FoldPoint(cursor.end(&()).0.0) + FoldPoint(cursor.end().0.0) } } else { let overshoot = InlayPoint(point.0 - transform_start); @@ -940,7 +951,7 @@ fn intersecting_folds<'a>( start_cmp == Ordering::Less && end_cmp == Ordering::Greater } }); - cursor.next(buffer); + cursor.next(); cursor } @@ -1060,7 +1071,7 @@ impl sum_tree::Summary for TransformSummary { } #[derive(Copy, Clone, Eq, PartialEq, Debug, Default, Ord, PartialOrd, Hash)] -pub struct FoldId(usize); +pub struct FoldId(pub(super) usize); impl From for ElementId { fn from(val: FoldId) -> Self { @@ -1198,7 +1209,7 @@ impl<'a> sum_tree::Dimension<'a, FoldSummary> for usize { #[derive(Clone)] pub struct FoldRows<'a> { - cursor: Cursor<'a, Transform, (FoldPoint, InlayPoint)>, + cursor: Cursor<'a, Transform, Dimensions>, input_rows: InlayBufferRows<'a>, fold_point: FoldPoint, } @@ -1206,7 +1217,7 @@ pub struct FoldRows<'a> { impl FoldRows<'_> { pub(crate) fn seek(&mut self, row: u32) { let fold_point = FoldPoint::new(row, 0); - self.cursor.seek(&fold_point, Bias::Left, &()); + self.cursor.seek(&fold_point, Bias::Left); let overshoot = fold_point.0 - self.cursor.start().0.0; let inlay_point = InlayPoint(self.cursor.start().1.0 + overshoot); self.input_rows.seek(inlay_point.row()); @@ -1219,8 +1230,8 @@ impl Iterator for FoldRows<'_> { fn next(&mut self) -> Option { let mut traversed_fold = false; - while self.fold_point > self.cursor.end(&()).0 { - self.cursor.next(&()); + while self.fold_point > self.cursor.end().0 { + self.cursor.next(); traversed_fold = true; if self.cursor.item().is_none() { break; @@ -1259,15 +1270,23 @@ pub struct Chunk<'a> { pub underline: bool, /// Whether this chunk of text was originally a tab character. pub is_tab: bool, + /// Whether this chunk of text was originally a tab character. + pub is_inlay: bool, /// An optional recipe for how the chunk should be presented. pub renderer: Option, } +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum ChunkRendererId { + Fold(FoldId), + Inlay(InlayId), +} + /// A recipe for how the chunk should be presented. #[derive(Clone)] pub struct ChunkRenderer { - /// The id of the fold associated with this chunk. - pub id: FoldId, + /// The id of the renderer associated with this chunk. + pub id: ChunkRendererId, /// Creates a custom element to represent this chunk. pub render: Arc AnyElement>, /// If true, the element is constrained to the shaped width of the text. @@ -1307,9 +1326,9 @@ impl DerefMut for ChunkRendererContext<'_, '_> { } pub struct FoldChunks<'a> { - transform_cursor: Cursor<'a, Transform, (FoldOffset, InlayOffset)>, + transform_cursor: Cursor<'a, Transform, Dimensions>, inlay_chunks: InlayChunks<'a>, - inlay_chunk: Option<(InlayOffset, language::Chunk<'a>)>, + inlay_chunk: Option<(InlayOffset, InlayChunk<'a>)>, inlay_offset: InlayOffset, output_offset: FoldOffset, max_output_offset: FoldOffset, @@ -1317,19 +1336,19 @@ pub struct FoldChunks<'a> { impl FoldChunks<'_> { pub(crate) fn seek(&mut self, range: Range) { - self.transform_cursor.seek(&range.start, Bias::Right, &()); + self.transform_cursor.seek(&range.start, Bias::Right); let inlay_start = { let overshoot = range.start.0 - self.transform_cursor.start().0.0; self.transform_cursor.start().1 + InlayOffset(overshoot) }; - let transform_end = self.transform_cursor.end(&()); + let transform_end = self.transform_cursor.end(); let inlay_end = if self .transform_cursor .item() - .map_or(true, |transform| transform.is_fold()) + .is_none_or(|transform| transform.is_fold()) { inlay_start } else if range.end < transform_end.0 { @@ -1363,10 +1382,10 @@ impl<'a> Iterator for FoldChunks<'a> { self.inlay_chunk.take(); self.inlay_offset += InlayOffset(transform.summary.input.len); - while self.inlay_offset >= self.transform_cursor.end(&()).1 + while self.inlay_offset >= self.transform_cursor.end().1 && self.transform_cursor.item().is_some() { - self.transform_cursor.next(&()); + self.transform_cursor.next(); } self.output_offset.0 += placeholder.text.len(); @@ -1383,7 +1402,7 @@ impl<'a> Iterator for FoldChunks<'a> { && self.inlay_chunks.offset() != self.inlay_offset { let transform_start = self.transform_cursor.start(); - let transform_end = self.transform_cursor.end(&()); + let transform_end = self.transform_cursor.end(); let inlay_end = if self.max_output_offset < transform_end.0 { let overshoot = self.max_output_offset.0 - transform_start.0.0; transform_start.1 + InlayOffset(overshoot) @@ -1401,16 +1420,17 @@ impl<'a> Iterator for FoldChunks<'a> { } // Otherwise, take a chunk from the buffer's text. - if let Some((buffer_chunk_start, mut chunk)) = self.inlay_chunk.clone() { + if let Some((buffer_chunk_start, mut inlay_chunk)) = self.inlay_chunk.clone() { + let chunk = &mut inlay_chunk.chunk; let buffer_chunk_end = buffer_chunk_start + InlayOffset(chunk.text.len()); - let transform_end = self.transform_cursor.end(&()).1; + let transform_end = self.transform_cursor.end().1; let chunk_end = buffer_chunk_end.min(transform_end); chunk.text = &chunk.text [(self.inlay_offset - buffer_chunk_start).0..(chunk_end - buffer_chunk_start).0]; if chunk_end == transform_end { - self.transform_cursor.next(&()); + self.transform_cursor.next(); } else if chunk_end == buffer_chunk_end { self.inlay_chunk.take(); } @@ -1424,8 +1444,9 @@ impl<'a> Iterator for FoldChunks<'a> { diagnostic_severity: chunk.diagnostic_severity, is_unnecessary: chunk.is_unnecessary, is_tab: chunk.is_tab, + is_inlay: chunk.is_inlay, underline: chunk.underline, - renderer: None, + renderer: inlay_chunk.renderer, }); } @@ -1440,9 +1461,9 @@ impl FoldOffset { pub fn to_point(self, snapshot: &FoldSnapshot) -> FoldPoint { let mut cursor = snapshot .transforms - .cursor::<(FoldOffset, TransformSummary)>(&()); - cursor.seek(&self, Bias::Right, &()); - let overshoot = if cursor.item().map_or(true, |t| t.is_fold()) { + .cursor::>(&()); + cursor.seek(&self, Bias::Right); + let overshoot = if cursor.item().is_none_or(|t| t.is_fold()) { Point::new(0, (self.0 - cursor.start().0.0) as u32) } else { let inlay_offset = cursor.start().1.input.len + self.0 - cursor.start().0.0; @@ -1454,8 +1475,10 @@ impl FoldOffset { #[cfg(test)] pub fn to_inlay_offset(self, snapshot: &FoldSnapshot) -> InlayOffset { - let mut cursor = snapshot.transforms.cursor::<(FoldOffset, InlayOffset)>(&()); - cursor.seek(&self, Bias::Right, &()); + let mut cursor = snapshot + .transforms + .cursor::>(&()); + cursor.seek(&self, Bias::Right); let overshoot = self.0 - cursor.start().0.0; InlayOffset(cursor.start().1.0 + overshoot) } @@ -1534,7 +1557,7 @@ mod tests { let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx); let subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); let buffer_snapshot = buffer.read(cx).snapshot(cx); - let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot); let mut map = FoldMap::new(inlay_snapshot.clone()).0; let (mut writer, _, _) = map.write(inlay_snapshot, vec![]); @@ -1613,7 +1636,7 @@ mod tests { let buffer = MultiBuffer::build_simple("abcdefghijkl", cx); let subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); let buffer_snapshot = buffer.read(cx).snapshot(cx); - let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot); { let mut map = FoldMap::new(inlay_snapshot.clone()).0; @@ -1689,7 +1712,7 @@ mod tests { let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx); let subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); let buffer_snapshot = buffer.read(cx).snapshot(cx); - let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot); let mut map = FoldMap::new(inlay_snapshot.clone()).0; let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]); @@ -1697,7 +1720,7 @@ mod tests { (Point::new(0, 2)..Point::new(2, 2), FoldPlaceholder::test()), (Point::new(3, 1)..Point::new(4, 1), FoldPlaceholder::test()), ]); - let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]); + let (snapshot, _) = map.read(inlay_snapshot, vec![]); assert_eq!(snapshot.text(), "aa⋯cccc\nd⋯eeeee"); let buffer_snapshot = buffer.update(cx, |buffer, cx| { @@ -1724,7 +1747,7 @@ mod tests { (Point::new(1, 2)..Point::new(3, 2), FoldPlaceholder::test()), (Point::new(3, 1)..Point::new(4, 1), FoldPlaceholder::test()), ]); - let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]); + let (snapshot, _) = map.read(inlay_snapshot, vec![]); let fold_ranges = snapshot .folds_in_range(Point::new(1, 0)..Point::new(1, 3)) .map(|fold| { @@ -1759,7 +1782,7 @@ mod tests { let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); let mut map = FoldMap::new(inlay_snapshot.clone()).0; - let (mut initial_snapshot, _) = map.read(inlay_snapshot.clone(), vec![]); + let (mut initial_snapshot, _) = map.read(inlay_snapshot, vec![]); let mut snapshot_edits = Vec::new(); let mut next_inlay_id = 0; diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index ec3bc4865c..3db9d10fdc 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -1,5 +1,6 @@ -use crate::{HighlightStyles, InlayId}; +use crate::{ChunkRenderer, HighlightStyles, InlayId}; use collections::BTreeSet; +use gpui::{Hsla, Rgba}; use language::{Chunk, Edit, Point, TextSummary}; use multi_buffer::{ Anchor, MultiBufferRow, MultiBufferRows, MultiBufferSnapshot, RowInfo, ToOffset, @@ -7,11 +8,13 @@ use multi_buffer::{ use std::{ cmp, ops::{Add, AddAssign, Range, Sub, SubAssign}, + sync::Arc, }; -use sum_tree::{Bias, Cursor, SumTree}; +use sum_tree::{Bias, Cursor, Dimensions, SumTree}; use text::{Patch, Rope}; +use ui::{ActiveTheme, IntoElement as _, ParentElement as _, Styled as _, div}; -use super::{Highlights, custom_highlights::CustomHighlightsChunks}; +use super::{Highlights, custom_highlights::CustomHighlightsChunks, fold_map::ChunkRendererId}; /// Decides where the [`Inlay`]s should be displayed. /// @@ -39,39 +42,67 @@ pub struct Inlay { pub id: InlayId, pub position: Anchor, pub text: text::Rope, + color: Option, } impl Inlay { pub fn hint(id: usize, position: Anchor, hint: &project::InlayHint) -> Self { let mut text = hint.text(); - if hint.padding_right && !text.ends_with(' ') { - text.push(' '); + if hint.padding_right && text.reversed_chars_at(text.len()).next() != Some(' ') { + text.push(" "); } - if hint.padding_left && !text.starts_with(' ') { - text.insert(0, ' '); + if hint.padding_left && text.chars_at(0).next() != Some(' ') { + text.push_front(" "); } Self { id: InlayId::Hint(id), position, - text: text.into(), + text, + color: None, } } - pub fn inline_completion>(id: usize, position: Anchor, text: T) -> Self { + #[cfg(any(test, feature = "test-support"))] + pub fn mock_hint(id: usize, position: Anchor, text: impl Into) -> Self { Self { - id: InlayId::InlineCompletion(id), + id: InlayId::Hint(id), position, text: text.into(), + color: None, } } - pub fn debugger_hint>(id: usize, position: Anchor, text: T) -> Self { + pub fn color(id: usize, position: Anchor, color: Rgba) -> Self { + Self { + id: InlayId::Color(id), + position, + text: Rope::from("◼"), + color: Some(Hsla::from(color)), + } + } + + pub fn edit_prediction>(id: usize, position: Anchor, text: T) -> Self { + Self { + id: InlayId::EditPrediction(id), + position, + text: text.into(), + color: None, + } + } + + pub fn debugger>(id: usize, position: Anchor, text: T) -> Self { Self { id: InlayId::DebuggerValue(id), position, text: text.into(), + color: None, } } + + #[cfg(any(test, feature = "test-support"))] + pub fn get_color(&self) -> Option { + self.color + } } impl sum_tree::Item for Transform { @@ -204,14 +235,14 @@ impl<'a> sum_tree::Dimension<'a, TransformSummary> for Point { #[derive(Clone)] pub struct InlayBufferRows<'a> { - transforms: Cursor<'a, Transform, (InlayPoint, Point)>, + transforms: Cursor<'a, Transform, Dimensions>, buffer_rows: MultiBufferRows<'a>, inlay_row: u32, max_buffer_row: MultiBufferRow, } pub struct InlayChunks<'a> { - transforms: Cursor<'a, Transform, (InlayOffset, usize)>, + transforms: Cursor<'a, Transform, Dimensions>, buffer_chunks: CustomHighlightsChunks<'a>, buffer_chunk: Option>, inlay_chunks: Option>, @@ -223,9 +254,16 @@ pub struct InlayChunks<'a> { snapshot: &'a InlaySnapshot, } +#[derive(Clone)] +pub struct InlayChunk<'a> { + pub chunk: Chunk<'a>, + /// Whether the inlay should be customly rendered. + pub renderer: Option, +} + impl InlayChunks<'_> { pub fn seek(&mut self, new_range: Range) { - self.transforms.seek(&new_range.start, Bias::Right, &()); + self.transforms.seek(&new_range.start, Bias::Right); let buffer_range = self.snapshot.to_buffer_offset(new_range.start) ..self.snapshot.to_buffer_offset(new_range.end); @@ -242,7 +280,7 @@ impl InlayChunks<'_> { } impl<'a> Iterator for InlayChunks<'a> { - type Item = Chunk<'a>; + type Item = InlayChunk<'a>; fn next(&mut self) -> Option { if self.output_offset == self.max_output_offset { @@ -258,18 +296,34 @@ impl<'a> Iterator for InlayChunks<'a> { *chunk = self.buffer_chunks.next().unwrap(); } - let (prefix, suffix) = chunk.text.split_at( - chunk - .text - .len() - .min(self.transforms.end(&()).0.0 - self.output_offset.0), - ); + let desired_bytes = self.transforms.end().0.0 - self.output_offset.0; + + // If we're already at the transform boundary, skip to the next transform + if desired_bytes == 0 { + self.inlay_chunks = None; + self.transforms.next(); + return self.next(); + } + + // Determine split index handling edge cases + let split_index = if desired_bytes >= chunk.text.len() { + chunk.text.len() + } else if chunk.text.is_char_boundary(desired_bytes) { + desired_bytes + } else { + find_next_utf8_boundary(chunk.text, desired_bytes) + }; + + let (prefix, suffix) = chunk.text.split_at(split_index); chunk.text = suffix; self.output_offset.0 += prefix.len(); - Chunk { - text: prefix, - ..chunk.clone() + InlayChunk { + chunk: Chunk { + text: prefix, + ..chunk.clone() + }, + renderer: None, } } Transform::Inlay(inlay) => { @@ -284,18 +338,42 @@ impl<'a> Iterator for InlayChunks<'a> { } } + let mut renderer = None; let mut highlight_style = match inlay.id { - InlayId::InlineCompletion(_) => { - self.highlight_styles.inline_completion.map(|s| { - if inlay.text.chars().all(|c| c.is_whitespace()) { - s.whitespace - } else { - s.insertion - } - }) - } + InlayId::EditPrediction(_) => self.highlight_styles.edit_prediction.map(|s| { + if inlay.text.chars().all(|c| c.is_whitespace()) { + s.whitespace + } else { + s.insertion + } + }), InlayId::Hint(_) => self.highlight_styles.inlay_hint, InlayId::DebuggerValue(_) => self.highlight_styles.inlay_hint, + InlayId::Color(_) => { + if let Some(color) = inlay.color { + renderer = Some(ChunkRenderer { + id: ChunkRendererId::Inlay(inlay.id), + render: Arc::new(move |cx| { + div() + .relative() + .size_3p5() + .child( + div() + .absolute() + .right_1() + .size_3() + .border_1() + .border_color(cx.theme().colors().border) + .bg(color), + ) + .into_any_element() + }), + constrain_width: false, + measured_width: None, + }); + } + self.highlight_styles.inlay_hint + } }; let next_inlay_highlight_endpoint; let offset_in_inlay = self.output_offset - self.transforms.start().0; @@ -317,15 +395,31 @@ impl<'a> Iterator for InlayChunks<'a> { let inlay_chunks = self.inlay_chunks.get_or_insert_with(|| { let start = offset_in_inlay; - let end = cmp::min(self.max_output_offset, self.transforms.end(&()).0) + let end = cmp::min(self.max_output_offset, self.transforms.end().0) - self.transforms.start().0; inlay.text.chunks_in_range(start.0..end.0) }); let inlay_chunk = self .inlay_chunk .get_or_insert_with(|| inlay_chunks.next().unwrap()); - let (chunk, remainder) = - inlay_chunk.split_at(inlay_chunk.len().min(next_inlay_highlight_endpoint)); + + // Determine split index handling edge cases + let split_index = if next_inlay_highlight_endpoint >= inlay_chunk.len() { + inlay_chunk.len() + } else if next_inlay_highlight_endpoint == 0 { + // Need to take at least one character to make progress + inlay_chunk + .chars() + .next() + .map(|c| c.len_utf8()) + .unwrap_or(1) + } else if inlay_chunk.is_char_boundary(next_inlay_highlight_endpoint) { + next_inlay_highlight_endpoint + } else { + find_next_utf8_boundary(inlay_chunk, next_inlay_highlight_endpoint) + }; + + let (chunk, remainder) = inlay_chunk.split_at(split_index); *inlay_chunk = remainder; if inlay_chunk.is_empty() { self.inlay_chunk = None; @@ -333,17 +427,21 @@ impl<'a> Iterator for InlayChunks<'a> { self.output_offset.0 += chunk.len(); - Chunk { - text: chunk, - highlight_style, - ..Default::default() + InlayChunk { + chunk: Chunk { + text: chunk, + highlight_style, + is_inlay: true, + ..Chunk::default() + }, + renderer, } } }; - if self.output_offset == self.transforms.end(&()).0 { + if self.output_offset >= self.transforms.end().0 { self.inlay_chunks = None; - self.transforms.next(&()); + self.transforms.next(); } Some(chunk) @@ -353,7 +451,7 @@ impl<'a> Iterator for InlayChunks<'a> { impl InlayBufferRows<'_> { pub fn seek(&mut self, row: u32) { let inlay_point = InlayPoint::new(row, 0); - self.transforms.seek(&inlay_point, Bias::Left, &()); + self.transforms.seek(&inlay_point, Bias::Left); let mut buffer_point = self.transforms.start().1; let buffer_row = MultiBufferRow(if row == 0 { @@ -387,7 +485,7 @@ impl Iterator for InlayBufferRows<'_> { self.inlay_row += 1; self.transforms - .seek_forward(&InlayPoint::new(self.inlay_row, 0), Bias::Left, &()); + .seek_forward(&InlayPoint::new(self.inlay_row, 0), Bias::Left); Some(buffer_row) } @@ -453,21 +551,23 @@ impl InlayMap { } else { let mut inlay_edits = Patch::default(); let mut new_transforms = SumTree::default(); - let mut cursor = snapshot.transforms.cursor::<(usize, InlayOffset)>(&()); + let mut cursor = snapshot + .transforms + .cursor::>(&()); let mut buffer_edits_iter = buffer_edits.iter().peekable(); while let Some(buffer_edit) = buffer_edits_iter.next() { - new_transforms.append(cursor.slice(&buffer_edit.old.start, Bias::Left, &()), &()); - if let Some(Transform::Isomorphic(transform)) = cursor.item() { - if cursor.end(&()).0 == buffer_edit.old.start { - push_isomorphic(&mut new_transforms, *transform); - cursor.next(&()); - } + new_transforms.append(cursor.slice(&buffer_edit.old.start, Bias::Left), &()); + if let Some(Transform::Isomorphic(transform)) = cursor.item() + && cursor.end().0 == buffer_edit.old.start + { + push_isomorphic(&mut new_transforms, *transform); + cursor.next(); } // Remove all the inlays and transforms contained by the edit. let old_start = cursor.start().1 + InlayOffset(buffer_edit.old.start - cursor.start().0); - cursor.seek(&buffer_edit.old.end, Bias::Right, &()); + cursor.seek(&buffer_edit.old.end, Bias::Right); let old_end = cursor.start().1 + InlayOffset(buffer_edit.old.end - cursor.start().0); @@ -525,20 +625,20 @@ impl InlayMap { // we can push its remainder. if buffer_edits_iter .peek() - .map_or(true, |edit| edit.old.start >= cursor.end(&()).0) + .is_none_or(|edit| edit.old.start >= cursor.end().0) { let transform_start = new_transforms.summary().input.len; let transform_end = - buffer_edit.new.end + (cursor.end(&()).0 - buffer_edit.old.end); + buffer_edit.new.end + (cursor.end().0 - buffer_edit.old.end); push_isomorphic( &mut new_transforms, buffer_snapshot.text_summary_for_range(transform_start..transform_end), ); - cursor.next(&()); + cursor.next(); } } - new_transforms.append(cursor.suffix(&()), &()); + new_transforms.append(cursor.suffix(), &()); if new_transforms.is_empty() { new_transforms.push(Transform::Isomorphic(Default::default()), &()); } @@ -633,24 +733,24 @@ impl InlayMap { .take(len) .collect::(); - let inlay_id = if i % 2 == 0 { - InlayId::Hint(post_inc(next_inlay_id)) + let next_inlay = if i % 2 == 0 { + Inlay::mock_hint( + post_inc(next_inlay_id), + snapshot.buffer.anchor_at(position, bias), + &text, + ) } else { - InlayId::InlineCompletion(post_inc(next_inlay_id)) + Inlay::edit_prediction( + post_inc(next_inlay_id), + snapshot.buffer.anchor_at(position, bias), + &text, + ) }; + let inlay_id = next_inlay.id; log::info!( - "creating inlay {:?} at buffer offset {} with bias {:?} and text {:?}", - inlay_id, - position, - bias, - text + "creating inlay {inlay_id:?} at buffer offset {position} with bias {bias:?} and text {text:?}" ); - - to_insert.push(Inlay { - id: inlay_id, - position: snapshot.buffer.anchor_at(position, bias), - text: text.into(), - }); + to_insert.push(next_inlay); } else { to_remove.push( self.inlays @@ -672,20 +772,20 @@ impl InlaySnapshot { pub fn to_point(&self, offset: InlayOffset) -> InlayPoint { let mut cursor = self .transforms - .cursor::<(InlayOffset, (InlayPoint, usize))>(&()); - cursor.seek(&offset, Bias::Right, &()); + .cursor::>(&()); + cursor.seek(&offset, Bias::Right); let overshoot = offset.0 - cursor.start().0.0; match cursor.item() { Some(Transform::Isomorphic(_)) => { - let buffer_offset_start = cursor.start().1.1; + let buffer_offset_start = cursor.start().2; let buffer_offset_end = buffer_offset_start + overshoot; let buffer_start = self.buffer.offset_to_point(buffer_offset_start); let buffer_end = self.buffer.offset_to_point(buffer_offset_end); - InlayPoint(cursor.start().1.0.0 + (buffer_end - buffer_start)) + InlayPoint(cursor.start().1.0 + (buffer_end - buffer_start)) } Some(Transform::Inlay(inlay)) => { let overshoot = inlay.text.offset_to_point(overshoot); - InlayPoint(cursor.start().1.0.0 + overshoot) + InlayPoint(cursor.start().1.0 + overshoot) } None => self.max_point(), } @@ -702,27 +802,27 @@ impl InlaySnapshot { pub fn to_offset(&self, point: InlayPoint) -> InlayOffset { let mut cursor = self .transforms - .cursor::<(InlayPoint, (InlayOffset, Point))>(&()); - cursor.seek(&point, Bias::Right, &()); + .cursor::>(&()); + cursor.seek(&point, Bias::Right); let overshoot = point.0 - cursor.start().0.0; match cursor.item() { Some(Transform::Isomorphic(_)) => { - let buffer_point_start = cursor.start().1.1; + let buffer_point_start = cursor.start().2; let buffer_point_end = buffer_point_start + overshoot; let buffer_offset_start = self.buffer.point_to_offset(buffer_point_start); let buffer_offset_end = self.buffer.point_to_offset(buffer_point_end); - InlayOffset(cursor.start().1.0.0 + (buffer_offset_end - buffer_offset_start)) + InlayOffset(cursor.start().1.0 + (buffer_offset_end - buffer_offset_start)) } Some(Transform::Inlay(inlay)) => { let overshoot = inlay.text.point_to_offset(overshoot); - InlayOffset(cursor.start().1.0.0 + overshoot) + InlayOffset(cursor.start().1.0 + overshoot) } None => self.len(), } } pub fn to_buffer_point(&self, point: InlayPoint) -> Point { - let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>(&()); - cursor.seek(&point, Bias::Right, &()); + let mut cursor = self.transforms.cursor::>(&()); + cursor.seek(&point, Bias::Right); match cursor.item() { Some(Transform::Isomorphic(_)) => { let overshoot = point.0 - cursor.start().0.0; @@ -733,8 +833,10 @@ impl InlaySnapshot { } } pub fn to_buffer_offset(&self, offset: InlayOffset) -> usize { - let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>(&()); - cursor.seek(&offset, Bias::Right, &()); + let mut cursor = self + .transforms + .cursor::>(&()); + cursor.seek(&offset, Bias::Right); match cursor.item() { Some(Transform::Isomorphic(_)) => { let overshoot = offset - cursor.start().0; @@ -746,20 +848,22 @@ impl InlaySnapshot { } pub fn to_inlay_offset(&self, offset: usize) -> InlayOffset { - let mut cursor = self.transforms.cursor::<(usize, InlayOffset)>(&()); - cursor.seek(&offset, Bias::Left, &()); + let mut cursor = self + .transforms + .cursor::>(&()); + cursor.seek(&offset, Bias::Left); loop { match cursor.item() { Some(Transform::Isomorphic(_)) => { - if offset == cursor.end(&()).0 { + if offset == cursor.end().0 { while let Some(Transform::Inlay(inlay)) = cursor.next_item() { if inlay.position.bias() == Bias::Right { break; } else { - cursor.next(&()); + cursor.next(); } } - return cursor.end(&()).1; + return cursor.end().1; } else { let overshoot = offset - cursor.start().0; return InlayOffset(cursor.start().1.0 + overshoot); @@ -767,7 +871,7 @@ impl InlaySnapshot { } Some(Transform::Inlay(inlay)) => { if inlay.position.bias() == Bias::Left { - cursor.next(&()); + cursor.next(); } else { return cursor.start().1; } @@ -779,20 +883,20 @@ impl InlaySnapshot { } } pub fn to_inlay_point(&self, point: Point) -> InlayPoint { - let mut cursor = self.transforms.cursor::<(Point, InlayPoint)>(&()); - cursor.seek(&point, Bias::Left, &()); + let mut cursor = self.transforms.cursor::>(&()); + cursor.seek(&point, Bias::Left); loop { match cursor.item() { Some(Transform::Isomorphic(_)) => { - if point == cursor.end(&()).0 { + if point == cursor.end().0 { while let Some(Transform::Inlay(inlay)) = cursor.next_item() { if inlay.position.bias() == Bias::Right { break; } else { - cursor.next(&()); + cursor.next(); } } - return cursor.end(&()).1; + return cursor.end().1; } else { let overshoot = point - cursor.start().0; return InlayPoint(cursor.start().1.0 + overshoot); @@ -800,7 +904,7 @@ impl InlaySnapshot { } Some(Transform::Inlay(inlay)) => { if inlay.position.bias() == Bias::Left { - cursor.next(&()); + cursor.next(); } else { return cursor.start().1; } @@ -813,8 +917,8 @@ impl InlaySnapshot { } pub fn clip_point(&self, mut point: InlayPoint, mut bias: Bias) -> InlayPoint { - let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>(&()); - cursor.seek(&point, Bias::Left, &()); + let mut cursor = self.transforms.cursor::>(&()); + cursor.seek(&point, Bias::Left); loop { match cursor.item() { Some(Transform::Isomorphic(transform)) => { @@ -823,7 +927,7 @@ impl InlaySnapshot { if inlay.position.bias() == Bias::Left { return point; } else if bias == Bias::Left { - cursor.prev(&()); + cursor.prev(); } else if transform.first_line_chars == 0 { point.0 += Point::new(1, 0); } else { @@ -832,12 +936,12 @@ impl InlaySnapshot { } else { return point; } - } else if cursor.end(&()).0 == point { + } else if cursor.end().0 == point { if let Some(Transform::Inlay(inlay)) = cursor.next_item() { if inlay.position.bias() == Bias::Right { return point; } else if bias == Bias::Right { - cursor.next(&()); + cursor.next(); } else if point.0.column == 0 { point.0.row -= 1; point.0.column = self.line_len(point.0.row); @@ -870,7 +974,7 @@ impl InlaySnapshot { } _ => return point, } - } else if point == cursor.end(&()).0 && inlay.position.bias() == Bias::Left { + } else if point == cursor.end().0 && inlay.position.bias() == Bias::Left { match cursor.next_item() { Some(Transform::Inlay(inlay)) => { if inlay.position.bias() == Bias::Right { @@ -883,9 +987,9 @@ impl InlaySnapshot { if bias == Bias::Left { point = cursor.start().0; - cursor.prev(&()); + cursor.prev(); } else { - cursor.next(&()); + cursor.next(); point = cursor.start().0; } } @@ -893,9 +997,9 @@ impl InlaySnapshot { bias = bias.invert(); if bias == Bias::Left { point = cursor.start().0; - cursor.prev(&()); + cursor.prev(); } else { - cursor.next(&()); + cursor.next(); point = cursor.start().0; } } @@ -910,8 +1014,10 @@ impl InlaySnapshot { pub fn text_summary_for_range(&self, range: Range) -> TextSummary { let mut summary = TextSummary::default(); - let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>(&()); - cursor.seek(&range.start, Bias::Right, &()); + let mut cursor = self + .transforms + .cursor::>(&()); + cursor.seek(&range.start, Bias::Right); let overshoot = range.start.0 - cursor.start().0.0; match cursor.item() { @@ -919,22 +1025,22 @@ impl InlaySnapshot { let buffer_start = cursor.start().1; let suffix_start = buffer_start + overshoot; let suffix_end = - buffer_start + (cmp::min(cursor.end(&()).0, range.end).0 - cursor.start().0.0); + buffer_start + (cmp::min(cursor.end().0, range.end).0 - cursor.start().0.0); summary = self.buffer.text_summary_for_range(suffix_start..suffix_end); - cursor.next(&()); + cursor.next(); } Some(Transform::Inlay(inlay)) => { let suffix_start = overshoot; - let suffix_end = cmp::min(cursor.end(&()).0, range.end).0 - cursor.start().0.0; + let suffix_end = cmp::min(cursor.end().0, range.end).0 - cursor.start().0.0; summary = inlay.text.cursor(suffix_start).summary(suffix_end); - cursor.next(&()); + cursor.next(); } None => {} } if range.end > cursor.start().0 { summary += cursor - .summary::<_, TransformSummary>(&range.end, Bias::Right, &()) + .summary::<_, TransformSummary>(&range.end, Bias::Right) .output; let overshoot = range.end.0 - cursor.start().0.0; @@ -958,9 +1064,9 @@ impl InlaySnapshot { } pub fn row_infos(&self, row: u32) -> InlayBufferRows<'_> { - let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>(&()); + let mut cursor = self.transforms.cursor::>(&()); let inlay_point = InlayPoint::new(row, 0); - cursor.seek(&inlay_point, Bias::Left, &()); + cursor.seek(&inlay_point, Bias::Left); let max_buffer_row = self.buffer.max_row(); let mut buffer_point = cursor.start().1; @@ -1000,8 +1106,10 @@ impl InlaySnapshot { language_aware: bool, highlights: Highlights<'a>, ) -> InlayChunks<'a> { - let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>(&()); - cursor.seek(&range.start, Bias::Right, &()); + let mut cursor = self + .transforms + .cursor::>(&()); + cursor.seek(&range.start, Bias::Right); let buffer_range = self.to_buffer_offset(range.start)..self.to_buffer_offset(range.end); let buffer_chunks = CustomHighlightsChunks::new( @@ -1028,7 +1136,7 @@ impl InlaySnapshot { #[cfg(test)] pub fn text(&self) -> String { self.chunks(Default::default()..self.len(), false, Highlights::default()) - .map(|chunk| chunk.text) + .map(|chunk| chunk.chunk.text) .collect() } @@ -1072,12 +1180,37 @@ fn push_isomorphic(sum_tree: &mut SumTree, summary: TextSummary) { } } +/// Given a byte index that is NOT a UTF-8 boundary, find the next one. +/// Assumes: 0 < byte_index < text.len() and !text.is_char_boundary(byte_index) +#[inline(always)] +fn find_next_utf8_boundary(text: &str, byte_index: usize) -> usize { + let bytes = text.as_bytes(); + let mut idx = byte_index + 1; + + // Scan forward until we find a boundary + while idx < text.len() { + if is_utf8_char_boundary(bytes[idx]) { + return idx; + } + idx += 1; + } + + // Hit the end, return the full length + text.len() +} + +// Private helper function taken from Rust's core::num module (which is both Apache2 and MIT licensed) +const fn is_utf8_char_boundary(byte: u8) -> bool { + // This is bit magic equivalent to: b < 128 || b >= 192 + (byte as i8) >= -0x40 +} + #[cfg(test)] mod tests { use super::*; use crate::{ InlayId, MultiBuffer, - display_map::{InlayHighlights, TextHighlights}, + display_map::{HighlightKey, InlayHighlights, TextHighlights}, hover_links::InlayHighlight, }; use gpui::{App, HighlightStyle}; @@ -1172,6 +1305,29 @@ mod tests { ); } + #[gpui::test] + fn test_inlay_hint_padding_with_multibyte_chars() { + assert_eq!( + Inlay::hint( + 0, + Anchor::min(), + &InlayHint { + label: InlayHintLabel::String("🎨".to_string()), + position: text::Anchor::default(), + padding_left: true, + padding_right: true, + tooltip: None, + kind: None, + resolve_state: ResolveState::Resolved, + }, + ) + .text + .to_string(), + " 🎨 ", + "Should pad single emoji correctly" + ); + } + #[gpui::test] fn test_basic_inlays(cx: &mut App) { let buffer = MultiBuffer::build_simple("abcdefghi", cx); @@ -1182,11 +1338,11 @@ mod tests { let (inlay_snapshot, _) = inlay_map.splice( &[], - vec![Inlay { - id: InlayId::Hint(post_inc(&mut next_inlay_id)), - position: buffer.read(cx).snapshot(cx).anchor_after(3), - text: "|123|".into(), - }], + vec![Inlay::mock_hint( + post_inc(&mut next_inlay_id), + buffer.read(cx).snapshot(cx).anchor_after(3), + "|123|", + )], ); assert_eq!(inlay_snapshot.text(), "abc|123|defghi"); assert_eq!( @@ -1259,16 +1415,16 @@ mod tests { let (inlay_snapshot, _) = inlay_map.splice( &[], vec![ - Inlay { - id: InlayId::Hint(post_inc(&mut next_inlay_id)), - position: buffer.read(cx).snapshot(cx).anchor_before(3), - text: "|123|".into(), - }, - Inlay { - id: InlayId::InlineCompletion(post_inc(&mut next_inlay_id)), - position: buffer.read(cx).snapshot(cx).anchor_after(3), - text: "|456|".into(), - }, + Inlay::mock_hint( + post_inc(&mut next_inlay_id), + buffer.read(cx).snapshot(cx).anchor_before(3), + "|123|", + ), + Inlay::edit_prediction( + post_inc(&mut next_inlay_id), + buffer.read(cx).snapshot(cx).anchor_after(3), + "|456|", + ), ], ); assert_eq!(inlay_snapshot.text(), "abx|123||456|yDzefghi"); @@ -1474,21 +1630,21 @@ mod tests { let (inlay_snapshot, _) = inlay_map.splice( &[], vec![ - Inlay { - id: InlayId::Hint(post_inc(&mut next_inlay_id)), - position: buffer.read(cx).snapshot(cx).anchor_before(0), - text: "|123|\n".into(), - }, - Inlay { - id: InlayId::Hint(post_inc(&mut next_inlay_id)), - position: buffer.read(cx).snapshot(cx).anchor_before(4), - text: "|456|".into(), - }, - Inlay { - id: InlayId::InlineCompletion(post_inc(&mut next_inlay_id)), - position: buffer.read(cx).snapshot(cx).anchor_before(7), - text: "\n|567|\n".into(), - }, + Inlay::mock_hint( + post_inc(&mut next_inlay_id), + buffer.read(cx).snapshot(cx).anchor_before(0), + "|123|\n", + ), + Inlay::mock_hint( + post_inc(&mut next_inlay_id), + buffer.read(cx).snapshot(cx).anchor_before(4), + "|456|", + ), + Inlay::edit_prediction( + post_inc(&mut next_inlay_id), + buffer.read(cx).snapshot(cx).anchor_before(7), + "\n|567|\n", + ), ], ); assert_eq!(inlay_snapshot.text(), "|123|\nabc\n|456|def\n|567|\n\nghi"); @@ -1561,7 +1717,7 @@ mod tests { (offset, inlay.clone()) }) .collect::>(); - let mut expected_text = Rope::from(buffer_snapshot.text()); + let mut expected_text = Rope::from(&buffer_snapshot.text()); for (offset, inlay) in inlays.iter().rev() { expected_text.replace(*offset..*offset, &inlay.text.to_string()); } @@ -1591,7 +1747,7 @@ mod tests { text_highlight_ranges.sort_by_key(|range| (range.start, Reverse(range.end))); log::info!("highlighting text ranges {text_highlight_ranges:?}"); text_highlights.insert( - TypeId::of::<()>(), + HighlightKey::Type(TypeId::of::<()>()), Arc::new(( HighlightStyle::default(), text_highlight_ranges @@ -1666,7 +1822,7 @@ mod tests { ..Highlights::default() }, ) - .map(|chunk| chunk.text) + .map(|chunk| chunk.chunk.text) .collect::(); assert_eq!( actual_text, @@ -1811,4 +1967,210 @@ mod tests { cx.set_global(store); theme::init(theme::LoadThemes::JustBase, cx); } + + /// Helper to create test highlights for an inlay + fn create_inlay_highlights( + inlay_id: InlayId, + highlight_range: Range, + position: Anchor, + ) -> TreeMap> { + let mut inlay_highlights = TreeMap::default(); + let mut type_highlights = TreeMap::default(); + type_highlights.insert( + inlay_id, + ( + HighlightStyle::default(), + InlayHighlight { + inlay: inlay_id, + range: highlight_range, + inlay_position: position, + }, + ), + ); + inlay_highlights.insert(TypeId::of::<()>(), type_highlights); + inlay_highlights + } + + #[gpui::test] + fn test_inlay_utf8_boundary_panic_fix(cx: &mut App) { + init_test(cx); + + // This test verifies that we handle UTF-8 character boundaries correctly + // when splitting inlay text for highlighting. Previously, this would panic + // when trying to split at byte 13, which is in the middle of the '…' character. + // + // See https://github.com/zed-industries/zed/issues/33641 + let buffer = MultiBuffer::build_simple("fn main() {}\n", cx); + let (mut inlay_map, _) = InlayMap::new(buffer.read(cx).snapshot(cx)); + + // Create an inlay with text that contains a multi-byte character + // The string "SortingDirec…" contains an ellipsis character '…' which is 3 bytes (E2 80 A6) + let inlay_text = "SortingDirec…"; + let position = buffer.read(cx).snapshot(cx).anchor_before(Point::new(0, 5)); + + let inlay = Inlay { + id: InlayId::Hint(0), + position, + text: text::Rope::from(inlay_text), + color: None, + }; + + let (inlay_snapshot, _) = inlay_map.splice(&[], vec![inlay]); + + // Create highlights that request a split at byte 13, which is in the middle + // of the '…' character (bytes 12..15). We include the full character. + let inlay_highlights = create_inlay_highlights(InlayId::Hint(0), 0..13, position); + + let highlights = crate::display_map::Highlights { + text_highlights: None, + inlay_highlights: Some(&inlay_highlights), + styles: crate::display_map::HighlightStyles::default(), + }; + + // Collect chunks - this previously would panic + let chunks: Vec<_> = inlay_snapshot + .chunks( + InlayOffset(0)..InlayOffset(inlay_snapshot.len().0), + false, + highlights, + ) + .collect(); + + // Verify the chunks are correct + let full_text: String = chunks.iter().map(|c| c.chunk.text).collect(); + assert_eq!(full_text, "fn maSortingDirec…in() {}\n"); + + // Verify the highlighted portion includes the complete ellipsis character + let highlighted_chunks: Vec<_> = chunks + .iter() + .filter(|c| c.chunk.highlight_style.is_some() && c.chunk.is_inlay) + .collect(); + + assert_eq!(highlighted_chunks.len(), 1); + assert_eq!(highlighted_chunks[0].chunk.text, "SortingDirec…"); + } + + #[gpui::test] + fn test_inlay_utf8_boundaries(cx: &mut App) { + init_test(cx); + + struct TestCase { + inlay_text: &'static str, + highlight_range: Range, + expected_highlighted: &'static str, + description: &'static str, + } + + let test_cases = vec![ + TestCase { + inlay_text: "Hello👋World", + highlight_range: 0..7, + expected_highlighted: "Hello👋", + description: "Emoji boundary - rounds up to include full emoji", + }, + TestCase { + inlay_text: "Test→End", + highlight_range: 0..5, + expected_highlighted: "Test→", + description: "Arrow boundary - rounds up to include full arrow", + }, + TestCase { + inlay_text: "café", + highlight_range: 0..4, + expected_highlighted: "café", + description: "Accented char boundary - rounds up to include full é", + }, + TestCase { + inlay_text: "🎨🎭🎪", + highlight_range: 0..5, + expected_highlighted: "🎨🎭", + description: "Multiple emojis - partial highlight", + }, + TestCase { + inlay_text: "普通话", + highlight_range: 0..4, + expected_highlighted: "普通", + description: "Chinese characters - partial highlight", + }, + TestCase { + inlay_text: "Hello", + highlight_range: 0..2, + expected_highlighted: "He", + description: "ASCII only - no adjustment needed", + }, + TestCase { + inlay_text: "👋", + highlight_range: 0..1, + expected_highlighted: "👋", + description: "Single emoji - partial byte range includes whole char", + }, + TestCase { + inlay_text: "Test", + highlight_range: 0..0, + expected_highlighted: "", + description: "Empty range", + }, + TestCase { + inlay_text: "🎨ABC", + highlight_range: 2..5, + expected_highlighted: "A", + description: "Range starting mid-emoji skips the emoji", + }, + ]; + + for test_case in test_cases { + let buffer = MultiBuffer::build_simple("test", cx); + let (mut inlay_map, _) = InlayMap::new(buffer.read(cx).snapshot(cx)); + let position = buffer.read(cx).snapshot(cx).anchor_before(Point::new(0, 2)); + + let inlay = Inlay { + id: InlayId::Hint(0), + position, + text: text::Rope::from(test_case.inlay_text), + color: None, + }; + + let (inlay_snapshot, _) = inlay_map.splice(&[], vec![inlay]); + let inlay_highlights = create_inlay_highlights( + InlayId::Hint(0), + test_case.highlight_range.clone(), + position, + ); + + let highlights = crate::display_map::Highlights { + text_highlights: None, + inlay_highlights: Some(&inlay_highlights), + styles: crate::display_map::HighlightStyles::default(), + }; + + let chunks: Vec<_> = inlay_snapshot + .chunks( + InlayOffset(0)..InlayOffset(inlay_snapshot.len().0), + false, + highlights, + ) + .collect(); + + // Verify we got chunks and they total to the expected text + let full_text: String = chunks.iter().map(|c| c.chunk.text).collect(); + assert_eq!( + full_text, + format!("te{}st", test_case.inlay_text), + "Full text mismatch for case: {}", + test_case.description + ); + + // Verify that the highlighted portion matches expectations + let highlighted_text: String = chunks + .iter() + .filter(|c| c.chunk.highlight_style.is_some() && c.chunk.is_inlay) + .map(|c| c.chunk.text) + .collect(); + assert_eq!( + highlighted_text, test_case.expected_highlighted, + "Highlighted text mismatch for case: {} (text: '{}', range: {:?})", + test_case.description, test_case.inlay_text, test_case.highlight_range + ); + } + } } diff --git a/crates/editor/src/display_map/invisibles.rs b/crates/editor/src/display_map/invisibles.rs index 199986f2a4..0712ddf9e2 100644 --- a/crates/editor/src/display_map/invisibles.rs +++ b/crates/editor/src/display_map/invisibles.rs @@ -36,8 +36,8 @@ pub fn is_invisible(c: char) -> bool { } else if c >= '\u{7f}' { c <= '\u{9f}' || (c.is_whitespace() && c != IDEOGRAPHIC_SPACE) - || contains(c, &FORMAT) - || contains(c, &OTHER) + || contains(c, FORMAT) + || contains(c, OTHER) } else { false } @@ -50,7 +50,7 @@ pub fn replacement(c: char) -> Option<&'static str> { Some(C0_SYMBOLS[c as usize]) } else if c == '\x7f' { Some(DEL) - } else if contains(c, &PRESERVE) { + } else if contains(c, PRESERVE) { None } else { Some("\u{2007}") // fixed width space @@ -61,14 +61,14 @@ pub fn replacement(c: char) -> Option<&'static str> { // but could if we tracked state in the classifier. const IDEOGRAPHIC_SPACE: char = '\u{3000}'; -const C0_SYMBOLS: &'static [&'static str] = &[ +const C0_SYMBOLS: &[&str] = &[ "␀", "␁", "␂", "␃", "␄", "␅", "␆", "␇", "␈", "␉", "␊", "␋", "␌", "␍", "␎", "␏", "␐", "␑", "␒", "␓", "␔", "␕", "␖", "␗", "␘", "␙", "␚", "␛", "␜", "␝", "␞", "␟", ]; -const DEL: &'static str = "␡"; +const DEL: &str = "␡"; // generated using ucd-generate: ucd-generate general-category --include Format --chars ucd-16.0.0 -pub const FORMAT: &'static [(char, char)] = &[ +pub const FORMAT: &[(char, char)] = &[ ('\u{ad}', '\u{ad}'), ('\u{600}', '\u{605}'), ('\u{61c}', '\u{61c}'), @@ -93,7 +93,7 @@ pub const FORMAT: &'static [(char, char)] = &[ ]; // hand-made base on https://invisible-characters.com (Excluding Cf) -pub const OTHER: &'static [(char, char)] = &[ +pub const OTHER: &[(char, char)] = &[ ('\u{034f}', '\u{034f}'), ('\u{115F}', '\u{1160}'), ('\u{17b4}', '\u{17b5}'), @@ -107,7 +107,7 @@ pub const OTHER: &'static [(char, char)] = &[ ]; // a subset of FORMAT/OTHER that may appear within glyphs -const PRESERVE: &'static [(char, char)] = &[ +const PRESERVE: &[(char, char)] = &[ ('\u{034f}', '\u{034f}'), ('\u{200d}', '\u{200d}'), ('\u{17b4}', '\u{17b5}'), diff --git a/crates/editor/src/display_map/tab_map.rs b/crates/editor/src/display_map/tab_map.rs index eb5d57d484..6f5df9bb8e 100644 --- a/crates/editor/src/display_map/tab_map.rs +++ b/crates/editor/src/display_map/tab_map.rs @@ -116,7 +116,7 @@ impl TabMap { state.new.end = edit.new.end; Some(None) // Skip this edit, it's merged } else { - let new_state = edit.clone(); + let new_state = edit; let result = Some(Some(state.clone())); // Yield the previous edit **state = new_state; result @@ -611,7 +611,7 @@ mod tests { fn test_expand_tabs(cx: &mut gpui::App) { let buffer = MultiBuffer::build_simple("", cx); let buffer_snapshot = buffer.read(cx).snapshot(cx); - let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot); let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); @@ -628,7 +628,7 @@ mod tests { let buffer = MultiBuffer::build_simple(input, cx); let buffer_snapshot = buffer.read(cx).snapshot(cx); - let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot); let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); @@ -675,7 +675,7 @@ mod tests { let buffer = MultiBuffer::build_simple(input, cx); let buffer_snapshot = buffer.read(cx).snapshot(cx); - let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot); let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); @@ -689,7 +689,7 @@ mod tests { let buffer = MultiBuffer::build_simple(input, cx); let buffer_snapshot = buffer.read(cx).snapshot(cx); - let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot); let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); @@ -749,7 +749,7 @@ mod tests { let buffer_snapshot = buffer.read(cx).snapshot(cx); log::info!("Buffer text: {:?}", buffer_snapshot.text()); - let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot); log::info!("InlayMap text: {:?}", inlay_snapshot.text()); let (mut fold_map, _) = FoldMap::new(inlay_snapshot.clone()); fold_map.randomly_mutate(&mut rng); @@ -758,7 +758,7 @@ mod tests { let (inlay_snapshot, _) = inlay_map.randomly_mutate(&mut 0, &mut rng); log::info!("InlayMap text: {:?}", inlay_snapshot.text()); - let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size); + let (mut tab_map, _) = TabMap::new(fold_snapshot, tab_size); let tabs_snapshot = tab_map.set_max_expansion_column(32); let text = text::Rope::from(tabs_snapshot.text().as_str()); diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index ca7ee056c4..500ec3a0bb 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -9,7 +9,7 @@ use multi_buffer::{MultiBufferSnapshot, RowInfo}; use smol::future::yield_now; use std::sync::LazyLock; use std::{cmp, collections::VecDeque, mem, ops::Range, time::Duration}; -use sum_tree::{Bias, Cursor, SumTree}; +use sum_tree::{Bias, Cursor, Dimensions, SumTree}; use text::Patch; pub use super::tab_map::TextSummary; @@ -55,7 +55,7 @@ pub struct WrapChunks<'a> { input_chunk: Chunk<'a>, output_position: WrapPoint, max_output_row: u32, - transforms: Cursor<'a, Transform, (WrapPoint, TabPoint)>, + transforms: Cursor<'a, Transform, Dimensions>, snapshot: &'a WrapSnapshot, } @@ -66,18 +66,18 @@ pub struct WrapRows<'a> { output_row: u32, soft_wrapped: bool, max_output_row: u32, - transforms: Cursor<'a, Transform, (WrapPoint, TabPoint)>, + transforms: Cursor<'a, Transform, Dimensions>, } impl WrapRows<'_> { pub(crate) fn seek(&mut self, start_row: u32) { self.transforms - .seek(&WrapPoint::new(start_row, 0), Bias::Left, &()); + .seek(&WrapPoint::new(start_row, 0), Bias::Left); let mut input_row = self.transforms.start().1.row(); - if self.transforms.item().map_or(false, |t| t.is_isomorphic()) { + if self.transforms.item().is_some_and(|t| t.is_isomorphic()) { input_row += start_row - self.transforms.start().0.row(); } - self.soft_wrapped = self.transforms.item().map_or(false, |t| !t.is_isomorphic()); + self.soft_wrapped = self.transforms.item().is_some_and(|t| !t.is_isomorphic()); self.input_buffer_rows.seek(input_row); self.input_buffer_row = self.input_buffer_rows.next().unwrap(); self.output_row = start_row; @@ -249,48 +249,48 @@ impl WrapMap { return; } - if let Some(wrap_width) = self.wrap_width { - if self.background_task.is_none() { - let pending_edits = self.pending_edits.clone(); - let mut snapshot = self.snapshot.clone(); - let text_system = cx.text_system().clone(); - let (font, font_size) = self.font_with_size.clone(); - let update_task = cx.background_spawn(async move { - let mut edits = Patch::default(); - let mut line_wrapper = text_system.line_wrapper(font, font_size); - for (tab_snapshot, tab_edits) in pending_edits { - let wrap_edits = snapshot - .update(tab_snapshot, &tab_edits, wrap_width, &mut line_wrapper) - .await; - edits = edits.compose(&wrap_edits); - } - (snapshot, edits) - }); + if let Some(wrap_width) = self.wrap_width + && self.background_task.is_none() + { + let pending_edits = self.pending_edits.clone(); + let mut snapshot = self.snapshot.clone(); + let text_system = cx.text_system().clone(); + let (font, font_size) = self.font_with_size.clone(); + let update_task = cx.background_spawn(async move { + let mut edits = Patch::default(); + let mut line_wrapper = text_system.line_wrapper(font, font_size); + for (tab_snapshot, tab_edits) in pending_edits { + let wrap_edits = snapshot + .update(tab_snapshot, &tab_edits, wrap_width, &mut line_wrapper) + .await; + edits = edits.compose(&wrap_edits); + } + (snapshot, edits) + }); - match cx - .background_executor() - .block_with_timeout(Duration::from_millis(1), update_task) - { - Ok((snapshot, output_edits)) => { - self.snapshot = snapshot; - self.edits_since_sync = self.edits_since_sync.compose(&output_edits); - } - Err(update_task) => { - self.background_task = Some(cx.spawn(async move |this, cx| { - let (snapshot, edits) = update_task.await; - this.update(cx, |this, cx| { - this.snapshot = snapshot; - this.edits_since_sync = this - .edits_since_sync - .compose(mem::take(&mut this.interpolated_edits).invert()) - .compose(&edits); - this.background_task = None; - this.flush_edits(cx); - cx.notify(); - }) - .ok(); - })); - } + match cx + .background_executor() + .block_with_timeout(Duration::from_millis(1), update_task) + { + Ok((snapshot, output_edits)) => { + self.snapshot = snapshot; + self.edits_since_sync = self.edits_since_sync.compose(&output_edits); + } + Err(update_task) => { + self.background_task = Some(cx.spawn(async move |this, cx| { + let (snapshot, edits) = update_task.await; + this.update(cx, |this, cx| { + this.snapshot = snapshot; + this.edits_since_sync = this + .edits_since_sync + .compose(mem::take(&mut this.interpolated_edits).invert()) + .compose(&edits); + this.background_task = None; + this.flush_edits(cx); + cx.notify(); + }) + .ok(); + })); } } } @@ -340,7 +340,7 @@ impl WrapSnapshot { let mut tab_edits_iter = tab_edits.iter().peekable(); new_transforms = - old_cursor.slice(&tab_edits_iter.peek().unwrap().old.start, Bias::Right, &()); + old_cursor.slice(&tab_edits_iter.peek().unwrap().old.start, Bias::Right); while let Some(edit) = tab_edits_iter.next() { if edit.new.start > TabPoint::from(new_transforms.summary().input.lines) { @@ -356,31 +356,29 @@ impl WrapSnapshot { )); } - old_cursor.seek_forward(&edit.old.end, Bias::Right, &()); + old_cursor.seek_forward(&edit.old.end, Bias::Right); if let Some(next_edit) = tab_edits_iter.peek() { - if next_edit.old.start > old_cursor.end(&()) { - if old_cursor.end(&()) > edit.old.end { + if next_edit.old.start > old_cursor.end() { + if old_cursor.end() > edit.old.end { let summary = self .tab_snapshot - .text_summary_for_range(edit.old.end..old_cursor.end(&())); + .text_summary_for_range(edit.old.end..old_cursor.end()); new_transforms.push_or_extend(Transform::isomorphic(summary)); } - old_cursor.next(&()); - new_transforms.append( - old_cursor.slice(&next_edit.old.start, Bias::Right, &()), - &(), - ); + old_cursor.next(); + new_transforms + .append(old_cursor.slice(&next_edit.old.start, Bias::Right), &()); } } else { - if old_cursor.end(&()) > edit.old.end { + if old_cursor.end() > edit.old.end { let summary = self .tab_snapshot - .text_summary_for_range(edit.old.end..old_cursor.end(&())); + .text_summary_for_range(edit.old.end..old_cursor.end()); new_transforms.push_or_extend(Transform::isomorphic(summary)); } - old_cursor.next(&()); - new_transforms.append(old_cursor.suffix(&()), &()); + old_cursor.next(); + new_transforms.append(old_cursor.suffix(), &()); } } } @@ -441,7 +439,6 @@ impl WrapSnapshot { new_transforms = old_cursor.slice( &TabPoint::new(row_edits.peek().unwrap().old_rows.start, 0), Bias::Right, - &(), ); while let Some(edit) = row_edits.next() { @@ -516,34 +513,31 @@ impl WrapSnapshot { } new_transforms.extend(edit_transforms, &()); - old_cursor.seek_forward(&TabPoint::new(edit.old_rows.end, 0), Bias::Right, &()); + old_cursor.seek_forward(&TabPoint::new(edit.old_rows.end, 0), Bias::Right); if let Some(next_edit) = row_edits.peek() { - if next_edit.old_rows.start > old_cursor.end(&()).row() { - if old_cursor.end(&()) > TabPoint::new(edit.old_rows.end, 0) { + if next_edit.old_rows.start > old_cursor.end().row() { + if old_cursor.end() > TabPoint::new(edit.old_rows.end, 0) { let summary = self.tab_snapshot.text_summary_for_range( - TabPoint::new(edit.old_rows.end, 0)..old_cursor.end(&()), + TabPoint::new(edit.old_rows.end, 0)..old_cursor.end(), ); new_transforms.push_or_extend(Transform::isomorphic(summary)); } - old_cursor.next(&()); + old_cursor.next(); new_transforms.append( - old_cursor.slice( - &TabPoint::new(next_edit.old_rows.start, 0), - Bias::Right, - &(), - ), + old_cursor + .slice(&TabPoint::new(next_edit.old_rows.start, 0), Bias::Right), &(), ); } } else { - if old_cursor.end(&()) > TabPoint::new(edit.old_rows.end, 0) { + if old_cursor.end() > TabPoint::new(edit.old_rows.end, 0) { let summary = self.tab_snapshot.text_summary_for_range( - TabPoint::new(edit.old_rows.end, 0)..old_cursor.end(&()), + TabPoint::new(edit.old_rows.end, 0)..old_cursor.end(), ); new_transforms.push_or_extend(Transform::isomorphic(summary)); } - old_cursor.next(&()); - new_transforms.append(old_cursor.suffix(&()), &()); + old_cursor.next(); + new_transforms.append(old_cursor.suffix(), &()); } } } @@ -570,19 +564,19 @@ impl WrapSnapshot { tab_edit.new.start.0.column = 0; tab_edit.new.end.0 += Point::new(1, 0); - old_cursor.seek(&tab_edit.old.start, Bias::Right, &()); + old_cursor.seek(&tab_edit.old.start, Bias::Right); let mut old_start = old_cursor.start().output.lines; old_start += tab_edit.old.start.0 - old_cursor.start().input.lines; - old_cursor.seek(&tab_edit.old.end, Bias::Right, &()); + old_cursor.seek(&tab_edit.old.end, Bias::Right); let mut old_end = old_cursor.start().output.lines; old_end += tab_edit.old.end.0 - old_cursor.start().input.lines; - new_cursor.seek(&tab_edit.new.start, Bias::Right, &()); + new_cursor.seek(&tab_edit.new.start, Bias::Right); let mut new_start = new_cursor.start().output.lines; new_start += tab_edit.new.start.0 - new_cursor.start().input.lines; - new_cursor.seek(&tab_edit.new.end, Bias::Right, &()); + new_cursor.seek(&tab_edit.new.end, Bias::Right); let mut new_end = new_cursor.start().output.lines; new_end += tab_edit.new.end.0 - new_cursor.start().input.lines; @@ -604,10 +598,12 @@ impl WrapSnapshot { ) -> WrapChunks<'a> { let output_start = WrapPoint::new(rows.start, 0); let output_end = WrapPoint::new(rows.end, 0); - let mut transforms = self.transforms.cursor::<(WrapPoint, TabPoint)>(&()); - transforms.seek(&output_start, Bias::Right, &()); + let mut transforms = self + .transforms + .cursor::>(&()); + transforms.seek(&output_start, Bias::Right); let mut input_start = TabPoint(transforms.start().1.0); - if transforms.item().map_or(false, |t| t.is_isomorphic()) { + if transforms.item().is_some_and(|t| t.is_isomorphic()) { input_start.0 += output_start.0 - transforms.start().0.0; } let input_end = self @@ -632,11 +628,13 @@ impl WrapSnapshot { } pub fn line_len(&self, row: u32) -> u32 { - let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(&()); - cursor.seek(&WrapPoint::new(row + 1, 0), Bias::Left, &()); + let mut cursor = self + .transforms + .cursor::>(&()); + cursor.seek(&WrapPoint::new(row + 1, 0), Bias::Left); if cursor .item() - .map_or(false, |transform| transform.is_isomorphic()) + .is_some_and(|transform| transform.is_isomorphic()) { let overshoot = row - cursor.start().0.row(); let tab_row = cursor.start().1.row() + overshoot; @@ -657,11 +655,13 @@ impl WrapSnapshot { let start = WrapPoint::new(rows.start, 0); let end = WrapPoint::new(rows.end, 0); - let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(&()); - cursor.seek(&start, Bias::Right, &()); + let mut cursor = self + .transforms + .cursor::>(&()); + cursor.seek(&start, Bias::Right); if let Some(transform) = cursor.item() { let start_in_transform = start.0 - cursor.start().0.0; - let end_in_transform = cmp::min(end, cursor.end(&()).0).0 - cursor.start().0.0; + let end_in_transform = cmp::min(end, cursor.end().0).0 - cursor.start().0.0; if transform.is_isomorphic() { let tab_start = TabPoint(cursor.start().1.0 + start_in_transform); let tab_end = TabPoint(cursor.start().1.0 + end_in_transform); @@ -678,12 +678,12 @@ impl WrapSnapshot { }; } - cursor.next(&()); + cursor.next(); } if rows.end > cursor.start().0.row() { summary += &cursor - .summary::<_, TransformSummary>(&WrapPoint::new(rows.end, 0), Bias::Right, &()) + .summary::<_, TransformSummary>(&WrapPoint::new(rows.end, 0), Bias::Right) .output; if let Some(transform) = cursor.item() { @@ -712,7 +712,7 @@ impl WrapSnapshot { pub fn soft_wrap_indent(&self, row: u32) -> Option { let mut cursor = self.transforms.cursor::(&()); - cursor.seek(&WrapPoint::new(row + 1, 0), Bias::Right, &()); + cursor.seek(&WrapPoint::new(row + 1, 0), Bias::Right); cursor.item().and_then(|transform| { if transform.is_isomorphic() { None @@ -726,14 +726,16 @@ impl WrapSnapshot { self.transforms.summary().output.longest_row } - pub fn row_infos(&self, start_row: u32) -> WrapRows { - let mut transforms = self.transforms.cursor::<(WrapPoint, TabPoint)>(&()); - transforms.seek(&WrapPoint::new(start_row, 0), Bias::Left, &()); + pub fn row_infos(&self, start_row: u32) -> WrapRows<'_> { + let mut transforms = self + .transforms + .cursor::>(&()); + transforms.seek(&WrapPoint::new(start_row, 0), Bias::Left); let mut input_row = transforms.start().1.row(); - if transforms.item().map_or(false, |t| t.is_isomorphic()) { + if transforms.item().is_some_and(|t| t.is_isomorphic()) { input_row += start_row - transforms.start().0.row(); } - let soft_wrapped = transforms.item().map_or(false, |t| !t.is_isomorphic()); + let soft_wrapped = transforms.item().is_some_and(|t| !t.is_isomorphic()); let mut input_buffer_rows = self.tab_snapshot.rows(input_row); let input_buffer_row = input_buffer_rows.next().unwrap(); WrapRows { @@ -747,10 +749,12 @@ impl WrapSnapshot { } pub fn to_tab_point(&self, point: WrapPoint) -> TabPoint { - let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(&()); - cursor.seek(&point, Bias::Right, &()); + let mut cursor = self + .transforms + .cursor::>(&()); + cursor.seek(&point, Bias::Right); let mut tab_point = cursor.start().1.0; - if cursor.item().map_or(false, |t| t.is_isomorphic()) { + if cursor.item().is_some_and(|t| t.is_isomorphic()) { tab_point += point.0 - cursor.start().0.0; } TabPoint(tab_point) @@ -765,16 +769,18 @@ impl WrapSnapshot { } pub fn tab_point_to_wrap_point(&self, point: TabPoint) -> WrapPoint { - let mut cursor = self.transforms.cursor::<(TabPoint, WrapPoint)>(&()); - cursor.seek(&point, Bias::Right, &()); + let mut cursor = self + .transforms + .cursor::>(&()); + cursor.seek(&point, Bias::Right); WrapPoint(cursor.start().1.0 + (point.0 - cursor.start().0.0)) } pub fn clip_point(&self, mut point: WrapPoint, bias: Bias) -> WrapPoint { if bias == Bias::Left { let mut cursor = self.transforms.cursor::(&()); - cursor.seek(&point, Bias::Right, &()); - if cursor.item().map_or(false, |t| !t.is_isomorphic()) { + cursor.seek(&point, Bias::Right); + if cursor.item().is_some_and(|t| !t.is_isomorphic()) { point = *cursor.start(); *point.column_mut() -= 1; } @@ -790,17 +796,19 @@ impl WrapSnapshot { *point.column_mut() = 0; - let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(&()); - cursor.seek(&point, Bias::Right, &()); + let mut cursor = self + .transforms + .cursor::>(&()); + cursor.seek(&point, Bias::Right); if cursor.item().is_none() { - cursor.prev(&()); + cursor.prev(); } while let Some(transform) = cursor.item() { if transform.is_isomorphic() && cursor.start().1.column() == 0 { - return cmp::min(cursor.end(&()).0.row(), point.row()); + return cmp::min(cursor.end().0.row(), point.row()); } else { - cursor.prev(&()); + cursor.prev(); } } @@ -810,13 +818,15 @@ impl WrapSnapshot { pub fn next_row_boundary(&self, mut point: WrapPoint) -> Option { point.0 += Point::new(1, 0); - let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(&()); - cursor.seek(&point, Bias::Right, &()); + let mut cursor = self + .transforms + .cursor::>(&()); + cursor.seek(&point, Bias::Right); while let Some(transform) = cursor.item() { if transform.is_isomorphic() && cursor.start().1.column() == 0 { return Some(cmp::max(cursor.start().0.row(), point.row())); } else { - cursor.next(&()); + cursor.next(); } } @@ -889,9 +899,9 @@ impl WrapChunks<'_> { pub(crate) fn seek(&mut self, rows: Range) { let output_start = WrapPoint::new(rows.start, 0); let output_end = WrapPoint::new(rows.end, 0); - self.transforms.seek(&output_start, Bias::Right, &()); + self.transforms.seek(&output_start, Bias::Right); let mut input_start = TabPoint(self.transforms.start().1.0); - if self.transforms.item().map_or(false, |t| t.is_isomorphic()) { + if self.transforms.item().is_some_and(|t| t.is_isomorphic()) { input_start.0 += output_start.0 - self.transforms.start().0.0; } let input_end = self @@ -930,10 +940,10 @@ impl<'a> Iterator for WrapChunks<'a> { } self.output_position.0 += summary; - self.transforms.next(&()); + self.transforms.next(); return Some(Chunk { text: &display_text[start_ix..end_ix], - ..self.input_chunk.clone() + ..Default::default() }); } @@ -942,7 +952,7 @@ impl<'a> Iterator for WrapChunks<'a> { } let mut input_len = 0; - let transform_end = self.transforms.end(&()).0; + let transform_end = self.transforms.end().0; for c in self.input_chunk.text.chars() { let char_len = c.len_utf8(); input_len += char_len; @@ -954,7 +964,7 @@ impl<'a> Iterator for WrapChunks<'a> { } if self.output_position >= transform_end { - self.transforms.next(&()); + self.transforms.next(); break; } } @@ -982,8 +992,8 @@ impl Iterator for WrapRows<'_> { self.output_row += 1; self.transforms - .seek_forward(&WrapPoint::new(self.output_row, 0), Bias::Left, &()); - if self.transforms.item().map_or(false, |t| t.is_isomorphic()) { + .seek_forward(&WrapPoint::new(self.output_row, 0), Bias::Left); + if self.transforms.item().is_some_and(|t| t.is_isomorphic()) { self.input_buffer_row = self.input_buffer_rows.next().unwrap(); self.soft_wrapped = false; } else { @@ -1055,12 +1065,12 @@ impl sum_tree::Item for Transform { } fn push_isomorphic(transforms: &mut Vec, summary: TextSummary) { - if let Some(last_transform) = transforms.last_mut() { - if last_transform.is_isomorphic() { - last_transform.summary.input += &summary; - last_transform.summary.output += &summary; - return; - } + if let Some(last_transform) = transforms.last_mut() + && last_transform.is_isomorphic() + { + last_transform.summary.input += &summary; + last_transform.summary.output += &summary; + return; } transforms.push(Transform::isomorphic(summary)); } @@ -1213,7 +1223,7 @@ mod tests { let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap(); let font = test_font(); - let _font_id = text_system.font_id(&font); + let _font_id = text_system.resolve_font(&font); let font_size = px(14.0); log::info!("Tab size: {}", tab_size); @@ -1451,7 +1461,7 @@ mod tests { } let mut prev_ix = 0; - for boundary in line_wrapper.wrap_line(&[LineFragment::text(&line)], wrap_width) { + for boundary in line_wrapper.wrap_line(&[LineFragment::text(line)], wrap_width) { wrapped_text.push_str(&line[prev_ix..boundary.ix]); wrapped_text.push('\n'); wrapped_text.push_str(&" ".repeat(boundary.next_indent as usize)); diff --git a/crates/editor/src/inline_completion_tests.rs b/crates/editor/src/edit_prediction_tests.rs similarity index 52% rename from crates/editor/src/inline_completion_tests.rs rename to crates/editor/src/edit_prediction_tests.rs index 5ac34c94f5..bba632e81f 100644 --- a/crates/editor/src/inline_completion_tests.rs +++ b/crates/editor/src/edit_prediction_tests.rs @@ -1,26 +1,28 @@ +use edit_prediction::EditPredictionProvider; use gpui::{Entity, prelude::*}; use indoc::indoc; -use inline_completion::EditPredictionProvider; use multi_buffer::{Anchor, MultiBufferSnapshot, ToPoint}; use project::Project; use std::ops::Range; use text::{Point, ToOffset}; use crate::{ - InlineCompletion, editor_tests::init_test, test::editor_test_context::EditorTestContext, + EditPrediction, + editor_tests::{init_test, update_test_language_settings}, + test::editor_test_context::EditorTestContext, }; #[gpui::test] -async fn test_inline_completion_insert(cx: &mut gpui::TestAppContext) { +async fn test_edit_prediction_insert(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx).await; - let provider = cx.new(|_| FakeInlineCompletionProvider::default()); + let provider = cx.new(|_| FakeEditPredictionProvider::default()); assign_editor_completion_provider(provider.clone(), &mut cx); cx.set_state("let absolute_zero_celsius = ˇ;"); propose_edits(&provider, vec![(28..28, "-273.15")], &mut cx); - cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx)); + cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); assert_editor_active_edit_completion(&mut cx, |_, edits| { assert_eq!(edits.len(), 1); @@ -33,16 +35,16 @@ async fn test_inline_completion_insert(cx: &mut gpui::TestAppContext) { } #[gpui::test] -async fn test_inline_completion_modification(cx: &mut gpui::TestAppContext) { +async fn test_edit_prediction_modification(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx).await; - let provider = cx.new(|_| FakeInlineCompletionProvider::default()); + let provider = cx.new(|_| FakeEditPredictionProvider::default()); assign_editor_completion_provider(provider.clone(), &mut cx); cx.set_state("let pi = ˇ\"foo\";"); propose_edits(&provider, vec![(9..14, "3.14159")], &mut cx); - cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx)); + cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); assert_editor_active_edit_completion(&mut cx, |_, edits| { assert_eq!(edits.len(), 1); @@ -55,11 +57,11 @@ async fn test_inline_completion_modification(cx: &mut gpui::TestAppContext) { } #[gpui::test] -async fn test_inline_completion_jump_button(cx: &mut gpui::TestAppContext) { +async fn test_edit_prediction_jump_button(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx).await; - let provider = cx.new(|_| FakeInlineCompletionProvider::default()); + let provider = cx.new(|_| FakeEditPredictionProvider::default()); assign_editor_completion_provider(provider.clone(), &mut cx); // Cursor is 2+ lines above the proposed edit @@ -77,7 +79,7 @@ async fn test_inline_completion_jump_button(cx: &mut gpui::TestAppContext) { &mut cx, ); - cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx)); + cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); assert_editor_active_move_completion(&mut cx, |snapshot, move_target| { assert_eq!(move_target.to_point(&snapshot), Point::new(4, 3)); }); @@ -107,7 +109,7 @@ async fn test_inline_completion_jump_button(cx: &mut gpui::TestAppContext) { &mut cx, ); - cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx)); + cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); assert_editor_active_move_completion(&mut cx, |snapshot, move_target| { assert_eq!(move_target.to_point(&snapshot), Point::new(1, 3)); }); @@ -124,11 +126,11 @@ async fn test_inline_completion_jump_button(cx: &mut gpui::TestAppContext) { } #[gpui::test] -async fn test_inline_completion_invalidation_range(cx: &mut gpui::TestAppContext) { +async fn test_edit_prediction_invalidation_range(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx).await; - let provider = cx.new(|_| FakeInlineCompletionProvider::default()); + let provider = cx.new(|_| FakeEditPredictionProvider::default()); assign_editor_completion_provider(provider.clone(), &mut cx); // Cursor is 3+ lines above the proposed edit @@ -148,7 +150,7 @@ async fn test_inline_completion_invalidation_range(cx: &mut gpui::TestAppContext &mut cx, ); - cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx)); + cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); assert_editor_active_move_completion(&mut cx, |snapshot, move_target| { assert_eq!(move_target.to_point(&snapshot), edit_location); }); @@ -176,7 +178,7 @@ async fn test_inline_completion_invalidation_range(cx: &mut gpui::TestAppContext line "}); cx.editor(|editor, _, _| { - assert!(editor.active_inline_completion.is_none()); + assert!(editor.active_edit_prediction.is_none()); }); // Cursor is 3+ lines below the proposed edit @@ -196,7 +198,7 @@ async fn test_inline_completion_invalidation_range(cx: &mut gpui::TestAppContext &mut cx, ); - cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx)); + cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); assert_editor_active_move_completion(&mut cx, |snapshot, move_target| { assert_eq!(move_target.to_point(&snapshot), edit_location); }); @@ -224,7 +226,88 @@ async fn test_inline_completion_invalidation_range(cx: &mut gpui::TestAppContext line ˇ5 "}); cx.editor(|editor, _, _| { - assert!(editor.active_inline_completion.is_none()); + assert!(editor.active_edit_prediction.is_none()); + }); +} + +#[gpui::test] +async fn test_edit_prediction_jump_disabled_for_non_zed_providers(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + let provider = cx.new(|_| FakeNonZedEditPredictionProvider::default()); + assign_editor_completion_provider_non_zed(provider.clone(), &mut cx); + + // Cursor is 2+ lines above the proposed edit + cx.set_state(indoc! {" + line 0 + line ˇ1 + line 2 + line 3 + line + "}); + + propose_edits_non_zed( + &provider, + vec![(Point::new(4, 3)..Point::new(4, 3), " 4")], + &mut cx, + ); + + cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); + + // For non-Zed providers, there should be no move completion (jump functionality disabled) + cx.editor(|editor, _, _| { + if let Some(completion_state) = &editor.active_edit_prediction { + // Should be an Edit prediction, not a Move prediction + match &completion_state.completion { + EditPrediction::Edit { .. } => { + // This is expected for non-Zed providers + } + EditPrediction::Move { .. } => { + panic!( + "Non-Zed providers should not show Move predictions (jump functionality)" + ); + } + } + } + }); +} + +#[gpui::test] +async fn test_edit_predictions_disabled_in_scope(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + update_test_language_settings(cx, |settings| { + settings.defaults.edit_predictions_disabled_in = Some(vec!["string".to_string()]); + }); + + let mut cx = EditorTestContext::new(cx).await; + let provider = cx.new(|_| FakeEditPredictionProvider::default()); + assign_editor_completion_provider(provider.clone(), &mut cx); + + let language = languages::language("javascript", tree_sitter_typescript::LANGUAGE_TSX.into()); + cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + + // Test disabled inside of string + cx.set_state("const x = \"hello ˇworld\";"); + propose_edits(&provider, vec![(17..17, "beautiful ")], &mut cx); + cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); + cx.editor(|editor, _, _| { + assert!( + editor.active_edit_prediction.is_none(), + "Edit predictions should be disabled in string scopes when configured in edit_predictions_disabled_in" + ); + }); + + // Test enabled outside of string + cx.set_state("const x = \"hello world\"; ˇ"); + propose_edits(&provider, vec![(24..24, "// comment")], &mut cx); + cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); + cx.editor(|editor, _, _| { + assert!( + editor.active_edit_prediction.is_some(), + "Edit predictions should work outside of disabled scopes" + ); }); } @@ -234,11 +317,11 @@ fn assert_editor_active_edit_completion( ) { cx.editor(|editor, _, cx| { let completion_state = editor - .active_inline_completion + .active_edit_prediction .as_ref() .expect("editor has no active completion"); - if let InlineCompletion::Edit { edits, .. } = &completion_state.completion { + if let EditPrediction::Edit { edits, .. } = &completion_state.completion { assert(editor.buffer().read(cx).snapshot(cx), edits); } else { panic!("expected edit completion"); @@ -252,11 +335,11 @@ fn assert_editor_active_move_completion( ) { cx.editor(|editor, _, cx| { let completion_state = editor - .active_inline_completion + .active_edit_prediction .as_ref() .expect("editor has no active completion"); - if let InlineCompletion::Move { target, .. } = &completion_state.completion { + if let EditPrediction::Move { target, .. } = &completion_state.completion { assert(editor.buffer().read(cx).snapshot(cx), *target); } else { panic!("expected move completion"); @@ -271,7 +354,7 @@ fn accept_completion(cx: &mut EditorTestContext) { } fn propose_edits( - provider: &Entity, + provider: &Entity, edits: Vec<(Range, &str)>, cx: &mut EditorTestContext, ) { @@ -283,7 +366,7 @@ fn propose_edits( cx.update(|_, cx| { provider.update(cx, |provider, _| { - provider.set_inline_completion(Some(inline_completion::InlineCompletion { + provider.set_edit_prediction(Some(edit_prediction::EditPrediction { id: None, edits: edits.collect(), edit_preview: None, @@ -293,7 +376,38 @@ fn propose_edits( } fn assign_editor_completion_provider( - provider: Entity, + provider: Entity, + cx: &mut EditorTestContext, +) { + cx.update_editor(|editor, window, cx| { + editor.set_edit_prediction_provider(Some(provider), window, cx); + }) +} + +fn propose_edits_non_zed( + provider: &Entity, + edits: Vec<(Range, &str)>, + cx: &mut EditorTestContext, +) { + let snapshot = cx.buffer_snapshot(); + let edits = edits.into_iter().map(|(range, text)| { + let range = snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end); + (range, text.into()) + }); + + cx.update(|_, cx| { + provider.update(cx, |provider, _| { + provider.set_edit_prediction(Some(edit_prediction::EditPrediction { + id: None, + edits: edits.collect(), + edit_preview: None, + })) + }) + }); +} + +fn assign_editor_completion_provider_non_zed( + provider: Entity, cx: &mut EditorTestContext, ) { cx.update_editor(|editor, window, cx| { @@ -302,20 +416,17 @@ fn assign_editor_completion_provider( } #[derive(Default, Clone)] -pub struct FakeInlineCompletionProvider { - pub completion: Option, +pub struct FakeEditPredictionProvider { + pub completion: Option, } -impl FakeInlineCompletionProvider { - pub fn set_inline_completion( - &mut self, - completion: Option, - ) { +impl FakeEditPredictionProvider { + pub fn set_edit_prediction(&mut self, completion: Option) { self.completion = completion; } } -impl EditPredictionProvider for FakeInlineCompletionProvider { +impl EditPredictionProvider for FakeEditPredictionProvider { fn name() -> &'static str { "fake-completion-provider" } @@ -328,6 +439,84 @@ impl EditPredictionProvider for FakeInlineCompletionProvider { false } + fn supports_jump_to_edit() -> bool { + true + } + + fn is_enabled( + &self, + _buffer: &gpui::Entity, + _cursor_position: language::Anchor, + _cx: &gpui::App, + ) -> bool { + true + } + + fn is_refreshing(&self) -> bool { + false + } + + fn refresh( + &mut self, + _project: Option>, + _buffer: gpui::Entity, + _cursor_position: language::Anchor, + _debounce: bool, + _cx: &mut gpui::Context, + ) { + } + + fn cycle( + &mut self, + _buffer: gpui::Entity, + _cursor_position: language::Anchor, + _direction: edit_prediction::Direction, + _cx: &mut gpui::Context, + ) { + } + + fn accept(&mut self, _cx: &mut gpui::Context) {} + + fn discard(&mut self, _cx: &mut gpui::Context) {} + + fn suggest<'a>( + &mut self, + _buffer: &gpui::Entity, + _cursor_position: language::Anchor, + _cx: &mut gpui::Context, + ) -> Option { + self.completion.clone() + } +} + +#[derive(Default, Clone)] +pub struct FakeNonZedEditPredictionProvider { + pub completion: Option, +} + +impl FakeNonZedEditPredictionProvider { + pub fn set_edit_prediction(&mut self, completion: Option) { + self.completion = completion; + } +} + +impl EditPredictionProvider for FakeNonZedEditPredictionProvider { + fn name() -> &'static str { + "fake-non-zed-provider" + } + + fn display_name() -> &'static str { + "Fake Non-Zed Provider" + } + + fn show_completions_in_menu() -> bool { + false + } + + fn supports_jump_to_edit() -> bool { + false + } + fn is_enabled( &self, _buffer: &gpui::Entity, @@ -355,7 +544,7 @@ impl EditPredictionProvider for FakeInlineCompletionProvider { &mut self, _buffer: gpui::Entity, _cursor_position: language::Anchor, - _direction: inline_completion::Direction, + _direction: edit_prediction::Direction, _cx: &mut gpui::Context, ) { } @@ -369,7 +558,7 @@ impl EditPredictionProvider for FakeInlineCompletionProvider { _buffer: &gpui::Entity, _cursor_position: language::Anchor, _cx: &mut gpui::Context, - ) -> Option { + ) -> Option { self.completion.clone() } } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 8276992734..29e009fdf8 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -29,6 +29,7 @@ mod inlay_hint_cache; pub mod items; mod jsx_tag_auto_close; mod linked_editing_ranges; +mod lsp_colors; mod lsp_ext; mod mouse_context_menu; pub mod movement; @@ -42,49 +43,65 @@ pub mod tasks; #[cfg(test)] mod code_completion_tests; #[cfg(test)] -mod editor_tests; +mod edit_prediction_tests; #[cfg(test)] -mod inline_completion_tests; +mod editor_tests; mod signature_help; #[cfg(any(test, feature = "test-support"))] pub mod test; pub(crate) use actions::*; -pub use actions::{AcceptEditPrediction, OpenExcerpts, OpenExcerptsSplit}; +pub use display_map::{ChunkRenderer, ChunkRendererContext, DisplayPoint, FoldPlaceholder}; +pub use edit_prediction::Direction; +pub use editor_settings::{ + CurrentLineHighlight, DocumentColorsRenderMode, EditorSettings, HideMouseMode, + ScrollBeyondLastLine, ScrollbarAxes, SearchSettings, ShowMinimap, ShowScrollbar, +}; +pub use editor_settings_controls::*; +pub use element::{ + CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, PointForPosition, +}; +pub use git::blame::BlameRenderer; +pub use hover_popover::hover_markdown_style; +pub use items::MAX_TAB_TITLE_LEN; +pub use lsp::CompletionContext; +pub use lsp_ext::lsp_tasks; +pub use multi_buffer::{ + Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, PathKey, + RowInfo, ToOffset, ToPoint, +}; +pub use proposed_changes_editor::{ + ProposedChangeLocation, ProposedChangesEditor, ProposedChangesEditorToolbar, +}; +pub use text::Bias; + +use ::git::{ + Restore, + blame::{BlameEntry, ParsedCommitMessage}, +}; use aho_corasick::AhoCorasick; use anyhow::{Context as _, Result, anyhow}; use blink_manager::BlinkManager; use buffer_diff::DiffHunkStatus; use client::{Collaborator, ParticipantIndex}; use clock::{AGENT_REPLICA_ID, ReplicaId}; -use collections::{BTreeMap, HashMap, HashSet, VecDeque}; -use convert_case::{Case, Casing}; -use dap::TelemetrySpawnLocation; -use display_map::*; -pub use display_map::{ChunkRenderer, ChunkRendererContext, DisplayPoint, FoldPlaceholder}; -pub use editor_settings::{ - CurrentLineHighlight, EditorSettings, HideMouseMode, ScrollBeyondLastLine, ScrollbarAxes, - SearchSettings, ShowScrollbar, -}; -use editor_settings::{GoToDefinitionFallback, Minimap as MinimapSettings}; -pub use editor_settings_controls::*; -use element::{AcceptEditPredictionBinding, LineWithInvisibles, PositionMap, layout_line}; -pub use element::{ - CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, PointForPosition, -}; -use feature_flags::{DebuggerFeatureFlag, FeatureFlagAppExt}; -use futures::{ - FutureExt, - future::{self, Shared, join}, -}; -use fuzzy::{StringMatch, StringMatchCandidate}; - -use ::git::blame::BlameEntry; -use ::git::{Restore, blame::ParsedCommitMessage}; use code_context_menus::{ AvailableCodeAction, CodeActionContents, CodeActionsItem, CodeActionsMenu, CodeContextMenu, CompletionsMenu, ContextMenuOrigin, }; +use collections::{BTreeMap, HashMap, HashSet, VecDeque}; +use convert_case::{Case, Casing}; +use dap::TelemetrySpawnLocation; +use display_map::*; +use edit_prediction::{EditPredictionProvider, EditPredictionProviderHandle}; +use editor_settings::{GoToDefinitionFallback, Minimap as MinimapSettings}; +use element::{AcceptEditPredictionBinding, LineWithInvisibles, PositionMap, layout_line}; +use futures::{ + FutureExt, StreamExt as _, + future::{self, Shared, join}, + stream::FuturesUnordered, +}; +use fuzzy::{StringMatch, StringMatchCandidate}; use git::blame::{GitBlame, GlobalBlameRenderer}; use gpui::{ Action, Animation, AnimationExt, AnyElement, App, AppContext, AsyncWindowContext, @@ -94,36 +111,46 @@ use gpui::{ MouseButton, MouseDownEvent, PaintQuad, ParentElement, Pixels, Render, ScrollHandle, SharedString, Size, Stateful, Styled, Subscription, Task, TextStyle, TextStyleRefinement, UTF16Selection, UnderlineStyle, UniformListScrollHandle, WeakEntity, WeakFocusHandle, Window, - div, impl_actions, point, prelude::*, pulsating_between, px, relative, size, + div, point, prelude::*, pulsating_between, px, relative, size, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_links::{HoverLink, HoveredLinkState, InlayHighlight, find_file}; -pub use hover_popover::hover_markdown_style; use hover_popover::{HoverState, hide_hover}; use indent_guides::ActiveIndentGuidesState; use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy}; -pub use inline_completion::Direction; -use inline_completion::{EditPredictionProvider, InlineCompletionProviderHandle}; -pub use items::MAX_TAB_TITLE_LEN; -use itertools::Itertools; +use itertools::{Either, Itertools}; use language::{ - AutoindentMode, BracketMatch, BracketPair, Buffer, Capability, CharKind, CodeLabel, - CursorShape, DiagnosticEntry, DiffOptions, DocumentationConfig, EditPredictionsMode, - EditPreview, HighlightedText, IndentKind, IndentSize, Language, OffsetRangeExt, Point, - Selection, SelectionGoal, TextObject, TransactionId, TreeSitterOptions, WordsQuery, + AutoindentMode, BlockCommentConfig, BracketMatch, BracketPair, Buffer, BufferRow, + BufferSnapshot, Capability, CharClassifier, CharKind, CodeLabel, CursorShape, DiagnosticEntry, + DiffOptions, EditPredictionsMode, EditPreview, HighlightedText, IndentKind, IndentSize, + Language, OffsetRangeExt, Point, Runnable, RunnableRange, Selection, SelectionGoal, TextObject, + TransactionId, TreeSitterOptions, WordsQuery, language_settings::{ self, InlayHintSettings, LspInsertMode, RewrapBehavior, WordsCompletionMode, all_language_settings, language_settings, }, - point_from_lsp, text_diff_with_options, + point_from_lsp, point_to_lsp, text_diff_with_options, }; -use language::{BufferRow, CharClassifier, Runnable, RunnableRange, point_to_lsp}; use linked_editing_ranges::refresh_linked_ranges; +use lsp::{ + CodeActionKind, CompletionItemKind, CompletionTriggerKind, InsertTextFormat, InsertTextMode, + LanguageServerId, +}; +use lsp_colors::LspColorData; use markdown::Markdown; use mouse_context_menu::MouseContextMenu; +use movement::TextLayoutDetails; +use multi_buffer::{ + ExcerptInfo, ExpandExcerptDirection, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow, + MultiOrSingleBufferOffsetRange, ToOffsetUtf16, +}; +use parking_lot::Mutex; use persistence::DB; use project::{ - BreakpointWithPosition, ProjectPath, + BreakpointWithPosition, CodeAction, Completion, CompletionIntent, CompletionResponse, + CompletionSource, DisableAiSettings, DocumentHighlight, InlayHint, Location, LocationLink, + PrepareRenameResponse, Project, ProjectItem, ProjectPath, ProjectTransaction, TaskSourceKind, + debugger::breakpoint_store::Breakpoint, debugger::{ breakpoint_store::{ BreakpointEditAction, BreakpointSessionState, BreakpointState, BreakpointStore, @@ -131,44 +158,13 @@ use project::{ }, session::{Session, SessionEvent}, }, - project_settings::DiagnosticSeverity, -}; - -pub use git::blame::BlameRenderer; -pub use proposed_changes_editor::{ - ProposedChangeLocation, ProposedChangesEditor, ProposedChangesEditorToolbar, -}; -use std::{cell::OnceCell, iter::Peekable, ops::Not}; -use task::{ResolvedTask, RunnableTag, TaskTemplate, TaskVariables}; - -pub use lsp::CompletionContext; -use lsp::{ - CodeActionKind, CompletionItemKind, CompletionTriggerKind, InsertTextFormat, InsertTextMode, - LanguageServerId, LanguageServerName, -}; - -use language::BufferSnapshot; -pub use lsp_ext::lsp_tasks; -use movement::TextLayoutDetails; -pub use multi_buffer::{ - Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, PathKey, - RowInfo, ToOffset, ToPoint, -}; -use multi_buffer::{ - ExcerptInfo, ExpandExcerptDirection, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow, - MultiOrSingleBufferOffsetRange, ToOffsetUtf16, -}; -use parking_lot::Mutex; -use project::{ - CodeAction, Completion, CompletionIntent, CompletionSource, DocumentHighlight, InlayHint, - Location, LocationLink, PrepareRenameResponse, Project, ProjectItem, ProjectTransaction, - TaskSourceKind, - debugger::breakpoint_store::Breakpoint, + git_store::{GitStoreEvent, RepositoryEvent}, lsp_store::{CompletionDocumentation, FormatTrigger, LspFormatTarget, OpenLspBufferHandle}, + project_settings::{DiagnosticSeverity, GoToDiagnosticSeverityFilter}, project_settings::{GitGutterSetting, ProjectSettings}, }; -use rand::prelude::*; -use rpc::{ErrorExt, proto::*}; +use rand::{seq::SliceRandom, thread_rng}; +use rpc::{ErrorCode, ErrorExt, proto::PeerId}; use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide}; use selections_collection::{ MutableSelectionsCollection, SelectionsCollection, resolve_selections, @@ -177,24 +173,27 @@ use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsLocation, SettingsStore, update_settings_file}; use smallvec::{SmallVec, smallvec}; use snippet::Snippet; -use std::sync::Arc; use std::{ any::TypeId, borrow::Cow, + cell::OnceCell, cell::RefCell, cmp::{self, Ordering, Reverse}, + iter::Peekable, mem, num::NonZeroU32, + ops::Not, ops::{ControlFlow, Deref, DerefMut, Range, RangeInclusive}, path::{Path, PathBuf}, rc::Rc, + sync::Arc, time::{Duration, Instant}, }; -pub use sum_tree::Bias; use sum_tree::TreeMap; +use task::{ResolvedTask, RunnableTag, TaskTemplate, TaskVariables}; use text::{BufferId, FromAnchor, OffsetUtf16, Rope}; use theme::{ - ActiveTheme, PlayerColor, StatusColors, SyntaxTheme, ThemeColors, ThemeSettings, + ActiveTheme, PlayerColor, StatusColors, SyntaxTheme, Theme, ThemeSettings, observe_buffer_font_size_adjustment, }; use ui::{ @@ -206,13 +205,17 @@ use workspace::{ CollaboratorId, Item as WorkspaceItem, ItemId, ItemNavHistory, OpenInTerminal, OpenTerminal, RestoreOnStartupBehavior, SERIALIZATION_THROTTLE_TIME, SplitDirection, TabBarSettings, Toast, ViewId, Workspace, WorkspaceId, WorkspaceSettings, - item::{ItemHandle, PreviewTabsSettings}, + item::{ItemHandle, PreviewTabsSettings, SaveOptions}, notifications::{DetachAndPromptErr, NotificationId, NotifyTaskExt}, searchable::SearchEvent, }; -use crate::hover_links::{find_url, find_url_from_range}; -use crate::signature_help::{SignatureHelpHiddenBy, SignatureHelpState}; +use crate::{ + code_context_menus::CompletionsMenuSource, + editor_settings::MultiCursorModifier, + hover_links::{find_url, find_url_from_range}, + signature_help::{SignatureHelpHiddenBy, SignatureHelpState}, +}; pub const FILE_HEADER_HEIGHT: u32 = 2; pub const MULTI_BUFFER_EXCERPT_HEADER_HEIGHT: u32 = 1; @@ -232,7 +235,6 @@ pub(crate) const SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT: Duration = Duration: pub(crate) const EDIT_PREDICTION_KEY_CONTEXT: &str = "edit_prediction"; pub(crate) const EDIT_PREDICTION_CONFLICT_KEY_CONTEXT: &str = "edit_prediction_conflict"; -pub(crate) const MIN_LINE_NUMBER_DIGITS: u32 = 4; pub(crate) const MINIMAP_FONT_SIZE: AbsoluteLength = AbsoluteLength::Pixels(px(2.)); pub type RenderDiffHunkControlsFn = Arc< @@ -248,13 +250,21 @@ pub type RenderDiffHunkControlsFn = Arc< ) -> AnyElement, >; -const COLUMNAR_SELECTION_MODIFIERS: Modifiers = Modifiers { - alt: true, - shift: true, - control: false, - platform: false, - function: false, -}; +enum ReportEditorEvent { + Saved { auto_saved: bool }, + EditorOpened, + Closed, +} + +impl ReportEditorEvent { + pub fn event_type(&self) -> &'static str { + match self { + Self::Saved { .. } => "Editor Saved", + Self::EditorOpened => "Editor Opened", + Self::Closed => "Editor Closed", + } + } +} struct InlineValueCache { enabled: bool, @@ -274,17 +284,20 @@ impl InlineValueCache { #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum InlayId { - InlineCompletion(usize), - Hint(usize), + EditPrediction(usize), DebuggerValue(usize), + // LSP + Hint(usize), + Color(usize), } impl InlayId { fn id(&self) -> usize { match self { - Self::InlineCompletion(id) => *id, - Self::Hint(id) => *id, + Self::EditPrediction(id) => *id, Self::DebuggerValue(id) => *id, + Self::Hint(id) => *id, + Self::Color(id) => *id, } } } @@ -294,6 +307,7 @@ pub enum DebugStackFrameLine {} enum DocumentHighlightRead {} enum DocumentHighlightWrite {} enum InputComposition {} +pub enum PendingInput {} enum SelectedTextHighlight {} pub enum ConflictsOuter {} @@ -352,6 +366,7 @@ pub fn init(cx: &mut App) { workspace.register_action(Editor::new_file_vertical); workspace.register_action(Editor::new_file_horizontal); workspace.register_action(Editor::cancel_language_server_work); + workspace.register_action(Editor::toggle_focus); }, ) .detach(); @@ -447,6 +462,7 @@ pub enum SelectPhase { BeginColumnar { position: DisplayPoint, reset: bool, + mode: ColumnarMode, goal_column: u32, }, Extend { @@ -461,6 +477,12 @@ pub enum SelectPhase { End, } +#[derive(Clone, Debug, PartialEq)] +pub enum ColumnarMode { + FromMouse, + FromSelection, +} + #[derive(Clone, Debug)] pub enum SelectMode { Character, @@ -471,11 +493,10 @@ pub enum SelectMode { #[derive(Clone, PartialEq, Eq, Debug)] pub enum EditorMode { - SingleLine { - auto_width: bool, - }, + SingleLine, AutoHeight { - max_lines: usize, + min_lines: usize, + max_lines: Option, }, Full { /// When set to `true`, the editor will scale its UI elements with the buffer font size. @@ -499,10 +520,17 @@ impl EditorMode { } } + #[inline] pub fn is_full(&self) -> bool { matches!(self, Self::Full { .. }) } + #[inline] + pub fn is_single_line(&self) -> bool { + matches!(self, Self::SingleLine { .. }) + } + + #[inline] fn is_minimap(&self) -> bool { matches!(self, Self::Minimap { .. }) } @@ -528,13 +556,14 @@ pub enum SoftWrap { #[derive(Clone)] pub struct EditorStyle { pub background: Hsla, + pub border: Hsla, pub local_player: PlayerColor, pub text: TextStyle, pub scrollbar_width: Pixels, pub syntax: Arc, pub status: StatusColors, pub inlay_hints_style: HighlightStyle, - pub inline_completion_styles: InlineCompletionStyles, + pub edit_prediction_styles: EditPredictionStyles, pub unnecessary_code_fade: f32, pub show_underlines: bool, } @@ -543,6 +572,7 @@ impl Default for EditorStyle { fn default() -> Self { Self { background: Hsla::default(), + border: Hsla::default(), local_player: PlayerColor::default(), text: TextStyle::default(), scrollbar_width: Pixels::default(), @@ -552,7 +582,7 @@ impl Default for EditorStyle { // style and retrieve them directly from the theme. status: StatusColors::dark(), inlay_hints_style: HighlightStyle::default(), - inline_completion_styles: InlineCompletionStyles { + edit_prediction_styles: EditPredictionStyles { insertion: HighlightStyle::default(), whitespace: HighlightStyle::default(), }, @@ -574,8 +604,8 @@ pub fn make_inlay_hints_style(cx: &mut App) -> HighlightStyle { } } -pub fn make_suggestion_styles(cx: &mut App) -> InlineCompletionStyles { - InlineCompletionStyles { +pub fn make_suggestion_styles(cx: &mut App) -> EditPredictionStyles { + EditPredictionStyles { insertion: HighlightStyle { color: Some(cx.theme().status().predictive), ..HighlightStyle::default() @@ -595,7 +625,7 @@ pub(crate) enum EditDisplayMode { Inline, } -enum InlineCompletion { +enum EditPrediction { Edit { edits: Vec<(Range, String)>, edit_preview: Option, @@ -608,9 +638,9 @@ enum InlineCompletion { }, } -struct InlineCompletionState { +struct EditPredictionState { inlay_ids: Vec, - completion: InlineCompletion, + completion: EditPrediction, completion_id: Option, invalidation_range: Range, } @@ -623,7 +653,7 @@ enum EditPredictionSettings { }, } -enum InlineCompletionHighlight {} +enum EditPredictionHighlight {} #[derive(Debug, Clone)] struct InlineDiagnostic { @@ -634,7 +664,7 @@ struct InlineDiagnostic { severity: lsp::DiagnosticSeverity, } -pub enum MenuInlineCompletionsPolicy { +pub enum MenuEditPredictionsPolicy { Never, ByProvider, } @@ -696,8 +726,8 @@ impl EditorActionId { // type GetFieldEditorTheme = dyn Fn(&theme::Theme) -> theme::FieldEditor; // type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option; -type BackgroundHighlight = (fn(&ThemeColors) -> Hsla, Arc<[Range]>); -type GutterHighlight = (fn(&App) -> Hsla, Arc<[Range]>); +type BackgroundHighlight = (fn(&Theme) -> Hsla, Arc<[Range]>); +type GutterHighlight = (fn(&App) -> Hsla, Vec>); #[derive(Default)] struct ScrollbarMarkerState { @@ -750,10 +780,7 @@ impl MinimapVisibility { } fn disabled(&self) -> bool { - match *self { - Self::Disabled => true, - _ => false, - } + matches!(*self, Self::Disabled) } fn settings_visibility(&self) -> bool { @@ -844,9 +871,19 @@ pub trait Addon: 'static { } } +struct ChangeLocation { + current: Option>, + original: Vec, +} +impl ChangeLocation { + fn locations(&self) -> &[Anchor] { + self.current.as_ref().unwrap_or(&self.original) + } +} + /// A set of caret positions, registered when the editor was edited. pub struct ChangeList { - changes: Vec>, + changes: Vec, /// Currently "selected" change. position: Option, } @@ -873,20 +910,38 @@ impl ChangeList { (prev + count).min(self.changes.len() - 1) }; self.position = Some(next); - self.changes.get(next).map(|anchors| anchors.as_slice()) + self.changes.get(next).map(|change| change.locations()) } /// Adds a new change to the list, resetting the change list position. - pub fn push_to_change_list(&mut self, pop_state: bool, new_positions: Vec) { + pub fn push_to_change_list(&mut self, group: bool, new_positions: Vec) { self.position.take(); - if pop_state { - self.changes.pop(); + if let Some(last) = self.changes.last_mut() + && group + { + last.current = Some(new_positions) + } else { + self.changes.push(ChangeLocation { + original: new_positions, + current: None, + }); } - self.changes.push(new_positions.clone()); } pub fn last(&self) -> Option<&[Anchor]> { - self.changes.last().map(|anchors| anchors.as_slice()) + self.changes.last().map(|change| change.locations()) + } + + pub fn last_before_grouping(&self) -> Option<&[Anchor]> { + self.changes.last().map(|change| change.original.as_slice()) + } + + pub fn invert_last_group(&mut self) { + if let Some(last) = self.changes.last_mut() + && let Some(current) = last.current.as_mut() + { + mem::swap(&mut last.original, current); + } } } @@ -899,15 +954,42 @@ struct InlineBlamePopoverState { struct InlineBlamePopover { position: gpui::Point, - show_task: Option>, hide_task: Option>, popover_bounds: Option>, popover_state: InlineBlamePopoverState, + keyboard_grace: bool, +} + +enum SelectionDragState { + /// State when no drag related activity is detected. + None, + /// State when the mouse is down on a selection that is about to be dragged. + ReadyToDrag { + selection: Selection, + click_position: gpui::Point, + mouse_down_time: Instant, + }, + /// State when the mouse is dragging the selection in the editor. + Dragging { + selection: Selection, + drop_cursor: Selection, + hide_drop_cursor: bool, + }, +} + +enum ColumnarSelectionState { + FromMouse { + selection_tail: Anchor, + display_point: Option, + }, + FromSelection { + selection_tail: Anchor, + }, } /// Represents a breakpoint indicator that shows up when hovering over lines in the gutter that don't have /// a breakpoint on them. -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] struct PhantomBreakpointIndicator { display_row: DisplayRow, /// There's a small debounce between hovering over the line and showing the indicator. @@ -915,6 +997,7 @@ struct PhantomBreakpointIndicator { is_active: bool, collides_with_existing_breakpoint: bool, } + /// Zed's primary implementation of text input, allowing users to edit a [`MultiBuffer`]. /// /// See the [module level documentation](self) for more information. @@ -931,7 +1014,7 @@ pub struct Editor { /// When inline assist editors are linked, they all render cursors because /// typing enters text into each of them, even the ones that aren't focused. pub(crate) show_cursor_when_unfocused: bool, - columnar_selection_tail: Option, + columnar_selection_state: Option, add_selections_state: Option, select_next_state: Option, select_prev_state: Option, @@ -947,12 +1030,11 @@ pub struct Editor { show_inline_diagnostics: bool, inline_diagnostics_update: Task<()>, inline_diagnostics_enabled: bool, + diagnostics_enabled: bool, inline_diagnostics: Vec<(Anchor, InlineDiagnostic)>, soft_wrap_mode_override: Option, hard_wrap: Option, - - // TODO: make this a access method - pub project: Option>, + project: Option>, semantics_provider: Option>, completion_provider: Option>, collaboration_hub: Option>, @@ -978,7 +1060,7 @@ pub struct Editor { placeholder_text: Option>, highlight_order: usize, highlighted_rows: HashMap>, - background_highlights: TreeMap, + background_highlights: TreeMap, gutter_highlights: TreeMap, scrollbar_marker_state: ScrollbarMarkerState, active_indent_guides_state: ActiveIndentGuidesState, @@ -986,8 +1068,9 @@ pub struct Editor { context_menu: RefCell>, context_menu_options: Option, mouse_context_menu: Option, - completion_tasks: Vec<(CompletionId, Task>)>, + completion_tasks: Vec<(CompletionId, Task<()>)>, inline_blame_popover: Option, + inline_blame_popover_show_task: Option>, signature_help_state: SignatureHelpState, auto_signature_help: Option, find_all_references_task_sources: Vec, @@ -1015,15 +1098,15 @@ pub struct Editor { pending_mouse_down: Option>>>, gutter_hovered: bool, hovered_link_state: Option, - edit_prediction_provider: Option, + edit_prediction_provider: Option, code_action_providers: Vec>, - active_inline_completion: Option, + active_edit_prediction: Option, /// Used to prevent flickering as the user types while the menu is open - stale_inline_completion_in_menu: Option, + stale_edit_prediction_in_menu: Option, edit_prediction_settings: EditPredictionSettings, - inline_completions_hidden_for_vim_mode: bool, - show_inline_completions_override: Option, - menu_inline_completions_policy: MenuInlineCompletionsPolicy, + edit_predictions_hidden_for_vim_mode: bool, + show_edit_predictions_override: Option, + menu_edit_predictions_policy: MenuEditPredictionsPolicy, edit_prediction_preview: EditPredictionPreview, edit_prediction_indent_conflict: bool, edit_prediction_requires_modifier_in_indent_conflict: bool, @@ -1035,8 +1118,9 @@ pub struct Editor { style: Option, text_style_refinement: Option, next_editor_action_id: EditorActionId, - editor_actions: - Rc)>>>>, + editor_actions: Rc< + RefCell)>>>, + >, use_autoclose: bool, use_auto_surround: bool, auto_replace_emoji_shortcode: bool, @@ -1068,6 +1152,8 @@ pub struct Editor { tasks_update_task: Option>, breakpoint_store: Option>, gutter_breakpoint_indicator: (Option, Option>), + hovered_diff_hunk_row: Option, + pull_diagnostics_task: Task<()>, in_project_search: bool, previous_search_ranges: Option]>>, breadcrumb_header: Option, @@ -1088,6 +1174,10 @@ pub struct Editor { hide_mouse_mode: HideMouseMode, pub change_list: ChangeList, inline_value_cache: InlineValueCache, + selection_drag_state: SelectionDragState, + next_color_inlay_id: usize, + colors: Option, + folding_newlines: Task<()>, } #[derive(Copy, Clone, Debug, PartialEq, Eq, Default)] @@ -1160,6 +1250,12 @@ impl GutterDimensions { } } +struct CharacterDimensions { + em_width: Pixels, + em_advance: Pixels, + line_height: Pixels, +} + #[derive(Debug)] pub struct RemoteSelection { pub replica_id: ReplicaId, @@ -1179,10 +1275,12 @@ struct SelectionHistoryEntry { add_selections_state: Option, } +#[derive(Copy, Clone, Debug, PartialEq, Eq)] enum SelectionHistoryMode { Normal, Undoing, Redoing, + Skipping, } #[derive(Clone, PartialEq, Eq, Hash)] @@ -1197,10 +1295,69 @@ impl Default for SelectionHistoryMode { } } +#[derive(Debug)] +/// SelectionEffects controls the side-effects of updating the selection. +/// +/// The default behaviour does "what you mostly want": +/// - it pushes to the nav history if the cursor moved by >10 lines +/// - it re-triggers completion requests +/// - it scrolls to fit +/// +/// You might want to modify these behaviours. For example when doing a "jump" +/// like go to definition, we always want to add to nav history; but when scrolling +/// in vim mode we never do. +/// +/// Similarly, you might want to disable scrolling if you don't want the viewport to +/// move. +#[derive(Clone)] +pub struct SelectionEffects { + nav_history: Option, + completions: bool, + scroll: Option, +} + +impl Default for SelectionEffects { + fn default() -> Self { + Self { + nav_history: None, + completions: true, + scroll: Some(Autoscroll::fit()), + } + } +} +impl SelectionEffects { + pub fn scroll(scroll: Autoscroll) -> Self { + Self { + scroll: Some(scroll), + ..Default::default() + } + } + + pub fn no_scroll() -> Self { + Self { + scroll: None, + ..Default::default() + } + } + + pub fn completions(self, completions: bool) -> Self { + Self { + completions, + ..self + } + } + + pub fn nav_history(self, nav_history: bool) -> Self { + Self { + nav_history: Some(nav_history), + ..self + } + } +} + struct DeferredSelectionEffectsState { changed: bool, - show_completions: bool, - autoscroll: Option, + effects: SelectionEffects, old_cursor_position: Anchor, history_entry: SelectionHistoryEntry, } @@ -1216,11 +1373,19 @@ struct SelectionHistory { } impl SelectionHistory { + #[track_caller] fn insert_transaction( &mut self, transaction_id: TransactionId, selections: Arc<[Selection]>, ) { + if selections.is_empty() { + log::error!( + "SelectionHistory::insert_transaction called with empty selections. Caller: {}", + std::panic::Location::caller() + ); + return; + } self.selections_by_transaction .insert(transaction_id, (selections, None)); } @@ -1250,6 +1415,7 @@ impl SelectionHistory { } SelectionHistoryMode::Undoing => self.push_redo(entry), SelectionHistoryMode::Redoing => self.push_undo(entry), + SelectionHistoryMode::Skipping => {} } } } @@ -1258,7 +1424,7 @@ impl SelectionHistory { if self .undo_stack .back() - .map_or(true, |e| e.selections != entry.selections) + .is_none_or(|e| e.selections != entry.selections) { self.undo_stack.push_back(entry); if self.undo_stack.len() > MAX_SELECTION_HISTORY_LEN { @@ -1271,7 +1437,7 @@ impl SelectionHistory { if self .redo_stack .back() - .map_or(true, |e| e.selections != entry.selections) + .is_none_or(|e| e.selections != entry.selections) { self.redo_stack.push_back(entry); if self.redo_stack.len() > MAX_SELECTION_HISTORY_LEN { @@ -1306,6 +1472,11 @@ struct RowHighlight { #[derive(Clone, Debug)] struct AddSelectionsState { + groups: Vec, +} + +#[derive(Clone, Debug)] +struct AddSelectionsGroup { above: bool, stack: Vec, } @@ -1350,8 +1521,8 @@ pub struct RenameState { struct InvalidationStack(Vec); -struct RegisteredInlineCompletionProvider { - provider: Arc, +struct RegisteredEditPredictionProvider { + provider: Arc, _subscription: Subscription, } @@ -1459,7 +1630,7 @@ impl InlayHintRefreshReason { } pub enum FormatTarget { - Buffers, + Buffers(HashSet>), Ranges(Vec>), } @@ -1497,13 +1668,7 @@ impl Editor { pub fn single_line(window: &mut Window, cx: &mut Context) -> Self { let buffer = cx.new(|cx| Buffer::local("", cx)); let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); - Self::new( - EditorMode::SingleLine { auto_width: false }, - buffer, - None, - window, - cx, - ) + Self::new(EditorMode::SingleLine, buffer, None, window, cx) } pub fn multi_line(window: &mut Window, cx: &mut Context) -> Self { @@ -1512,11 +1677,19 @@ impl Editor { Self::new(EditorMode::full(), buffer, None, window, cx) } - pub fn auto_width(window: &mut Window, cx: &mut Context) -> Self { + pub fn auto_height( + min_lines: usize, + max_lines: usize, + window: &mut Window, + cx: &mut Context, + ) -> Self { let buffer = cx.new(|cx| Buffer::local("", cx)); let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); Self::new( - EditorMode::SingleLine { auto_width: true }, + EditorMode::AutoHeight { + min_lines, + max_lines: Some(max_lines), + }, buffer, None, window, @@ -1524,11 +1697,20 @@ impl Editor { ) } - pub fn auto_height(max_lines: usize, window: &mut Window, cx: &mut Context) -> Self { + /// Creates a new auto-height editor with a minimum number of lines but no maximum. + /// The editor grows as tall as needed to fit its content. + pub fn auto_height_unbounded( + min_lines: usize, + window: &mut Window, + cx: &mut Context, + ) -> Self { let buffer = cx.new(|cx| Buffer::local("", cx)); let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); Self::new( - EditorMode::AutoHeight { max_lines }, + EditorMode::AutoHeight { + min_lines, + max_lines: None, + }, buffer, None, window, @@ -1597,10 +1779,11 @@ impl Editor { ) -> Self { debug_assert!( display_map.is_none() || mode.is_minimap(), - "Providing a display map for a new editor is only intended for the minimap and might have unindended side effects otherwise!" + "Providing a display map for a new editor is only intended for the minimap and might have unintended side effects otherwise!" ); let full_mode = mode.is_full(); + let is_minimap = mode.is_minimap(); let diagnostics_max_severity = if full_mode { EditorSettings::get_global(cx) .diagnostics_max_severity @@ -1661,90 +1844,174 @@ impl Editor { let selections = SelectionsCollection::new(display_map.clone(), buffer.clone()); - let blink_manager = cx.new(|cx| BlinkManager::new(CURSOR_BLINK_INTERVAL, cx)); + let blink_manager = cx.new(|cx| { + let mut blink_manager = BlinkManager::new(CURSOR_BLINK_INTERVAL, cx); + if is_minimap { + blink_manager.disable(cx); + } + blink_manager + }); - let soft_wrap_mode_override = matches!(mode, EditorMode::SingleLine { .. }) - .then(|| language_settings::SoftWrap::None); + let soft_wrap_mode_override = + matches!(mode, EditorMode::SingleLine).then(|| language_settings::SoftWrap::None); let mut project_subscriptions = Vec::new(); - if mode.is_full() { - if let Some(project) = project.as_ref() { - project_subscriptions.push(cx.subscribe_in( - project, - window, - |editor, _, event, window, cx| match event { - project::Event::RefreshCodeLens => { - // we always query lens with actions, without storing them, always refreshing them + if full_mode && let Some(project) = project.as_ref() { + project_subscriptions.push(cx.subscribe_in( + project, + window, + |editor, _, event, window, cx| match event { + project::Event::RefreshCodeLens => { + // we always query lens with actions, without storing them, always refreshing them + } + project::Event::RefreshInlayHints => { + editor.refresh_inlay_hints(InlayHintRefreshReason::RefreshRequested, cx); + } + project::Event::LanguageServerAdded(..) + | project::Event::LanguageServerRemoved(..) => { + if editor.tasks_update_task.is_none() { + editor.tasks_update_task = Some(editor.refresh_runnables(window, cx)); } - project::Event::RefreshInlayHints => { - editor - .refresh_inlay_hints(InlayHintRefreshReason::RefreshRequested, cx); - } - project::Event::LanguageServerAdded(..) - | project::Event::LanguageServerRemoved(..) => { - if editor.tasks_update_task.is_none() { - editor.tasks_update_task = - Some(editor.refresh_runnables(window, cx)); - } - } - project::Event::SnippetEdit(id, snippet_edits) => { - if let Some(buffer) = editor.buffer.read(cx).buffer(*id) { - let focus_handle = editor.focus_handle(cx); - if focus_handle.is_focused(window) { - let snapshot = buffer.read(cx).snapshot(); - for (range, snippet) in snippet_edits { - let editor_range = - language::range_from_lsp(*range).to_offset(&snapshot); - editor - .insert_snippet( - &[editor_range], - snippet.clone(), - window, - cx, - ) - .ok(); - } + } + project::Event::SnippetEdit(id, snippet_edits) => { + if let Some(buffer) = editor.buffer.read(cx).buffer(*id) { + let focus_handle = editor.focus_handle(cx); + if focus_handle.is_focused(window) { + let snapshot = buffer.read(cx).snapshot(); + for (range, snippet) in snippet_edits { + let editor_range = + language::range_from_lsp(*range).to_offset(&snapshot); + editor + .insert_snippet( + &[editor_range], + snippet.clone(), + window, + cx, + ) + .ok(); } } } - _ => {} - }, - )); - if let Some(task_inventory) = project - .read(cx) - .task_store() - .read(cx) - .task_inventory() - .cloned() - { - project_subscriptions.push(cx.observe_in( - &task_inventory, - window, - |editor, _, window, cx| { - editor.tasks_update_task = Some(editor.refresh_runnables(window, cx)); - }, - )); - }; - - project_subscriptions.push(cx.subscribe_in( - &project.read(cx).breakpoint_store(), - window, - |editor, _, event, window, cx| match event { - BreakpointStoreEvent::ClearDebugLines => { - editor.clear_row_highlights::(); - editor.refresh_inline_values(cx); + } + project::Event::LanguageServerBufferRegistered { buffer_id, .. } => { + if editor.buffer().read(cx).buffer(*buffer_id).is_some() { + editor.update_lsp_data(false, Some(*buffer_id), window, cx); } - BreakpointStoreEvent::SetDebugLine => { - if editor.go_to_active_debug_line(window, cx) { - cx.stop_propagation(); + } + + project::Event::EntryRenamed(transaction) => { + let Some(workspace) = editor.workspace() else { + return; + }; + let Some(active_editor) = workspace.read(cx).active_item_as::(cx) + else { + return; + }; + if active_editor.entity_id() == cx.entity_id() { + let edited_buffers_already_open = { + let other_editors: Vec> = workspace + .read(cx) + .panes() + .iter() + .flat_map(|pane| pane.read(cx).items_of_type::()) + .filter(|editor| editor.entity_id() != cx.entity_id()) + .collect(); + + transaction.0.keys().all(|buffer| { + other_editors.iter().any(|editor| { + let multi_buffer = editor.read(cx).buffer(); + multi_buffer.read(cx).is_singleton() + && multi_buffer.read(cx).as_singleton().map_or( + false, + |singleton| { + singleton.entity_id() == buffer.entity_id() + }, + ) + }) + }) + }; + + if !edited_buffers_already_open { + let workspace = workspace.downgrade(); + let transaction = transaction.clone(); + cx.defer_in(window, move |_, window, cx| { + cx.spawn_in(window, async move |editor, cx| { + Self::open_project_transaction( + &editor, + workspace, + transaction, + "Rename".to_string(), + cx, + ) + .await + .ok() + }) + .detach(); + }); } - - editor.refresh_inline_values(cx); } - _ => {} + } + + _ => {} + }, + )); + if let Some(task_inventory) = project + .read(cx) + .task_store() + .read(cx) + .task_inventory() + .cloned() + { + project_subscriptions.push(cx.observe_in( + &task_inventory, + window, + |editor, _, window, cx| { + editor.tasks_update_task = Some(editor.refresh_runnables(window, cx)); }, )); - } + }; + + project_subscriptions.push(cx.subscribe_in( + &project.read(cx).breakpoint_store(), + window, + |editor, _, event, window, cx| match event { + BreakpointStoreEvent::ClearDebugLines => { + editor.clear_row_highlights::(); + editor.refresh_inline_values(cx); + } + BreakpointStoreEvent::SetDebugLine => { + if editor.go_to_active_debug_line(window, cx) { + cx.stop_propagation(); + } + + editor.refresh_inline_values(cx); + } + _ => {} + }, + )); + let git_store = project.read(cx).git_store().clone(); + let project = project.clone(); + project_subscriptions.push(cx.subscribe(&git_store, move |this, _, event, cx| { + if let GitStoreEvent::RepositoryUpdated( + _, + RepositoryEvent::Updated { + new_instance: true, .. + }, + _, + ) = event + { + this.load_diff_task = Some( + update_uncommitted_diff_for_buffer( + cx.entity(), + &project, + this.buffer.read(cx).all_buffers(), + this.buffer.clone(), + cx, + ) + .shared(), + ); + } + })); } let buffer_snapshot = buffer.read(cx).snapshot(cx); @@ -1752,20 +2019,25 @@ impl Editor { let inlay_hint_settings = inlay_hint_settings(selections.newest_anchor().head(), &buffer_snapshot, cx); let focus_handle = cx.focus_handle(); - cx.on_focus(&focus_handle, window, Self::handle_focus) - .detach(); - cx.on_focus_in(&focus_handle, window, Self::handle_focus_in) - .detach(); - cx.on_focus_out(&focus_handle, window, Self::handle_focus_out) - .detach(); - cx.on_blur(&focus_handle, window, Self::handle_blur) - .detach(); + if !is_minimap { + cx.on_focus(&focus_handle, window, Self::handle_focus) + .detach(); + cx.on_focus_in(&focus_handle, window, Self::handle_focus_in) + .detach(); + cx.on_focus_out(&focus_handle, window, Self::handle_focus_out) + .detach(); + cx.on_blur(&focus_handle, window, Self::handle_blur) + .detach(); + cx.observe_pending_input(window, Self::observe_pending_input) + .detach(); + } - let show_indent_guides = if matches!(mode, EditorMode::SingleLine { .. }) { - Some(false) - } else { - None - }; + let show_indent_guides = + if matches!(mode, EditorMode::SingleLine | EditorMode::Minimap { .. }) { + Some(false) + } else { + None + }; let breakpoint_store = match (&mode, project.as_ref()) { (EditorMode::Full { .. }, Some(project)) => Some(project.read(cx).breakpoint_store()), @@ -1788,7 +2060,7 @@ impl Editor { code_action_providers.push(Rc::new(project) as Rc<_>); } - let mut this = Self { + let mut editor = Self { focus_handle, show_cursor_when_unfocused: false, last_focused_descendant: None, @@ -1796,7 +2068,7 @@ impl Editor { display_map: display_map.clone(), selections, scroll_manager: ScrollManager::new(cx), - columnar_selection_tail: None, + columnar_selection_state: None, add_selections_state: None, select_next_state: None, select_prev_state: None, @@ -1825,12 +2097,12 @@ impl Editor { vertical: full_mode, }, minimap_visibility: MinimapVisibility::for_mode(&mode, cx), - offset_content: !matches!(mode, EditorMode::SingleLine { .. }), + offset_content: !matches!(mode, EditorMode::SingleLine), show_breadcrumbs: EditorSettings::get_global(cx).toolbar.breadcrumbs, - show_gutter: mode.is_full(), - show_line_numbers: None, + show_gutter: full_mode, + show_line_numbers: (!full_mode).then_some(false), use_relative_line_numbers: None, - disable_expand_excerpt_buttons: false, + disable_expand_excerpt_buttons: !full_mode, show_git_diff_gutter: None, show_code_actions: None, show_runnables: None, @@ -1850,6 +2122,7 @@ impl Editor { mouse_context_menu: None, completion_tasks: Vec::new(), inline_blame_popover: None, + inline_blame_popover_show_task: None, signature_help_state: SignatureHelpState::default(), auto_signature_help: None, find_all_references_task_sources: Vec::new(), @@ -1863,7 +2136,7 @@ impl Editor { document_highlights_task: None, linked_editing_range_task: None, pending_rename: None, - searchable: true, + searchable: !is_minimap, cursor_shape: EditorSettings::get_global(cx) .cursor_shape .unwrap_or_default(), @@ -1871,9 +2144,9 @@ impl Editor { autoindent_mode: Some(AutoindentMode::EachLine), collapse_matches: false, workspace: None, - input_enabled: true, - use_modal_editing: mode.is_full(), - read_only: mode.is_minimap(), + input_enabled: !is_minimap, + use_modal_editing: full_mode, + read_only: is_minimap, use_autoclose: true, use_auto_surround: true, auto_replace_emoji_shortcode: false, @@ -1884,15 +2157,15 @@ impl Editor { pending_mouse_down: None, hovered_link_state: None, edit_prediction_provider: None, - active_inline_completion: None, - stale_inline_completion_in_menu: None, + active_edit_prediction: None, + stale_edit_prediction_in_menu: None, edit_prediction_preview: EditPredictionPreview::Inactive { released_too_fast: false, }, - inline_diagnostics_enabled: mode.is_full(), + inline_diagnostics_enabled: full_mode, + diagnostics_enabled: full_mode, inline_value_cache: InlineValueCache::new(inlay_hint_settings.show_value_hints), inlay_hint_cache: InlayHintCache::new(inlay_hint_settings), - gutter_hovered: false, pixel_position_of_newest_cursor: None, last_bounds: None, @@ -1904,9 +2177,9 @@ impl Editor { hovered_cursors: HashMap::default(), next_editor_action_id: EditorActionId::default(), editor_actions: Rc::default(), - inline_completions_hidden_for_vim_mode: false, - show_inline_completions_override: None, - menu_inline_completions_policy: MenuInlineCompletionsPolicy::ByProvider, + edit_predictions_hidden_for_vim_mode: false, + show_edit_predictions_override: None, + menu_edit_predictions_policy: MenuEditPredictionsPolicy::ByProvider, edit_prediction_settings: EditPredictionSettings::Disabled, edit_prediction_indent_conflict: false, edit_prediction_requires_modifier_in_indent_conflict: true, @@ -1915,9 +2188,10 @@ impl Editor { show_git_blame_inline: false, show_selection_menu: None, show_git_blame_inline_delay_task: None, - git_blame_inline_enabled: ProjectSettings::get_global(cx).git.inline_blame_enabled(), + git_blame_inline_enabled: full_mode + && ProjectSettings::get_global(cx).git.inline_blame_enabled(), render_diff_hunk_controls: Arc::new(render_diff_hunk_controls), - serialize_dirty_buffers: !mode.is_minimap() + serialize_dirty_buffers: !is_minimap && ProjectSettings::get_global(cx) .session .restore_unsaved_buffers, @@ -1927,28 +2201,36 @@ impl Editor { breakpoint_store, gutter_breakpoint_indicator: (None, None), - _subscriptions: vec![ - cx.observe(&buffer, Self::on_buffer_changed), - cx.subscribe_in(&buffer, window, Self::on_buffer_event), - cx.observe_in(&display_map, window, Self::on_display_map_changed), - cx.observe(&blink_manager, |_, _, cx| cx.notify()), - cx.observe_global_in::(window, Self::settings_changed), - observe_buffer_font_size_adjustment(cx, |_, cx| cx.notify()), - cx.observe_window_activation(window, |editor, window, cx| { - let active = window.is_window_active(); - editor.blink_manager.update(cx, |blink_manager, cx| { - if active { - blink_manager.enable(cx); - } else { - blink_manager.disable(cx); - } - }); - if active { - editor.show_mouse_cursor(); - } - }), - ], + hovered_diff_hunk_row: None, + _subscriptions: (!is_minimap) + .then(|| { + vec![ + cx.observe(&buffer, Self::on_buffer_changed), + cx.subscribe_in(&buffer, window, Self::on_buffer_event), + cx.observe_in(&display_map, window, Self::on_display_map_changed), + cx.observe(&blink_manager, |_, _, cx| cx.notify()), + cx.observe_global_in::(window, Self::settings_changed), + observe_buffer_font_size_adjustment(cx, |_, cx| cx.notify()), + cx.observe_window_activation(window, |editor, window, cx| { + let active = window.is_window_active(); + editor.blink_manager.update(cx, |blink_manager, cx| { + if active { + blink_manager.enable(cx); + } else { + blink_manager.disable(cx); + } + }); + if active { + editor.show_mouse_cursor(cx); + } + }), + ] + }) + .unwrap_or_default(), tasks_update_task: None, + pull_diagnostics_task: Task::ready(()), + colors: None, + next_color_inlay_id: 0, linked_edit_ranges: Default::default(), in_project_search: false, previous_search_ranges: None, @@ -1972,17 +2254,25 @@ impl Editor { .unwrap_or_default(), change_list: ChangeList::new(), mode, + selection_drag_state: SelectionDragState::None, + folding_newlines: Task::ready(()), }; - if let Some(breakpoints) = this.breakpoint_store.as_ref() { - this._subscriptions + + if is_minimap { + return editor; + } + + if let Some(breakpoints) = editor.breakpoint_store.as_ref() { + editor + ._subscriptions .push(cx.observe(breakpoints, |_, _, cx| { cx.notify(); })); } - this.tasks_update_task = Some(this.refresh_runnables(window, cx)); - this._subscriptions.extend(project_subscriptions); + editor.tasks_update_task = Some(editor.refresh_runnables(window, cx)); + editor._subscriptions.extend(project_subscriptions); - this._subscriptions.push(cx.subscribe_in( + editor._subscriptions.push(cx.subscribe_in( &cx.entity(), window, |editor, _, e: &EditorEvent, window, cx| match e { @@ -2027,14 +2317,15 @@ impl Editor { }, )); - if let Some(dap_store) = this + if let Some(dap_store) = editor .project .as_ref() .map(|project| project.read(cx).dap_store()) { let weak_editor = cx.weak_entity(); - this._subscriptions + editor + ._subscriptions .push( cx.observe_new::(move |_, _, cx| { let session_entity = cx.entity(); @@ -2049,40 +2340,52 @@ impl Editor { ); for session in dap_store.read(cx).sessions().cloned().collect::>() { - this._subscriptions + editor + ._subscriptions .push(cx.subscribe(&session, Self::on_debug_session_event)); } } - this.end_selection(window, cx); - this.scroll_manager.show_scrollbars(window, cx); - jsx_tag_auto_close::refresh_enabled_in_any_buffer(&mut this, &buffer, cx); + // skip adding the initial selection to selection history + editor.selection_history.mode = SelectionHistoryMode::Skipping; + editor.end_selection(window, cx); + editor.selection_history.mode = SelectionHistoryMode::Normal; + + editor.scroll_manager.show_scrollbars(window, cx); + jsx_tag_auto_close::refresh_enabled_in_any_buffer(&mut editor, &buffer, cx); if full_mode { let should_auto_hide_scrollbars = cx.should_auto_hide_scrollbars(); cx.set_global(ScrollbarAutoHide(should_auto_hide_scrollbars)); - if this.git_blame_inline_enabled { - this.start_git_blame_inline(false, window, cx); + if editor.git_blame_inline_enabled { + editor.start_git_blame_inline(false, window, cx); } - this.go_to_active_debug_line(window, cx); + editor.go_to_active_debug_line(window, cx); - if let Some(buffer) = buffer.read(cx).as_singleton() { - if let Some(project) = this.project.as_ref() { - let handle = project.update(cx, |project, cx| { - project.register_buffer_with_language_servers(&buffer, cx) - }); - this.registered_buffers - .insert(buffer.read(cx).remote_id(), handle); - } + if let Some(buffer) = buffer.read(cx).as_singleton() + && let Some(project) = editor.project() + { + let handle = project.update(cx, |project, cx| { + project.register_buffer_with_language_servers(&buffer, cx) + }); + editor + .registered_buffers + .insert(buffer.read(cx).remote_id(), handle); } - this.minimap = this.create_minimap(EditorSettings::get_global(cx).minimap, window, cx); + editor.minimap = + editor.create_minimap(EditorSettings::get_global(cx).minimap, window, cx); + editor.colors = Some(LspColorData::new(cx)); + editor.update_lsp_data(false, None, window, cx); } - this.report_editor_event("Editor Opened", None, cx); - this + if editor.mode.is_full() { + editor.report_editor_event(ReportEditorEvent::EditorOpened, None, cx); + } + + editor } pub fn deploy_mouse_context_menu( @@ -2107,8 +2410,36 @@ impl Editor { .is_some_and(|menu| menu.context_menu.focus_handle(cx).is_focused(window)) } + pub fn is_range_selected(&mut self, range: &Range, cx: &mut Context) -> bool { + if self + .selections + .pending + .as_ref() + .is_some_and(|pending_selection| { + let snapshot = self.buffer().read(cx).snapshot(cx); + pending_selection + .selection + .range() + .includes(range, &snapshot) + }) + { + return true; + } + + self.selections + .disjoint_in_range::(range.clone(), cx) + .into_iter() + .any(|selection| { + // This is needed to cover a corner case, if we just check for an existing + // selection in the fold range, having a cursor at the start of the fold + // marks it as selected. Non-empty selections don't cause this. + let length = selection.end - selection.start; + length > 0 + }) + } + pub fn key_context(&self, window: &Window, cx: &App) -> KeyContext { - self.key_context_internal(self.has_active_inline_completion(), window, cx) + self.key_context_internal(self.has_active_edit_prediction(), window, cx) } fn key_context_internal( @@ -2120,7 +2451,7 @@ impl Editor { let mut key_context = KeyContext::new_with_defaults(); key_context.add("Editor"); let mode = match self.mode { - EditorMode::SingleLine { .. } => "single_line", + EditorMode::SingleLine => "single_line", EditorMode::AutoHeight { .. } => "auto_height", EditorMode::Minimap { .. } => "minimap", EditorMode::Full { .. } => "full", @@ -2136,17 +2467,25 @@ impl Editor { } match self.context_menu.borrow().as_ref() { - Some(CodeContextMenu::Completions(_)) => { - key_context.add("menu"); - key_context.add("showing_completions"); + Some(CodeContextMenu::Completions(menu)) => { + if menu.visible() { + key_context.add("menu"); + key_context.add("showing_completions"); + } } - Some(CodeContextMenu::CodeActions(_)) => { - key_context.add("menu"); - key_context.add("showing_code_actions") + Some(CodeContextMenu::CodeActions(menu)) => { + if menu.visible() { + key_context.add("menu"); + key_context.add("showing_code_actions") + } } None => {} } + if self.signature_help_state.has_multiple_signatures() { + key_context.add("showing_signature_help"); + } + // Disable vim contexts when a sub-editor (e.g. rename/inline assistant) is focused. if !self.focus_handle(cx).contains_focused(window, cx) || (self.is_focused(window) || self.mouse_menu_is_focused(window, cx)) @@ -2184,12 +2523,15 @@ impl Editor { key_context } - fn show_mouse_cursor(&mut self) { - self.mouse_cursor_hidden = false; + fn show_mouse_cursor(&mut self, cx: &mut Context) { + if self.mouse_cursor_hidden { + self.mouse_cursor_hidden = false; + cx.notify(); + } } - pub fn hide_mouse_cursor(&mut self, origin: &HideMouseCursorOrigin) { - self.mouse_cursor_hidden = match origin { + pub fn hide_mouse_cursor(&mut self, origin: HideMouseCursorOrigin, cx: &mut Context) { + let hide_mouse_cursor = match origin { HideMouseCursorOrigin::TypingAction => { matches!( self.hide_mouse_mode, @@ -2200,6 +2542,10 @@ impl Editor { matches!(self.hide_mouse_mode, HideMouseMode::OnTypingAndMovement) } }; + if self.mouse_cursor_hidden != hide_mouse_cursor { + self.mouse_cursor_hidden = hide_mouse_cursor; + cx.notify(); + } } pub fn edit_prediction_in_conflict(&self) -> bool { @@ -2211,9 +2557,7 @@ impl Editor { .context_menu .borrow() .as_ref() - .map_or(false, |context| { - matches!(context, CodeContextMenu::Completions(_)) - }); + .is_some_and(|context| matches!(context, CodeContextMenu::Completions(_))); showing_completions || self.edit_prediction_requires_modifier() @@ -2224,31 +2568,28 @@ impl Editor { pub fn accept_edit_prediction_keybind( &self, + accept_partial: bool, window: &Window, cx: &App, ) -> AcceptEditPredictionBinding { let key_context = self.key_context_internal(true, window, cx); let in_conflict = self.edit_prediction_in_conflict(); - AcceptEditPredictionBinding( - window - .bindings_for_action_in_context(&AcceptEditPrediction, key_context) - .into_iter() - .filter(|binding| { - !in_conflict - || binding - .keystrokes() - .first() - .map_or(false, |keystroke| keystroke.modifiers.modified()) - }) - .rev() - .min_by_key(|binding| { - binding - .keystrokes() - .first() - .map_or(u8::MAX, |k| k.modifiers.number_of_modifiers()) - }), - ) + let bindings = if accept_partial { + window.bindings_for_action_in_context(&AcceptPartialEditPrediction, key_context) + } else { + window.bindings_for_action_in_context(&AcceptEditPrediction, key_context) + }; + + // TODO: if the binding contains multiple keystrokes, display all of them, not + // just the first one. + AcceptEditPredictionBinding(bindings.into_iter().rev().find(|binding| { + !in_conflict + || binding + .keystrokes() + .first() + .is_some_and(|keystroke| keystroke.modifiers.modified()) + })) } pub fn new_file( @@ -2350,6 +2691,10 @@ impl Editor { &self.buffer } + pub fn project(&self) -> Option<&Entity> { + self.project.as_ref() + } + pub fn workspace(&self) -> Option> { self.workspace.as_ref()?.0.upgrade() } @@ -2447,6 +2792,11 @@ impl Editor { self.completion_provider = provider; } + #[cfg(any(test, feature = "test-support"))] + pub fn completion_provider(&self) -> Option> { + self.completion_provider.clone() + } + pub fn semantics_provider(&self) -> Option> { self.semantics_provider.clone() } @@ -2463,17 +2813,16 @@ impl Editor { ) where T: EditPredictionProvider, { - self.edit_prediction_provider = - provider.map(|provider| RegisteredInlineCompletionProvider { - _subscription: cx.observe_in(&provider, window, |this, _, window, cx| { - if this.focus_handle.is_focused(window) { - this.update_visible_inline_completion(window, cx); - } - }), - provider: Arc::new(provider), - }); + self.edit_prediction_provider = provider.map(|provider| RegisteredEditPredictionProvider { + _subscription: cx.observe_in(&provider, window, |this, _, window, cx| { + if this.focus_handle.is_focused(window) { + this.update_visible_edit_prediction(window, cx); + } + }), + provider: Arc::new(provider), + }); self.update_edit_prediction_settings(cx); - self.refresh_inline_completion(false, false, window, cx); + self.refresh_edit_prediction(false, false, window, cx); } pub fn placeholder_text(&self) -> Option<&str> { @@ -2544,24 +2893,24 @@ impl Editor { self.input_enabled = input_enabled; } - pub fn set_inline_completions_hidden_for_vim_mode( + pub fn set_edit_predictions_hidden_for_vim_mode( &mut self, hidden: bool, window: &mut Window, cx: &mut Context, ) { - if hidden != self.inline_completions_hidden_for_vim_mode { - self.inline_completions_hidden_for_vim_mode = hidden; + if hidden != self.edit_predictions_hidden_for_vim_mode { + self.edit_predictions_hidden_for_vim_mode = hidden; if hidden { - self.update_visible_inline_completion(window, cx); + self.update_visible_edit_prediction(window, cx); } else { - self.refresh_inline_completion(true, false, window, cx); + self.refresh_edit_prediction(true, false, window, cx); } } } - pub fn set_menu_inline_completions_policy(&mut self, value: MenuInlineCompletionsPolicy) { - self.menu_inline_completions_policy = value; + pub fn set_menu_edit_predictions_policy(&mut self, value: MenuEditPredictionsPolicy) { + self.menu_edit_predictions_policy = value; } pub fn set_autoindent(&mut self, autoindent: bool) { @@ -2598,7 +2947,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if self.show_inline_completions_override.is_some() { + if self.show_edit_predictions_override.is_some() { self.set_show_edit_predictions(None, window, cx); } else { let show_edit_predictions = !self.edit_predictions_enabled(); @@ -2612,17 +2961,17 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.show_inline_completions_override = show_edit_predictions; + self.show_edit_predictions_override = show_edit_predictions; self.update_edit_prediction_settings(cx); if let Some(false) = show_edit_predictions { - self.discard_inline_completion(false, cx); + self.discard_edit_prediction(false, cx); } else { - self.refresh_inline_completion(false, true, window, cx); + self.refresh_edit_prediction(false, true, window, cx); } } - fn inline_completions_disabled_in_scope( + fn edit_predictions_disabled_in_scope( &self, buffer: &Entity, buffer_position: language::Anchor, @@ -2635,7 +2984,7 @@ impl Editor { return false; }; - scope.override_name().map_or(false, |scope_name| { + scope.override_name().is_some_and(|scope_name| { settings .edit_predictions_disabled_in .iter() @@ -2655,7 +3004,7 @@ impl Editor { &mut self, local: bool, old_cursor_position: &Anchor, - show_completions: bool, + effects: SelectionEffects, window: &mut Window, cx: &mut Context, ) { @@ -2684,10 +3033,12 @@ impl Editor { } } + let selection_anchors = self.selections.disjoint_anchors(); + if self.focus_handle.is_focused(window) && self.leader_id.is_none() { self.buffer.update(cx, |buffer, cx| { buffer.set_active_selections( - &self.selections.disjoint_anchors(), + &selection_anchors, self.selections.line_mode, self.cursor_shape, cx, @@ -2698,94 +3049,81 @@ impl Editor { .display_map .update(cx, |display_map, cx| display_map.snapshot(cx)); let buffer = &display_map.buffer_snapshot; - self.add_selections_state = None; + if self.selections.count() == 1 { + self.add_selections_state = None; + } self.select_next_state = None; self.select_prev_state = None; self.select_syntax_node_history.try_clear(); - self.invalidate_autoclose_regions(&self.selections.disjoint_anchors(), buffer); - self.snippet_stack - .invalidate(&self.selections.disjoint_anchors(), buffer); + self.invalidate_autoclose_regions(&selection_anchors, buffer); + self.snippet_stack.invalidate(&selection_anchors, buffer); self.take_rename(false, window, cx); - let new_cursor_position = self.selections.newest_anchor().head(); + let newest_selection = self.selections.newest_anchor(); + let new_cursor_position = newest_selection.head(); + let selection_start = newest_selection.start; - self.push_to_nav_history( - *old_cursor_position, - Some(new_cursor_position.to_point(buffer)), - false, - cx, - ); + if effects.nav_history.is_none() || effects.nav_history == Some(true) { + self.push_to_nav_history( + *old_cursor_position, + Some(new_cursor_position.to_point(buffer)), + false, + effects.nav_history == Some(true), + cx, + ); + } if local { - let new_cursor_position = self.selections.newest_anchor().head(); + if let Some(buffer_id) = new_cursor_position.buffer_id + && !self.registered_buffers.contains_key(&buffer_id) + && let Some(project) = self.project.as_ref() + { + project.update(cx, |project, cx| { + let Some(buffer) = self.buffer.read(cx).buffer(buffer_id) else { + return; + }; + self.registered_buffers.insert( + buffer_id, + project.register_buffer_with_language_servers(&buffer, cx), + ); + }) + } + let mut context_menu = self.context_menu.borrow_mut(); let completion_menu = match context_menu.as_ref() { Some(CodeContextMenu::Completions(menu)) => Some(menu), - _ => { + Some(CodeContextMenu::CodeActions(_)) => { *context_menu = None; None } + None => None, }; - if let Some(buffer_id) = new_cursor_position.buffer_id { - if !self.registered_buffers.contains_key(&buffer_id) { - if let Some(project) = self.project.as_ref() { - project.update(cx, |project, cx| { - let Some(buffer) = self.buffer.read(cx).buffer(buffer_id) else { - return; - }; - self.registered_buffers.insert( - buffer_id, - project.register_buffer_with_language_servers(&buffer, cx), - ); - }) - } - } - } + let completion_position = completion_menu.map(|menu| menu.initial_position); + drop(context_menu); - if let Some(completion_menu) = completion_menu { - let cursor_position = new_cursor_position.to_offset(buffer); - let (word_range, kind) = - buffer.surrounding_word(completion_menu.initial_position, true); - if kind == Some(CharKind::Word) - && word_range.to_inclusive().contains(&cursor_position) - { - let mut completion_menu = completion_menu.clone(); - drop(context_menu); - - let query = Self::completion_query(buffer, cursor_position); - let completion_provider = self.completion_provider.clone(); - cx.spawn_in(window, async move |this, cx| { - completion_menu - .filter(query.as_deref(), completion_provider, this.clone(), cx) - .await; - - this.update(cx, |this, cx| { - let mut context_menu = this.context_menu.borrow_mut(); - let Some(CodeContextMenu::Completions(menu)) = context_menu.as_ref() - else { - return; - }; - - if menu.id > completion_menu.id { - return; - } - - *context_menu = Some(CodeContextMenu::Completions(completion_menu)); - drop(context_menu); - cx.notify(); - }) - }) - .detach(); - - if show_completions { - self.show_completions(&ShowCompletions { trigger: None }, window, cx); + if effects.completions + && let Some(completion_position) = completion_position + { + let start_offset = selection_start.to_offset(buffer); + let position_matches = start_offset == completion_position.to_offset(buffer); + let continue_showing = if position_matches { + if self.snippet_stack.is_empty() { + buffer.char_kind_before(start_offset, true) == Some(CharKind::Word) + } else { + // Snippet choices can be shown even when the cursor is in whitespace. + // Dismissing the menu with actions like backspace is handled by + // invalidation regions. + true } } else { - drop(context_menu); + false + }; + + if continue_showing { + self.show_completions(&ShowCompletions { trigger: None }, window, cx); + } else { self.hide_context_menu(window, cx); } - } else { - drop(context_menu); } hide_hover(self, cx); @@ -2799,7 +3137,7 @@ impl Editor { self.refresh_document_highlights(cx); self.refresh_selected_text_highlights(false, window, cx); refresh_matching_bracket_highlights(self, window, cx); - self.update_visible_inline_completion(window, cx); + self.update_visible_edit_prediction(window, cx); self.edit_prediction_requires_modifier_in_indent_conflict = true; linked_editing_ranges::refresh_linked_ranges(self, window, cx); self.inline_blame_popover.take(); @@ -2815,48 +3153,43 @@ impl Editor { if selections.len() == 1 { cx.emit(SearchEvent::ActiveMatchChanged) } - if local { - if let Some((_, _, buffer_snapshot)) = buffer.as_singleton() { - let inmemory_selections = selections - .iter() - .map(|s| { - text::ToPoint::to_point(&s.range().start.text_anchor, buffer_snapshot) - ..text::ToPoint::to_point(&s.range().end.text_anchor, buffer_snapshot) - }) - .collect(); - self.update_restoration_data(cx, |data| { - data.selections = inmemory_selections; - }); + if local && let Some((_, _, buffer_snapshot)) = buffer.as_singleton() { + let inmemory_selections = selections + .iter() + .map(|s| { + text::ToPoint::to_point(&s.range().start.text_anchor, buffer_snapshot) + ..text::ToPoint::to_point(&s.range().end.text_anchor, buffer_snapshot) + }) + .collect(); + self.update_restoration_data(cx, |data| { + data.selections = inmemory_selections; + }); - if WorkspaceSettings::get(None, cx).restore_on_startup - != RestoreOnStartupBehavior::None - { - if let Some(workspace_id) = - self.workspace.as_ref().and_then(|workspace| workspace.1) - { - let snapshot = self.buffer().read(cx).snapshot(cx); - let selections = selections.clone(); - let background_executor = cx.background_executor().clone(); - let editor_id = cx.entity().entity_id().as_u64() as ItemId; - self.serialize_selections = cx.background_spawn(async move { - background_executor.timer(SERIALIZATION_THROTTLE_TIME).await; - let db_selections = selections - .iter() - .map(|selection| { - ( - selection.start.to_offset(&snapshot), - selection.end.to_offset(&snapshot), - ) - }) - .collect(); + if WorkspaceSettings::get(None, cx).restore_on_startup != RestoreOnStartupBehavior::None + && let Some(workspace_id) = + self.workspace.as_ref().and_then(|workspace| workspace.1) + { + let snapshot = self.buffer().read(cx).snapshot(cx); + let selections = selections.clone(); + let background_executor = cx.background_executor().clone(); + let editor_id = cx.entity().entity_id().as_u64() as ItemId; + self.serialize_selections = cx.background_spawn(async move { + background_executor.timer(SERIALIZATION_THROTTLE_TIME).await; + let db_selections = selections + .iter() + .map(|selection| { + ( + selection.start.to_offset(&snapshot), + selection.end.to_offset(&snapshot), + ) + }) + .collect(); - DB.save_editor_selections(editor_id, workspace_id, db_selections) - .await - .with_context(|| format!("persisting editor selections for editor {editor_id}, workspace {workspace_id:?}")) - .log_err(); - }); - } - } + DB.save_editor_selections(editor_id, workspace_id, db_selections) + .await + .with_context(|| format!("persisting editor selections for editor {editor_id}, workspace {workspace_id:?}")) + .log_err(); + }); } } @@ -2933,35 +3266,31 @@ impl Editor { selections.select_anchors(other_selections); }); - let other_subscription = - cx.subscribe(&other, |this, other, other_evt, cx| match other_evt { - EditorEvent::SelectionsChanged { local: true } => { - let other_selections = other.read(cx).selections.disjoint.to_vec(); - if other_selections.is_empty() { - return; - } - this.selections.change_with(cx, |selections| { - selections.select_anchors(other_selections); - }); + let other_subscription = cx.subscribe(&other, |this, other, other_evt, cx| { + if let EditorEvent::SelectionsChanged { local: true } = other_evt { + let other_selections = other.read(cx).selections.disjoint.to_vec(); + if other_selections.is_empty() { + return; } - _ => {} - }); + this.selections.change_with(cx, |selections| { + selections.select_anchors(other_selections); + }); + } + }); - let this_subscription = - cx.subscribe_self::(move |this, this_evt, cx| match this_evt { - EditorEvent::SelectionsChanged { local: true } => { - let these_selections = this.selections.disjoint.to_vec(); - if these_selections.is_empty() { - return; - } - other.update(cx, |other_editor, cx| { - other_editor.selections.change_with(cx, |selections| { - selections.select_anchors(these_selections); - }) - }); + let this_subscription = cx.subscribe_self::(move |this, this_evt, cx| { + if let EditorEvent::SelectionsChanged { local: true } = this_evt { + let these_selections = this.selections.disjoint.to_vec(); + if these_selections.is_empty() { + return; } - _ => {} - }); + other.update(cx, |other_editor, cx| { + other_editor.selections.change_with(cx, |selections| { + selections.select_anchors(these_selections); + }) + }); + } + }); Subscription::join(other_subscription, this_subscription) } @@ -2971,43 +3300,22 @@ impl Editor { /// effects of selection change occur at the end of the transaction. pub fn change_selections( &mut self, - autoscroll: Option, - window: &mut Window, - cx: &mut Context, - change: impl FnOnce(&mut MutableSelectionsCollection<'_>) -> R, - ) -> R { - self.change_selections_inner(true, autoscroll, window, cx, change) - } - - pub(crate) fn change_selections_without_showing_completions( - &mut self, - autoscroll: Option, - window: &mut Window, - cx: &mut Context, - change: impl FnOnce(&mut MutableSelectionsCollection<'_>) -> R, - ) -> R { - self.change_selections_inner(false, autoscroll, window, cx, change) - } - - fn change_selections_inner( - &mut self, - show_completions: bool, - autoscroll: Option, + effects: SelectionEffects, window: &mut Window, cx: &mut Context, change: impl FnOnce(&mut MutableSelectionsCollection<'_>) -> R, ) -> R { if let Some(state) = &mut self.deferred_selection_effects_state { - state.autoscroll = autoscroll.or(state.autoscroll); - state.show_completions = show_completions; + state.effects.scroll = effects.scroll.or(state.effects.scroll); + state.effects.completions = effects.completions; + state.effects.nav_history = effects.nav_history.or(state.effects.nav_history); let (changed, result) = self.selections.change_with(cx, change); state.changed |= changed; return result; } let mut state = DeferredSelectionEffectsState { changed: false, - show_completions, - autoscroll, + effects, old_cursor_position: self.selections.newest_anchor().head(), history_entry: SelectionHistoryEntry { selections: self.selections.disjoint_anchors(), @@ -3057,21 +3365,15 @@ impl Editor { if state.changed { self.selection_history.push(state.history_entry); - if let Some(autoscroll) = state.autoscroll { + if let Some(autoscroll) = state.effects.scroll { self.request_autoscroll(autoscroll, cx); } let old_cursor_position = &state.old_cursor_position; - self.selections_did_change( - true, - &old_cursor_position, - state.show_completions, - window, - cx, - ); + self.selections_did_change(true, old_cursor_position, state.effects, window, cx); - if self.should_open_signature_help_automatically(&old_cursor_position, cx) { + if self.should_open_signature_help_automatically(old_cursor_position, cx) { self.show_signature_help(&ShowSignatureHelp, window, cx); } } @@ -3144,7 +3446,8 @@ impl Editor { position, goal_column, reset, - } => self.begin_columnar_selection(position, goal_column, reset, window, cx), + mode, + } => self.begin_columnar_selection(position, goal_column, reset, mode, window, cx), SelectPhase::Extend { position, click_count, @@ -3189,9 +3492,13 @@ impl Editor { _ => {} } - let auto_scroll = EditorSettings::get_global(cx).autoscroll_on_clicks; + let effects = if EditorSettings::get_global(cx).autoscroll_on_clicks { + SelectionEffects::scroll(Autoscroll::fit()) + } else { + SelectionEffects::no_scroll() + }; - self.change_selections(auto_scroll.then(Autoscroll::fit), window, cx, |s| { + self.change_selections(effects, window, cx, |s| { s.set_pending(pending_selection, pending_mode) }); } @@ -3225,9 +3532,12 @@ impl Editor { auto_scroll = true; } 2 => { - let range = movement::surrounding_word(&display_map, position); - start = buffer.anchor_before(range.start.to_point(&display_map)); - end = buffer.anchor_before(range.end.to_point(&display_map)); + let position = display_map + .clip_point(position, Bias::Left) + .to_offset(&display_map, Bias::Left); + let (range, _) = buffer.surrounding_word(position, false); + start = buffer.anchor_before(range.start); + end = buffer.anchor_before(range.end); mode = SelectMode::Word(start..end); auto_scroll = true; } @@ -3274,8 +3584,13 @@ impl Editor { }; let selections_count = self.selections.count(); + let effects = if auto_scroll { + SelectionEffects::default() + } else { + SelectionEffects::no_scroll() + }; - self.change_selections(auto_scroll.then(Autoscroll::newest), window, cx, |s| { + self.change_selections(effects, window, cx, |s| { if let Some(point_to_delete) = point_to_delete { s.delete(point_to_delete); @@ -3297,6 +3612,7 @@ impl Editor { position: DisplayPoint, goal_column: u32, reset: bool, + mode: ColumnarMode, window: &mut Window, cx: &mut Context, ) { @@ -3312,27 +3628,42 @@ impl Editor { .buffer_snapshot .anchor_before(position.to_point(&display_map)); - self.change_selections(Some(Autoscroll::newest()), window, cx, |s| { - s.clear_disjoint(); - s.set_pending_anchor_range( - pointer_position..pointer_position, - SelectMode::Character, - ); - }); - } - - let tail = self.selections.newest::(cx).tail(); - self.columnar_selection_tail = Some(display_map.buffer_snapshot.anchor_before(tail)); - - if !reset { - self.select_columns( - tail.to_display_point(&display_map), - position, - goal_column, - &display_map, + self.change_selections( + SelectionEffects::scroll(Autoscroll::newest()), window, cx, + |s| { + s.clear_disjoint(); + s.set_pending_anchor_range( + pointer_position..pointer_position, + SelectMode::Character, + ); + }, ); + }; + + let tail = self.selections.newest::(cx).tail(); + let selection_anchor = display_map.buffer_snapshot.anchor_before(tail); + self.columnar_selection_state = match mode { + ColumnarMode::FromMouse => Some(ColumnarSelectionState::FromMouse { + selection_tail: selection_anchor, + display_point: if reset { + if position.column() != goal_column { + Some(DisplayPoint::new(position.row(), goal_column)) + } else { + None + } + } else { + None + }, + }), + ColumnarMode::FromSelection => Some(ColumnarSelectionState::FromSelection { + selection_tail: selection_anchor, + }), + }; + + if !reset { + self.select_columns(position, goal_column, &display_map, window, cx); } } @@ -3346,41 +3677,42 @@ impl Editor { ) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - if let Some(tail) = self.columnar_selection_tail.as_ref() { - let tail = tail.to_display_point(&display_map); - self.select_columns(tail, position, goal_column, &display_map, window, cx); + if self.columnar_selection_state.is_some() { + self.select_columns(position, goal_column, &display_map, window, cx); } else if let Some(mut pending) = self.selections.pending_anchor() { - let buffer = self.buffer.read(cx).snapshot(cx); + let buffer = &display_map.buffer_snapshot; let head; let tail; let mode = self.selections.pending_mode().unwrap(); match &mode { SelectMode::Character => { head = position.to_point(&display_map); - tail = pending.tail().to_point(&buffer); + tail = pending.tail().to_point(buffer); } SelectMode::Word(original_range) => { - let original_display_range = original_range.start.to_display_point(&display_map) - ..original_range.end.to_display_point(&display_map); - let original_buffer_range = original_display_range.start.to_point(&display_map) - ..original_display_range.end.to_point(&display_map); - if movement::is_inside_word(&display_map, position) - || original_display_range.contains(&position) + let offset = display_map + .clip_point(position, Bias::Left) + .to_offset(&display_map, Bias::Left); + let original_range = original_range.to_offset(buffer); + + let head_offset = if buffer.is_inside_word(offset, false) + || original_range.contains(&offset) { - let word_range = movement::surrounding_word(&display_map, position); - if word_range.start < original_display_range.start { - head = word_range.start.to_point(&display_map); + let (word_range, _) = buffer.surrounding_word(offset, false); + if word_range.start < original_range.start { + word_range.start } else { - head = word_range.end.to_point(&display_map); + word_range.end } } else { - head = position.to_point(&display_map); - } + offset + }; - if head <= original_buffer_range.start { - tail = original_buffer_range.end; + head = head_offset.to_point(buffer); + if head_offset <= original_range.start { + tail = original_range.end.to_point(buffer); } else { - tail = original_buffer_range.start; + tail = original_range.start.to_point(buffer); } } SelectMode::Line(original_range) => { @@ -3422,7 +3754,7 @@ impl Editor { pending.reversed = false; } - self.change_selections(None, window, cx, |s| { + self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.set_pending(pending, mode); }); } else { @@ -3435,10 +3767,10 @@ impl Editor { } fn end_selection(&mut self, window: &mut Window, cx: &mut Context) { - self.columnar_selection_tail.take(); + self.columnar_selection_state.take(); if self.selections.pending_anchor().is_some() { let selections = self.selections.all::(cx); - self.change_selections(None, window, cx, |s| { + self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select(selections); s.clear_pending(); }); @@ -3447,13 +3779,26 @@ impl Editor { fn select_columns( &mut self, - tail: DisplayPoint, head: DisplayPoint, goal_column: u32, display_map: &DisplaySnapshot, window: &mut Window, cx: &mut Context, ) { + let Some(columnar_state) = self.columnar_selection_state.as_ref() else { + return; + }; + + let tail = match columnar_state { + ColumnarSelectionState::FromMouse { + selection_tail, + display_point, + } => display_point.unwrap_or_else(|| selection_tail.to_display_point(display_map)), + ColumnarSelectionState::FromSelection { selection_tail } => { + selection_tail.to_display_point(display_map) + } + }; + let start_row = cmp::min(tail.row(), head.row()); let end_row = cmp::max(tail.row(), head.row()); let start_column = cmp::min(tail.column(), goal_column); @@ -3463,7 +3808,10 @@ impl Editor { let selection_ranges = (start_row.0..=end_row.0) .map(DisplayRow) .filter_map(|row| { - if start_column <= display_map.line_len(row) && !display_map.is_block_line(row) { + if (matches!(columnar_state, ColumnarSelectionState::FromMouse { .. }) + || start_column <= display_map.line_len(row)) + && !display_map.is_block_line(row) + { let start = display_map .clip_point(DisplayPoint::new(row, start_column), Bias::Left) .to_point(display_map); @@ -3481,8 +3829,23 @@ impl Editor { }) .collect::>(); - self.change_selections(None, window, cx, |s| { - s.select_ranges(selection_ranges); + let ranges = match columnar_state { + ColumnarSelectionState::FromMouse { .. } => { + let mut non_empty_ranges = selection_ranges + .iter() + .filter(|selection_range| selection_range.start != selection_range.end) + .peekable(); + if non_empty_ranges.peek().is_some() { + non_empty_ranges.cloned().collect() + } else { + selection_ranges + } + } + _ => selection_ranges, + }; + + self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges(ranges); }); cx.notify(); } @@ -3501,15 +3864,16 @@ impl Editor { }; pending_nonempty_selection - || (self.columnar_selection_tail.is_some() && self.selections.disjoint.len() > 1) + || (self.columnar_selection_state.is_some() && self.selections.disjoint.len() > 1) } pub fn has_pending_selection(&self) -> bool { - self.selections.pending_anchor().is_some() || self.columnar_selection_tail.is_some() + self.selections.pending_anchor().is_some() || self.columnar_selection_state.is_some() } pub fn cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context) { self.selection_mark_mode = false; + self.selection_drag_state = SelectionDragState::None; if self.clear_expanded_diff_hunks(cx) { cx.notify(); @@ -3520,7 +3884,7 @@ impl Editor { } if self.mode.is_full() - && self.change_selections(Some(Autoscroll::fit()), window, cx, |s| s.try_cancel()) + && self.change_selections(Default::default(), window, cx, |s| s.try_cancel()) { return; } @@ -3554,7 +3918,7 @@ impl Editor { return true; } - if is_user_requested && self.discard_inline_completion(true, cx) { + if is_user_requested && self.discard_edit_prediction(true, cx) { return true; } @@ -3634,7 +3998,7 @@ impl Editor { return; } - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let selections = self.selections.all_adjusted(cx); let mut bracket_inserted = false; @@ -3687,8 +4051,10 @@ impl Editor { bracket_pair_matching_end = Some(pair.clone()); } } - if bracket_pair.is_none() && bracket_pair_matching_end.is_some() { - bracket_pair = Some(bracket_pair_matching_end.unwrap()); + if let Some(end) = bracket_pair_matching_end + && bracket_pair.is_none() + { + bracket_pair = Some(end); is_bracket_pair_end = true; } } @@ -3706,18 +4072,18 @@ impl Editor { let following_text_allows_autoclose = snapshot .chars_at(selection.start) .next() - .map_or(true, |c| scope.should_autoclose_before(c)); + .is_none_or(|c| scope.should_autoclose_before(c)); let preceding_text_allows_autoclose = selection.start.column == 0 - || snapshot.reversed_chars_at(selection.start).next().map_or( - true, - |c| { + || snapshot + .reversed_chars_at(selection.start) + .next() + .is_none_or(|c| { bracket_pair.start != bracket_pair.end || !snapshot .char_classifier_at(selection.start) .is_word(c) - }, - ); + }); let is_closing_quote = if bracket_pair.end == bracket_pair.start && bracket_pair.start.len() == 1 @@ -3761,7 +4127,8 @@ impl Editor { // then don't insert that closing bracket again; just move the selection // past the closing bracket. let should_skip = selection.end == region.range.end.to_point(&snapshot) - && text.as_ref() == region.pair.end.as_str(); + && text.as_ref() == region.pair.end.as_str() + && snapshot.contains_str_at(region.range.end, text.as_ref()); if should_skip { let anchor = snapshot.anchor_after(selection.end); new_selections @@ -3816,42 +4183,38 @@ impl Editor { if self.auto_replace_emoji_shortcode && selection.is_empty() && text.as_ref().ends_with(':') - { - if let Some(possible_emoji_short_code) = + && let Some(possible_emoji_short_code) = Self::find_possible_emoji_shortcode_at_position(&snapshot, selection.start) - { - if !possible_emoji_short_code.is_empty() { - if let Some(emoji) = emojis::get_by_shortcode(&possible_emoji_short_code) { - let emoji_shortcode_start = Point::new( - selection.start.row, - selection.start.column - possible_emoji_short_code.len() as u32 - 1, - ); + && !possible_emoji_short_code.is_empty() + && let Some(emoji) = emojis::get_by_shortcode(&possible_emoji_short_code) + { + let emoji_shortcode_start = Point::new( + selection.start.row, + selection.start.column - possible_emoji_short_code.len() as u32 - 1, + ); - // Remove shortcode from buffer - edits.push(( - emoji_shortcode_start..selection.start, - "".to_string().into(), - )); - new_selections.push(( - Selection { - id: selection.id, - start: snapshot.anchor_after(emoji_shortcode_start), - end: snapshot.anchor_before(selection.start), - reversed: selection.reversed, - goal: selection.goal, - }, - 0, - )); + // Remove shortcode from buffer + edits.push(( + emoji_shortcode_start..selection.start, + "".to_string().into(), + )); + new_selections.push(( + Selection { + id: selection.id, + start: snapshot.anchor_after(emoji_shortcode_start), + end: snapshot.anchor_before(selection.start), + reversed: selection.reversed, + goal: selection.goal, + }, + 0, + )); - // Insert emoji - let selection_start_anchor = snapshot.anchor_after(selection.start); - new_selections.push((selection.map(|_| selection_start_anchor), 0)); - edits.push((selection.start..selection.end, emoji.to_string().into())); + // Insert emoji + let selection_start_anchor = snapshot.anchor_after(selection.start); + new_selections.push((selection.map(|_| selection_start_anchor), 0)); + edits.push((selection.start..selection.end, emoji.to_string().into())); - continue; - } - } - } + continue; } // If not handling any auto-close operation, then just replace the selected @@ -3861,7 +4224,7 @@ impl Editor { if !self.linked_edit_ranges.is_empty() { let start_anchor = snapshot.anchor_before(selection.start); - let is_word_char = text.chars().next().map_or(true, |char| { + let is_word_char = text.chars().next().is_none_or(|char| { let classifier = snapshot .char_classifier_at(start_anchor.to_offset(&snapshot)) .ignore_punctuation(true); @@ -3957,20 +4320,19 @@ impl Editor { ); } - let had_active_inline_completion = this.has_active_inline_completion(); - this.change_selections_without_showing_completions( - Some(Autoscroll::fit()), + let had_active_edit_prediction = this.has_active_edit_prediction(); + this.change_selections( + SelectionEffects::scroll(Autoscroll::fit()).completions(false), window, cx, |s| s.select(new_selections), ); - if !bracket_inserted { - if let Some(on_type_format_task) = + if !bracket_inserted + && let Some(on_type_format_task) = this.trigger_on_type_formatting(text.to_string(), window, cx) - { - on_type_format_task.detach_and_log_err(cx); - } + { + on_type_format_task.detach_and_log_err(cx); } let editor_settings = EditorSettings::get_global(cx); @@ -3982,7 +4344,7 @@ impl Editor { } let trigger_in_words = - this.show_edit_predictions_in_menu() || !had_active_inline_completion; + this.show_edit_predictions_in_menu() || !had_active_edit_prediction; if this.hard_wrap.is_some() { let latest: Range = this.selections.newest(cx).range(); if latest.is_empty() @@ -4004,7 +4366,7 @@ impl Editor { } this.trigger_completion_on_input(&text, trigger_in_words, window, cx); linked_editing_ranges::refresh_linked_ranges(this, window, cx); - this.refresh_inline_completion(true, false, window, cx); + this.refresh_edit_prediction(true, false, window, cx); jsx_tag_auto_close::handle_from(this, initial_buffer_versions, window, cx); }); } @@ -4057,7 +4419,7 @@ impl Editor { } pub fn newline(&mut self, _: &Newline, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.transact(window, cx, |this, window, cx| { let (edits_with_flags, selection_info): (Vec<_>, Vec<_>) = { let selections = this.selections.all::(cx); @@ -4106,7 +4468,7 @@ impl Editor { .take_while(|c| c.is_whitespace()) .count(); let comment_candidate = snapshot - .chars_for_range(range) + .chars_for_range(range.clone()) .skip(num_of_whitespaces) .take(max_len_of_delimiter) .collect::(); @@ -4122,6 +4484,24 @@ impl Editor { }) .max_by_key(|(_, len)| *len)?; + if let Some(BlockCommentConfig { + start: block_start, .. + }) = language.block_comment() + { + let block_start_trimmed = block_start.trim_end(); + if block_start_trimmed.starts_with(delimiter.trim_end()) { + let line_content = snapshot + .chars_for_range(range) + .skip(num_of_whitespaces) + .take(block_start_trimmed.len()) + .collect::(); + + if line_content.starts_with(block_start_trimmed) { + return None; + } + } + } + let cursor_is_placed_after_comment_marker = num_of_whitespaces + trimmed_len <= start_point.column as usize; if cursor_is_placed_after_comment_marker { @@ -4143,13 +4523,12 @@ impl Editor { return None; } - let DocumentationConfig { + let BlockCommentConfig { start: start_tag, end: end_tag, prefix: delimiter, tab_size: len, - } = language.documentation()?; - + } = language.documentation_comment()?; let is_within_block_comment = buffer .language_scope_at(start_point) .is_some_and(|scope| scope.override_name() == Some("comment")); @@ -4199,7 +4578,7 @@ impl Editor { let mut char_position = 0u32; let mut end_tag_offset = None; - 'outer: for chunk in snapshot.text_for_range(range.clone()) { + 'outer: for chunk in snapshot.text_for_range(range) { if let Some(byte_pos) = chunk.find(&**end_tag) { let chars_before_match = chunk[..byte_pos].chars().count() as u32; @@ -4219,7 +4598,7 @@ impl Editor { let cursor_is_at_start_of_end_tag = column == end_tag_offset; if cursor_is_at_start_of_end_tag { - indent_on_extra_newline.len = (*len).into(); + indent_on_extra_newline.len = *len; } } cursor_is_before_end_tag @@ -4232,7 +4611,7 @@ impl Editor { && cursor_is_before_end_tag_if_exists { if cursor_is_after_start_tag { - indent_on_newline.len = (*len).into(); + indent_on_newline.len = *len; } Some(delimiter.clone()) } else { @@ -4321,15 +4700,13 @@ impl Editor { }) .collect(); - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(new_selections) - }); - this.refresh_inline_completion(true, false, window, cx); + this.change_selections(Default::default(), window, cx, |s| s.select(new_selections)); + this.refresh_edit_prediction(true, false, window, cx); }); } pub fn newline_above(&mut self, _: &NewlineAbove, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let buffer = self.buffer.read(cx); let snapshot = buffer.snapshot(cx); @@ -4352,7 +4729,7 @@ impl Editor { self.transact(window, cx, |editor, window, cx| { editor.edit(edits, cx); - editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + editor.change_selections(Default::default(), window, cx, |s| { let mut index = 0; s.move_cursors_with(|map, _, _| { let row = rows[index]; @@ -4388,7 +4765,7 @@ impl Editor { } pub fn newline_below(&mut self, _: &NewlineBelow, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let buffer = self.buffer.read(cx); let snapshot = buffer.snapshot(cx); @@ -4414,7 +4791,7 @@ impl Editor { self.transact(window, cx, |editor, window, cx| { editor.edit(edits, cx); - editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + editor.change_selections(Default::default(), window, cx, |s| { let mut index = 0; s.move_cursors_with(|map, _, _| { let row = rows[index]; @@ -4491,7 +4868,7 @@ impl Editor { anchors }); - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + this.change_selections(Default::default(), window, cx, |s| { s.select_anchors(selection_anchors); }); @@ -4506,30 +4883,40 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - let ignore_completion_provider = self + let completions_source = self .context_menu .borrow() .as_ref() - .map(|menu| match menu { - CodeContextMenu::Completions(completions_menu) => { - completions_menu.ignore_completion_provider - } - CodeContextMenu::CodeActions(_) => false, - }) - .unwrap_or(false); + .and_then(|menu| match menu { + CodeContextMenu::Completions(completions_menu) => Some(completions_menu.source), + CodeContextMenu::CodeActions(_) => None, + }); - if ignore_completion_provider { - self.show_word_completions(&ShowWordCompletions, window, cx); - } else if self.is_completion_trigger(text, trigger_in_words, cx) { - self.show_completions( - &ShowCompletions { - trigger: Some(text.to_owned()).filter(|x| !x.is_empty()), - }, - window, - cx, - ); - } else { - self.hide_context_menu(window, cx); + match completions_source { + Some(CompletionsMenuSource::Words) => { + self.show_word_completions(&ShowWordCompletions, window, cx) + } + Some(CompletionsMenuSource::Normal) + | Some(CompletionsMenuSource::SnippetChoices) + | None + if self.is_completion_trigger( + text, + trigger_in_words, + completions_source.is_some(), + cx, + ) => + { + self.show_completions( + &ShowCompletions { + trigger: Some(text.to_owned()).filter(|x| !x.is_empty()), + }, + window, + cx, + ) + } + _ => { + self.hide_context_menu(window, cx); + } } } @@ -4537,14 +4924,11 @@ impl Editor { &self, text: &str, trigger_in_words: bool, + menu_is_open: bool, cx: &mut Context, ) -> bool { let position = self.selections.newest_anchor().head(); - let multibuffer = self.buffer.read(cx); - let Some(buffer) = position - .buffer_id - .and_then(|buffer_id| multibuffer.buffer(buffer_id).clone()) - else { + let Some(buffer) = self.buffer.read(cx).buffer_for_anchor(position, cx) else { return false; }; @@ -4554,6 +4938,7 @@ impl Editor { position.text_anchor, text, trigger_in_words, + menu_is_open, cx, ) } else { @@ -4623,7 +5008,7 @@ impl Editor { .collect(); drop(buffer); - self.change_selections(None, window, cx, |selections| { + self.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| { selections.select(new_selections) }); } @@ -4660,13 +5045,17 @@ impl Editor { }) } - /// Remove any autoclose regions that no longer contain their selection. + /// Remove any autoclose regions that no longer contain their selection or have invalid anchors in ranges. fn invalidate_autoclose_regions( &mut self, mut selections: &[Selection], buffer: &MultiBufferSnapshot, ) { self.autoclose_regions.retain(|state| { + if !state.range.start.is_valid(buffer) || !state.range.end.is_valid(buffer) { + return false; + } + let mut i = 0; while let Some(selection) = selections.get(i) { if selection.end.cmp(&state.range.start, buffer).is_lt() { @@ -4741,6 +5130,15 @@ impl Editor { .collect() } + #[cfg(any(test, feature = "test-support"))] + pub fn all_inlays(&self, cx: &App) -> Vec { + self.display_map + .read(cx) + .current_inlays() + .cloned() + .collect() + } + fn refresh_inlay_hints(&mut self, reason: InlayHintRefreshReason, cx: &mut Context) { if self.semantics_provider.is_none() || !self.mode.is_full() { return; @@ -4841,7 +5239,7 @@ impl Editor { to_insert, }) = self.inlay_hint_cache.spawn_hint_refresh( reason_description, - self.excerpts_for_inlay_hints_query(required_languages.as_ref(), cx), + self.visible_excerpts(required_languages.as_ref(), cx), invalidate_cache, ignore_debounce, cx, @@ -4859,12 +5257,12 @@ impl Editor { .collect() } - pub fn excerpts_for_inlay_hints_query( + pub fn visible_excerpts( &self, restrict_to_languages: Option<&HashSet>>, cx: &mut Context, ) -> HashMap, clock::Global, Range)> { - let Some(project) = self.project.as_ref() else { + let Some(project) = self.project() else { return HashMap::default(); }; let project = project.read(cx); @@ -4896,10 +5294,10 @@ impl Editor { } let language = buffer.language()?; - if let Some(restrict_to_languages) = restrict_to_languages { - if !restrict_to_languages.contains(language) { - return None; - } + if let Some(restrict_to_languages) = restrict_to_languages + && !restrict_to_languages.contains(language) + { + return None; } Some(( excerpt_id, @@ -4946,7 +5344,7 @@ impl Editor { return None; } - let project = self.project.as_ref()?; + let project = self.project()?; let position = self.selections.newest_anchor().head(); let (buffer, buffer_position) = self .buffer @@ -5004,7 +5402,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.open_completions_menu(true, None, window, cx); + self.open_or_update_completions_menu(Some(CompletionsMenuSource::Words), None, window, cx); } pub fn show_completions( @@ -5013,12 +5411,12 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.open_completions_menu(false, options.trigger.as_deref(), window, cx); + self.open_or_update_completions_menu(None, options.trigger.as_deref(), window, cx); } - fn open_completions_menu( + fn open_or_update_completions_menu( &mut self, - ignore_completion_provider: bool, + requested_source: Option, trigger: Option<&str>, window: &mut Window, cx: &mut Context, @@ -5026,11 +5424,17 @@ impl Editor { if self.pending_rename.is_some() { return; } - if !self.snippet_stack.is_empty() && self.context_menu.borrow().as_ref().is_some() { - return; - } - let position = self.selections.newest_anchor().head(); + let multibuffer_snapshot = self.buffer.read(cx).read(cx); + + // Typically `start` == `end`, but with snippet tabstop choices the default choice is + // inserted and selected. To handle that case, the start of the selection is used so that + // the menu starts with all choices. + let position = self + .selections + .newest_anchor() + .start + .bias_right(&multibuffer_snapshot); if position.diff_base_anchor.is_some() { return; } @@ -5041,11 +5445,28 @@ impl Editor { return; }; let buffer_snapshot = buffer.read(cx).snapshot(); - let show_completion_documentation = buffer_snapshot - .settings_at(buffer_position, cx) - .show_completion_documentation; - let query = Self::completion_query(&self.buffer.read(cx).read(cx), position); + let query: Option> = + Self::completion_query(&multibuffer_snapshot, position).map(|query| query.into()); + + drop(multibuffer_snapshot); + + let provider = match requested_source { + Some(CompletionsMenuSource::Normal) | None => self.completion_provider.clone(), + Some(CompletionsMenuSource::Words) => None, + Some(CompletionsMenuSource::SnippetChoices) => { + log::error!("bug: SnippetChoices requested_source is not handled"); + None + } + }; + + let sort_completions = provider + .as_ref() + .is_some_and(|provider| provider.sort_completions()); + + let filter_completions = provider + .as_ref() + .is_none_or(|provider| provider.filter_completions()); let trigger_kind = match trigger { Some(trigger) if buffer.read(cx).completion_triggers().contains(trigger) => { @@ -5064,14 +5485,59 @@ impl Editor { trigger_kind, }; - let (old_range, word_kind) = buffer_snapshot.surrounding_word(buffer_position); - let (old_range, word_to_exclude) = if word_kind == Some(CharKind::Word) { + // Hide the current completions menu when a trigger char is typed. Without this, cached + // completions from before the trigger char may be reused (#32774). Snippet choices could + // involve trigger chars, so this is skipped in that case. + if trigger_kind == CompletionTriggerKind::TRIGGER_CHARACTER && self.snippet_stack.is_empty() + { + let menu_is_open = matches!( + self.context_menu.borrow().as_ref(), + Some(CodeContextMenu::Completions(_)) + ); + if menu_is_open { + self.hide_context_menu(window, cx); + } + } + + if let Some(CodeContextMenu::Completions(menu)) = self.context_menu.borrow_mut().as_mut() { + if filter_completions { + menu.filter(query.clone(), provider.clone(), window, cx); + } + // When `is_incomplete` is false, no need to re-query completions when the current query + // is a suffix of the initial query. + if !menu.is_incomplete { + // If the new query is a suffix of the old query (typing more characters) and + // the previous result was complete, the existing completions can be filtered. + // + // Note that this is always true for snippet completions. + let query_matches = match (&menu.initial_query, &query) { + (Some(initial_query), Some(query)) => query.starts_with(initial_query.as_ref()), + (None, _) => true, + _ => false, + }; + if query_matches { + let position_matches = if menu.initial_position == position { + true + } else { + let snapshot = self.buffer.read(cx).read(cx); + menu.initial_position.to_offset(&snapshot) == position.to_offset(&snapshot) + }; + if position_matches { + return; + } + } + } + }; + + let (word_replace_range, word_to_exclude) = if let (word_range, Some(CharKind::Word)) = + buffer_snapshot.surrounding_word(buffer_position, false) + { let word_to_exclude = buffer_snapshot - .text_for_range(old_range.clone()) + .text_for_range(word_range.clone()) .collect::(); ( - buffer_snapshot.anchor_before(old_range.start) - ..buffer_snapshot.anchor_after(old_range.end), + buffer_snapshot.anchor_before(word_range.start) + ..buffer_snapshot.anchor_after(buffer_position), Some(word_to_exclude), ) } else { @@ -5085,6 +5551,10 @@ impl Editor { let completion_settings = language_settings(language.clone(), buffer_snapshot.file(), cx).completions; + let show_completion_documentation = buffer_snapshot + .settings_at(buffer_position, cx) + .show_completion_documentation; + // The document can be large, so stay in reasonable bounds when searching for words, // otherwise completion pop-up might be slow to appear. const WORD_LOOKUP_ROWS: u32 = 5_000; @@ -5100,18 +5570,18 @@ impl Editor { let word_search_range = buffer_snapshot.point_to_offset(min_word_search) ..buffer_snapshot.point_to_offset(max_word_search); - let provider = if ignore_completion_provider { - None - } else { - self.completion_provider.clone() - }; let skip_digits = query .as_ref() - .map_or(true, |query| !query.chars().any(|c| c.is_digit(10))); + .is_none_or(|query| !query.chars().any(|c| c.is_digit(10))); - let (mut words, provided_completions) = match &provider { + let omit_word_completions = match &query { + Some(query) => query.chars().count() < completion_settings.words_min_length, + None => completion_settings.words_min_length != 0, + }; + + let (mut words, provider_responses) = match &provider { Some(provider) => { - let completions = provider.completions( + let provider_responses = provider.completions( position.excerpt_id, &buffer, buffer_position, @@ -5120,9 +5590,11 @@ impl Editor { cx, ); - let words = match completion_settings.words { - WordsCompletionMode::Disabled => Task::ready(BTreeMap::default()), - WordsCompletionMode::Enabled | WordsCompletionMode::Fallback => cx + let words = match (omit_word_completions, completion_settings.words) { + (true, _) | (_, WordsCompletionMode::Disabled) => { + Task::ready(BTreeMap::default()) + } + (false, WordsCompletionMode::Enabled | WordsCompletionMode::Fallback) => cx .background_spawn(async move { buffer_snapshot.words_in_range(WordsQuery { fuzzy_contents: None, @@ -5132,147 +5604,176 @@ impl Editor { }), }; - (words, completions) + (words, provider_responses) } - None => ( - cx.background_spawn(async move { - buffer_snapshot.words_in_range(WordsQuery { - fuzzy_contents: None, - range: word_search_range, - skip_digits, + None => { + let words = if omit_word_completions { + Task::ready(BTreeMap::default()) + } else { + cx.background_spawn(async move { + buffer_snapshot.words_in_range(WordsQuery { + fuzzy_contents: None, + range: word_search_range, + skip_digits, + }) }) - }), - Task::ready(Ok(None)), - ), + }; + (words, Task::ready(Ok(Vec::new()))) + } }; - let sort_completions = provider - .as_ref() - .map_or(false, |provider| provider.sort_completions()); - - let filter_completions = provider - .as_ref() - .map_or(true, |provider| provider.filter_completions()); - let snippet_sort_order = EditorSettings::get_global(cx).snippet_sort_order; let id = post_inc(&mut self.next_completion_id); let task = cx.spawn_in(window, async move |editor, cx| { - async move { - editor.update(cx, |this, _| { - this.completion_tasks.retain(|(task_id, _)| *task_id >= id); - })?; + let Ok(()) = editor.update(cx, |this, _| { + this.completion_tasks.retain(|(task_id, _)| *task_id >= id); + }) else { + return; + }; - let mut completions = Vec::new(); - if let Some(provided_completions) = provided_completions.await.log_err().flatten() { - completions.extend(provided_completions); - if completion_settings.words == WordsCompletionMode::Fallback { - words = Task::ready(BTreeMap::default()); - } + // TODO: Ideally completions from different sources would be selectively re-queried, so + // that having one source with `is_incomplete: true` doesn't cause all to be re-queried. + let mut completions = Vec::new(); + let mut is_incomplete = false; + if let Some(provider_responses) = provider_responses.await.log_err() + && !provider_responses.is_empty() + { + for response in provider_responses { + completions.extend(response.completions); + is_incomplete = is_incomplete || response.is_incomplete; } - - let mut words = words.await; - if let Some(word_to_exclude) = &word_to_exclude { - words.remove(word_to_exclude); + if completion_settings.words == WordsCompletionMode::Fallback { + words = Task::ready(BTreeMap::default()); } - for lsp_completion in &completions { - words.remove(&lsp_completion.new_text); - } - completions.extend(words.into_iter().map(|(word, word_range)| Completion { - replace_range: old_range.clone(), - new_text: word.clone(), - label: CodeLabel::plain(word, None), - icon_path: None, - documentation: None, - source: CompletionSource::BufferWord { - word_range, - resolved: false, - }, - insert_text_mode: Some(InsertTextMode::AS_IS), - confirm: None, - })); + } - let menu = if completions.is_empty() { - None - } else { - let mut menu = editor.update(cx, |editor, cx| { - let languages = editor - .workspace - .as_ref() - .and_then(|(workspace, _)| workspace.upgrade()) - .map(|workspace| workspace.read(cx).app_state().languages.clone()); - CompletionsMenu::new( - id, - sort_completions, - show_completion_documentation, - ignore_completion_provider, - position, - buffer.clone(), - completions.into(), - snippet_sort_order, - languages, - language, - cx, - ) - })?; + let mut words = words.await; + if let Some(word_to_exclude) = &word_to_exclude { + words.remove(word_to_exclude); + } + for lsp_completion in &completions { + words.remove(&lsp_completion.new_text); + } + completions.extend(words.into_iter().map(|(word, word_range)| Completion { + replace_range: word_replace_range.clone(), + new_text: word.clone(), + label: CodeLabel::plain(word, None), + icon_path: None, + documentation: None, + source: CompletionSource::BufferWord { + word_range, + resolved: false, + }, + insert_text_mode: Some(InsertTextMode::AS_IS), + confirm: None, + })); - menu.filter( - if filter_completions { - query.as_deref() - } else { - None - }, - provider, - editor.clone(), + let menu = if completions.is_empty() { + None + } else { + let Ok((mut menu, matches_task)) = editor.update(cx, |editor, cx| { + let languages = editor + .workspace + .as_ref() + .and_then(|(workspace, _)| workspace.upgrade()) + .map(|workspace| workspace.read(cx).app_state().languages.clone()); + let menu = CompletionsMenu::new( + id, + requested_source.unwrap_or(CompletionsMenuSource::Normal), + sort_completions, + show_completion_documentation, + position, + query.clone(), + is_incomplete, + buffer.clone(), + completions.into(), + snippet_sort_order, + languages, + language, cx, - ) - .await; + ); - menu.visible().then_some(menu) + let query = if filter_completions { query } else { None }; + let matches_task = if let Some(query) = query { + menu.do_async_filtering(query, cx) + } else { + Task::ready(menu.unfiltered_matches()) + }; + (menu, matches_task) + }) else { + return; }; - editor.update_in(cx, |editor, window, cx| { - match editor.context_menu.borrow().as_ref() { - None => {} - Some(CodeContextMenu::Completions(prev_menu)) => { - if prev_menu.id > id { - return; - } - } - _ => return, - } + let matches = matches_task.await; - if editor.focus_handle.is_focused(window) && menu.is_some() { - let mut menu = menu.unwrap(); - menu.resolve_visible_completions(editor.completion_provider.as_deref(), cx); - crate::hover_popover::hide_hover(editor, cx); + let Ok(()) = editor.update_in(cx, |editor, window, cx| { + // Newer menu already set, so exit. + if let Some(CodeContextMenu::Completions(prev_menu)) = + editor.context_menu.borrow().as_ref() + && prev_menu.id > id + { + return; + }; + + // Only valid to take prev_menu because it the new menu is immediately set + // below, or the menu is hidden. + if let Some(CodeContextMenu::Completions(prev_menu)) = + editor.context_menu.borrow_mut().take() + { + let position_matches = + if prev_menu.initial_position == menu.initial_position { + true + } else { + let snapshot = editor.buffer.read(cx).read(cx); + prev_menu.initial_position.to_offset(&snapshot) + == menu.initial_position.to_offset(&snapshot) + }; + if position_matches { + // Preserve markdown cache before `set_filter_results` because it will + // try to populate the documentation cache. + menu.preserve_markdown_cache(prev_menu); + } + }; + + menu.set_filter_results(matches, provider, window, cx); + }) else { + return; + }; + + menu.visible().then_some(menu) + }; + + editor + .update_in(cx, |editor, window, cx| { + if editor.focus_handle.is_focused(window) + && let Some(menu) = menu + { *editor.context_menu.borrow_mut() = Some(CodeContextMenu::Completions(menu)); + crate::hover_popover::hide_hover(editor, cx); if editor.show_edit_predictions_in_menu() { - editor.update_visible_inline_completion(window, cx); + editor.update_visible_edit_prediction(window, cx); } else { - editor.discard_inline_completion(false, cx); + editor.discard_edit_prediction(false, cx); } cx.notify(); - } else if editor.completion_tasks.len() <= 1 { - // If there are no more completion tasks and the last menu was - // empty, we should hide it. + return; + } + + if editor.completion_tasks.len() <= 1 { + // If there are no more completion tasks and the last menu was empty, we should hide it. let was_hidden = editor.hide_context_menu(window, cx).is_none(); - // If it was already hidden and we don't show inline - // completions in the menu, we should also show the - // inline-completion when available. + // If it was already hidden and we don't show edit predictions in the menu, + // we should also show the edit prediction when available. if was_hidden && editor.show_edit_predictions_in_menu() { - editor.update_visible_inline_completion(window, cx); + editor.update_visible_edit_prediction(window, cx); } } - })?; - - anyhow::Ok(()) - } - .log_err() - .await + }) + .ok(); }); self.completion_tasks.push((id, task)); @@ -5292,17 +5793,16 @@ impl Editor { pub fn with_completions_menu_matching_id( &self, id: CompletionId, - on_absent: impl FnOnce() -> R, - on_match: impl FnOnce(&mut CompletionsMenu) -> R, + f: impl FnOnce(Option<&mut CompletionsMenu>) -> R, ) -> R { let mut context_menu = self.context_menu.borrow_mut(); let Some(CodeContextMenu::Completions(completions_menu)) = &mut *context_menu else { - return on_absent(); + return f(None); }; if completions_menu.id != id { - return on_absent(); + return f(None); } - on_match(completions_menu) + f(Some(completions_menu)) } pub fn confirm_completion( @@ -5311,7 +5811,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) -> Option>> { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.do_completion(action.item_ix, CompletionIntent::Complete, window, cx) } @@ -5321,7 +5821,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) -> Option>> { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.do_completion(None, CompletionIntent::CompleteWithInsert, window, cx) } @@ -5331,7 +5831,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) -> Option>> { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.do_completion(None, CompletionIntent::CompleteWithReplace, window, cx) } @@ -5341,7 +5841,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) -> Option>> { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.do_completion(action.item_ix, CompletionIntent::Compose, window, cx) } @@ -5363,12 +5863,11 @@ impl Editor { let entries = completions_menu.entries.borrow(); let mat = entries.get(item_ix.unwrap_or(completions_menu.selected_item))?; if self.show_edit_predictions_in_menu() { - self.discard_inline_completion(true, cx); + self.discard_edit_prediction(true, cx); } mat.candidate_id }; - let buffer_handle = completions_menu.buffer; let completion = completions_menu .completions .borrow() @@ -5376,34 +5875,23 @@ impl Editor { .clone(); cx.stop_propagation(); + let buffer_handle = completions_menu.buffer.clone(); + + let CompletionEdit { + new_text, + snippet, + replace_range, + } = process_completion_for_edit( + &completion, + intent, + &buffer_handle, + &completions_menu.initial_position.text_anchor, + cx, + ); + + let buffer = buffer_handle.read(cx); let snapshot = self.buffer.read(cx).snapshot(cx); let newest_anchor = self.selections.newest_anchor(); - - let snippet; - let new_text; - if completion.is_snippet() { - let mut snippet_source = completion.new_text.clone(); - if let Some(scope) = snapshot.language_scope_at(newest_anchor.head()) { - if scope.prefers_label_for_snippet_in_completion() { - if let Some(label) = completion.label() { - if matches!( - completion.kind(), - Some(CompletionItemKind::FUNCTION) | Some(CompletionItemKind::METHOD) - ) { - snippet_source = label; - } - } - } - } - snippet = Some(Snippet::parse(&snippet_source).log_err()?); - new_text = snippet.as_ref().unwrap().text.clone(); - } else { - snippet = None; - new_text = completion.new_text.clone(); - }; - - let replace_range = choose_completion_range(&completion, intent, &buffer_handle, cx); - let buffer = buffer_handle.read(cx); let replace_range_multibuffer = { let excerpt = snapshot.excerpt_containing(newest_anchor.range()).unwrap(); let multibuffer_anchor = snapshot @@ -5415,7 +5903,7 @@ impl Editor { multibuffer_anchor.start.to_offset(&snapshot) ..multibuffer_anchor.end.to_offset(&snapshot) }; - if newest_anchor.head().buffer_id != Some(buffer.remote_id()) { + if snapshot.buffer_id_for_anchor(newest_anchor.head()) != Some(buffer.remote_id()) { return None; } @@ -5475,23 +5963,32 @@ impl Editor { } } + let common_prefix_len = old_text + .chars() + .zip(new_text.chars()) + .take_while(|(a, b)| a == b) + .map(|(a, _)| a.len_utf8()) + .sum::(); + cx.emit(EditorEvent::InputHandled { utf16_range_to_replace: None, - text: new_text.clone().into(), + text: new_text[common_prefix_len..].into(), }); - self.transact(window, cx, |this, window, cx| { + self.transact(window, cx, |editor, window, cx| { if let Some(mut snippet) = snippet { snippet.text = new_text.to_string(); - this.insert_snippet(&ranges, snippet, window, cx).log_err(); + editor + .insert_snippet(&ranges, snippet, window, cx) + .log_err(); } else { - this.buffer.update(cx, |buffer, cx| { + editor.buffer.update(cx, |multi_buffer, cx| { let auto_indent = match completion.insert_text_mode { Some(InsertTextMode::AS_IS) => None, - _ => this.autoindent_mode.clone(), + _ => editor.autoindent_mode.clone(), }; let edits = ranges.into_iter().map(|range| (range, new_text.as_str())); - buffer.edit(edits, auto_indent, cx); + multi_buffer.edit(edits, auto_indent, cx); }); } for (buffer, edits) in linked_edits { @@ -5510,13 +6007,14 @@ impl Editor { }) } - this.refresh_inline_completion(true, false, window, cx); + editor.refresh_edit_prediction(true, false, window, cx); }); + self.invalidate_autoclose_regions(&self.selections.disjoint_anchors(), &snapshot); let show_new_completions_on_confirm = completion .confirm .as_ref() - .map_or(false, |confirm| confirm(intent, window, cx)); + .is_some_and(|confirm| confirm(intent, window, cx)); if show_new_completions_on_confirm { self.show_completions(&ShowCompletions { trigger: None }, window, cx); } @@ -5567,70 +6065,60 @@ impl Editor { drop(context_menu); let snapshot = self.snapshot(window, cx); let deployed_from = action.deployed_from.clone(); - let mut task = self.code_actions_task.take(); let action = action.clone(); - cx.spawn_in(window, async move |editor, cx| { - while let Some(prev_task) = task { - prev_task.await.log_err(); - task = editor.update(cx, |this, _| this.code_actions_task.take())?; + self.completion_tasks.clear(); + self.discard_edit_prediction(false, cx); + + let multibuffer_point = match &action.deployed_from { + Some(CodeActionSource::Indicator(row)) | Some(CodeActionSource::RunMenu(row)) => { + DisplayPoint::new(*row, 0).to_point(&snapshot) } + _ => self.selections.newest::(cx).head(), + }; + let Some((buffer, buffer_row)) = snapshot + .buffer_snapshot + .buffer_line_for_row(MultiBufferRow(multibuffer_point.row)) + .and_then(|(buffer_snapshot, range)| { + self.buffer() + .read(cx) + .buffer(buffer_snapshot.remote_id()) + .map(|buffer| (buffer, range.start.row)) + }) + else { + return; + }; + let buffer_id = buffer.read(cx).remote_id(); + let tasks = self + .tasks + .get(&(buffer_id, buffer_row)) + .map(|t| Arc::new(t.to_owned())); - let spawned_test_task = editor.update_in(cx, |editor, window, cx| { - if editor.focus_handle.is_focused(window) { - let multibuffer_point = match &action.deployed_from { - Some(CodeActionSource::Indicator(row)) => { - DisplayPoint::new(*row, 0).to_point(&snapshot) - } - _ => editor.selections.newest::(cx).head(), - }; - let (buffer, buffer_row) = snapshot - .buffer_snapshot - .buffer_line_for_row(MultiBufferRow(multibuffer_point.row)) - .and_then(|(buffer_snapshot, range)| { - editor - .buffer - .read(cx) - .buffer(buffer_snapshot.remote_id()) - .map(|buffer| (buffer, range.start.row)) - })?; - let (_, code_actions) = editor - .available_code_actions - .clone() - .and_then(|(location, code_actions)| { - let snapshot = location.buffer.read(cx).snapshot(); - let point_range = location.range.to_point(&snapshot); - let point_range = point_range.start.row..=point_range.end.row; - if point_range.contains(&buffer_row) { - Some((location, code_actions)) - } else { - None - } - }) - .unzip(); - let buffer_id = buffer.read(cx).remote_id(); - let tasks = editor - .tasks - .get(&(buffer_id, buffer_row)) - .map(|t| Arc::new(t.to_owned())); - if tasks.is_none() && code_actions.is_none() { - return None; - } + if !self.focus_handle.is_focused(window) { + return; + } + let project = self.project.clone(); - editor.completion_tasks.clear(); - editor.discard_inline_completion(false, cx); - let task_context = - tasks - .as_ref() - .zip(editor.project.clone()) - .map(|(tasks, project)| { - Self::build_tasks_context(&project, &buffer, buffer_row, tasks, cx) - }); + let code_actions_task = match deployed_from { + Some(CodeActionSource::RunMenu(_)) => Task::ready(None), + _ => self.code_actions(buffer_row, window, cx), + }; + + let runnable_task = match deployed_from { + Some(CodeActionSource::Indicator(_)) => Task::ready(Ok(Default::default())), + _ => { + let mut task_context_task = Task::ready(None); + if let Some(tasks) = &tasks + && let Some(project) = project + { + task_context_task = + Self::build_tasks_context(&project, &buffer, buffer_row, tasks, cx); + } + + cx.spawn_in(window, { + let buffer = buffer.clone(); + async move |editor, cx| { + let task_context = task_context_task.await; - Some(cx.spawn_in(window, async move |editor, cx| { - let task_context = match task_context { - Some(task_context) => task_context.await, - None => None, - }; let resolved_tasks = tasks .zip(task_context.clone()) @@ -5641,111 +6129,154 @@ impl Editor { tasks.column, )), }); - let debug_scenarios = editor.update(cx, |editor, cx| { - if cx.has_flag::() { - maybe!({ - let project = editor.project.as_ref()?; - let dap_store = project.read(cx).dap_store(); - let mut scenarios = vec![]; - let resolved_tasks = resolved_tasks.as_ref()?; - let buffer = buffer.read(cx); - let language = buffer.language()?; - let file = buffer.file(); - let debug_adapter = - language_settings(language.name().into(), file, cx) - .debuggers - .first() - .map(SharedString::from) - .or_else(|| { - language - .config() - .debuggers - .first() - .map(SharedString::from) - })?; - - dap_store.update(cx, |dap_store, cx| { - for (_, task) in &resolved_tasks.templates { - if let Some(scenario) = dap_store - .debug_scenario_for_build_task( - task.original_task().clone(), - debug_adapter.clone().into(), - task.display_label().to_owned().into(), - cx, - ) - { - scenarios.push(scenario); - } - } - }); - Some(scenarios) - }) - .unwrap_or_default() - } else { - vec![] - } - })?; - let spawn_straight_away = quick_launch - && resolved_tasks - .as_ref() - .map_or(false, |tasks| tasks.templates.len() == 1) - && code_actions - .as_ref() - .map_or(true, |actions| actions.is_empty()) - && debug_scenarios.is_empty(); - if let Ok(task) = editor.update_in(cx, |editor, window, cx| { - crate::hover_popover::hide_hover(editor, cx); - *editor.context_menu.borrow_mut() = - Some(CodeContextMenu::CodeActions(CodeActionsMenu { - buffer, - actions: CodeActionContents::new( - resolved_tasks, - code_actions, - debug_scenarios, - task_context.unwrap_or_default(), - ), - selected_item: Default::default(), - scroll_handle: UniformListScrollHandle::default(), - deployed_from, - })); - if spawn_straight_away { - if let Some(task) = editor.confirm_code_action( - &ConfirmCodeAction { item_ix: Some(0) }, - window, - cx, - ) { - cx.notify(); - return task; - } - } - cx.notify(); - Task::ready(Ok(())) - }) { - task.await - } else { - Ok(()) - } - })) - } else { - Some(Task::ready(Ok(()))) - } - })?; - if let Some(task) = spawned_test_task { - task.await?; + let debug_scenarios = editor + .update(cx, |editor, cx| { + editor.debug_scenarios(&resolved_tasks, &buffer, cx) + })? + .await; + anyhow::Ok((resolved_tasks, debug_scenarios, task_context)) + } + }) } + }; - anyhow::Ok(()) + cx.spawn_in(window, async move |editor, cx| { + let (resolved_tasks, debug_scenarios, task_context) = runnable_task.await?; + let code_actions = code_actions_task.await; + let spawn_straight_away = quick_launch + && resolved_tasks + .as_ref() + .is_some_and(|tasks| tasks.templates.len() == 1) + && code_actions + .as_ref() + .is_none_or(|actions| actions.is_empty()) + && debug_scenarios.is_empty(); + + editor.update_in(cx, |editor, window, cx| { + crate::hover_popover::hide_hover(editor, cx); + let actions = CodeActionContents::new( + resolved_tasks, + code_actions, + debug_scenarios, + task_context.unwrap_or_default(), + ); + + // Don't show the menu if there are no actions available + if actions.is_empty() { + cx.notify(); + return Task::ready(Ok(())); + } + + *editor.context_menu.borrow_mut() = + Some(CodeContextMenu::CodeActions(CodeActionsMenu { + buffer, + actions, + selected_item: Default::default(), + scroll_handle: UniformListScrollHandle::default(), + deployed_from, + })); + cx.notify(); + if spawn_straight_away + && let Some(task) = editor.confirm_code_action( + &ConfirmCodeAction { item_ix: Some(0) }, + window, + cx, + ) + { + return task; + } + + Task::ready(Ok(())) + }) }) .detach_and_log_err(cx); } + fn debug_scenarios( + &mut self, + resolved_tasks: &Option, + buffer: &Entity, + cx: &mut App, + ) -> Task> { + maybe!({ + let project = self.project()?; + let dap_store = project.read(cx).dap_store(); + let mut scenarios = vec![]; + let resolved_tasks = resolved_tasks.as_ref()?; + let buffer = buffer.read(cx); + let language = buffer.language()?; + let file = buffer.file(); + let debug_adapter = language_settings(language.name().into(), file, cx) + .debuggers + .first() + .map(SharedString::from) + .or_else(|| language.config().debuggers.first().map(SharedString::from))?; + + dap_store.update(cx, |dap_store, cx| { + for (_, task) in &resolved_tasks.templates { + let maybe_scenario = dap_store.debug_scenario_for_build_task( + task.original_task().clone(), + debug_adapter.clone().into(), + task.display_label().to_owned().into(), + cx, + ); + scenarios.push(maybe_scenario); + } + }); + Some(cx.background_spawn(async move { + futures::future::join_all(scenarios) + .await + .into_iter() + .flatten() + .collect::>() + })) + }) + .unwrap_or_else(|| Task::ready(vec![])) + } + + fn code_actions( + &mut self, + buffer_row: u32, + window: &mut Window, + cx: &mut Context, + ) -> Task>> { + let mut task = self.code_actions_task.take(); + cx.spawn_in(window, async move |editor, cx| { + while let Some(prev_task) = task { + prev_task.await.log_err(); + task = editor + .update(cx, |this, _| this.code_actions_task.take()) + .ok()?; + } + + editor + .update(cx, |editor, cx| { + editor + .available_code_actions + .clone() + .and_then(|(location, code_actions)| { + let snapshot = location.buffer.read(cx).snapshot(); + let point_range = location.range.to_point(&snapshot); + let point_range = point_range.start.row..=point_range.end.row; + if point_range.contains(&buffer_row) { + Some(code_actions) + } else { + None + } + }) + }) + .ok() + .flatten() + }) + } + pub fn confirm_code_action( &mut self, action: &ConfirmCodeAction, window: &mut Window, cx: &mut Context, ) -> Option>> { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let actions_menu = if let CodeContextMenu::CodeActions(menu) = self.hide_context_menu(window, cx)? { @@ -5795,11 +6326,18 @@ impl Editor { })) } CodeActionsItem::DebugScenario(scenario) => { - let context = actions_menu.actions.context.clone(); + let context = actions_menu.actions.context; workspace.update(cx, |workspace, cx| { dap::send_telemetry(&scenario, TelemetrySpawnLocation::Gutter, cx); - workspace.start_debug_session(scenario, context, Some(buffer), window, cx); + workspace.start_debug_session( + scenario, + context, + Some(buffer), + None, + window, + cx, + ); }); Some(Task::ready(Ok(()))) } @@ -5807,7 +6345,7 @@ impl Editor { } pub async fn open_project_transaction( - this: &WeakEntity, + editor: &WeakEntity, workspace: WeakEntity, transaction: ProjectTransaction, title: String, @@ -5825,27 +6363,26 @@ impl Editor { if let Some((buffer, transaction)) = entries.first() { if entries.len() == 1 { - let excerpt = this.update(cx, |editor, cx| { + let excerpt = editor.update(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((_, excerpted_buffer, excerpt_range)) = excerpt + && 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(()); - } + if all_edits_within_excerpt { + return Ok(()); } } } @@ -5883,7 +6420,7 @@ impl Editor { editor.update(cx, |editor, cx| { editor.highlight_background::( &ranges_to_highlight, - |theme| theme.editor_highlighted_line_background, + |theme| theme.colors().editor_highlighted_line_background, cx, ); }); @@ -5948,7 +6485,6 @@ impl Editor { IconButton::new("inline_code_actions", ui::IconName::BoltFilled) .icon_size(icon_size) .shape(ui::IconButtonShape::Square) - .style(ButtonStyle::Transparent) .icon_color(ui::Color::Hidden) .toggle_state(is_active) .when(show_tooltip, |this| { @@ -5990,7 +6526,7 @@ impl Editor { fn refresh_code_actions(&mut self, window: &mut Window, cx: &mut Context) -> Option<()> { let newest_selection = self.selections.newest_anchor().clone(); - let newest_selection_adjusted = self.selections.newest_adjusted(cx).clone(); + let newest_selection_adjusted = self.selections.newest_adjusted(cx); let buffer = self.buffer.read(cx); if newest_selection.head().diff_base_anchor.is_some() { return None; @@ -6068,79 +6604,108 @@ impl Editor { } } + pub fn blame_hover(&mut self, _: &BlameHover, window: &mut Window, cx: &mut Context) { + let snapshot = self.snapshot(window, cx); + let cursor = self.selections.newest::(cx).head(); + let Some((buffer, point, _)) = snapshot.buffer_snapshot.point_to_buffer_point(cursor) + else { + return; + }; + + let Some(blame) = self.blame.as_ref() else { + return; + }; + + let row_info = RowInfo { + buffer_id: Some(buffer.remote_id()), + buffer_row: Some(point.row), + ..Default::default() + }; + let Some(blame_entry) = blame + .update(cx, |blame, cx| blame.blame_for_rows(&[row_info], cx).next()) + .flatten() + else { + return; + }; + + let anchor = self.selections.newest_anchor().head(); + let position = self.to_pixel_point(anchor, &snapshot, window); + if let (Some(position), Some(last_bounds)) = (position, self.last_bounds) { + self.show_blame_popover(&blame_entry, position + last_bounds.origin, true, cx); + }; + } + fn show_blame_popover( &mut self, blame_entry: &BlameEntry, position: gpui::Point, + ignore_timeout: bool, cx: &mut Context, ) { if let Some(state) = &mut self.inline_blame_popover { state.hide_task.take(); - cx.notify(); } else { - let delay = EditorSettings::get_global(cx).hover_popover_delay; + let blame_popover_delay = EditorSettings::get_global(cx).hover_popover_delay; + let blame_entry = blame_entry.clone(); let show_task = cx.spawn(async move |editor, cx| { - cx.background_executor() - .timer(std::time::Duration::from_millis(delay)) - .await; + if !ignore_timeout { + cx.background_executor() + .timer(std::time::Duration::from_millis(blame_popover_delay)) + .await; + } editor .update(cx, |editor, cx| { - if let Some(state) = &mut editor.inline_blame_popover { - state.show_task = None; - cx.notify(); - } + editor.inline_blame_popover_show_task.take(); + let Some(blame) = editor.blame.as_ref() else { + return; + }; + let blame = blame.read(cx); + let details = blame.details_for_entry(&blame_entry); + let markdown = cx.new(|cx| { + Markdown::new( + details + .as_ref() + .map(|message| message.message.clone()) + .unwrap_or_default(), + None, + None, + cx, + ) + }); + editor.inline_blame_popover = Some(InlineBlamePopover { + position, + hide_task: None, + popover_bounds: None, + popover_state: InlineBlamePopoverState { + scroll_handle: ScrollHandle::new(), + commit_message: details, + markdown, + }, + keyboard_grace: ignore_timeout, + }); + cx.notify(); }) .ok(); }); - let Some(blame) = self.blame.as_ref() else { - return; - }; - let blame = blame.read(cx); - let details = blame.details_for_entry(&blame_entry); - let markdown = cx.new(|cx| { - Markdown::new( - details - .as_ref() - .map(|message| message.message.clone()) - .unwrap_or_default(), - None, - None, - cx, - ) - }); - self.inline_blame_popover = Some(InlineBlamePopover { - position, - show_task: Some(show_task), - hide_task: None, - popover_bounds: None, - popover_state: InlineBlamePopoverState { - scroll_handle: ScrollHandle::new(), - commit_message: details, - markdown, - }, - }); + self.inline_blame_popover_show_task = Some(show_task); } } fn hide_blame_popover(&mut self, cx: &mut Context) { + self.inline_blame_popover_show_task.take(); if let Some(state) = &mut self.inline_blame_popover { - if state.show_task.is_some() { - self.inline_blame_popover.take(); - cx.notify(); - } else { - let hide_task = cx.spawn(async move |editor, cx| { - cx.background_executor() - .timer(std::time::Duration::from_millis(100)) - .await; - editor - .update(cx, |editor, cx| { - editor.inline_blame_popover.take(); - cx.notify(); - }) - .ok(); - }); - state.hide_task = Some(hide_task); - } + let hide_task = cx.spawn(async move |editor, cx| { + cx.background_executor() + .timer(std::time::Duration::from_millis(100)) + .await; + editor + .update(cx, |editor, cx| { + editor.inline_blame_popover.take(); + cx.notify(); + }) + .ok(); + }); + state.hide_task = Some(hide_task); } } @@ -6162,8 +6727,8 @@ impl Editor { } let snapshot = cursor_buffer.read(cx).snapshot(); - let (start_word_range, _) = snapshot.surrounding_word(cursor_buffer_position); - let (end_word_range, _) = snapshot.surrounding_word(tail_buffer_position); + let (start_word_range, _) = snapshot.surrounding_word(cursor_buffer_position, false); + let (end_word_range, _) = snapshot.surrounding_word(tail_buffer_position, false); if start_word_range != end_word_range { self.document_highlights_task.take(); self.clear_background_highlights::(cx); @@ -6195,11 +6760,10 @@ impl Editor { return; } - let buffer_id = cursor_position.buffer_id; let buffer = this.buffer.read(cx); - if !buffer + if buffer .text_anchor_for_position(cursor_position, cx) - .map_or(false, |(buffer, _)| buffer == cursor_buffer) + .is_none_or(|(buffer, _)| buffer != cursor_buffer) { return; } @@ -6208,8 +6772,8 @@ impl Editor { let mut write_ranges = Vec::new(); let mut read_ranges = Vec::new(); for highlight in highlights { - for (excerpt_id, excerpt_range) in - buffer.excerpts_for_buffer(cursor_buffer.read(cx).remote_id(), cx) + let buffer_id = cursor_buffer.read(cx).remote_id(); + for (excerpt_id, excerpt_range) in buffer.excerpts_for_buffer(buffer_id, cx) { let start = highlight .range @@ -6224,12 +6788,12 @@ impl Editor { } let range = Anchor { - buffer_id, + buffer_id: Some(buffer_id), excerpt_id, text_anchor: start, diff_base_anchor: None, }..Anchor { - buffer_id, + buffer_id: Some(buffer_id), excerpt_id, text_anchor: end, diff_base_anchor: None, @@ -6244,12 +6808,12 @@ impl Editor { this.highlight_background::( &read_ranges, - |theme| theme.editor_document_highlight_read_background, + |theme| theme.colors().editor_document_highlight_read_background, cx, ); this.highlight_background::( &write_ranges, - |theme| theme.editor_document_highlight_write_background, + |theme| theme.colors().editor_document_highlight_write_background, cx, ); cx.notify(); @@ -6264,7 +6828,7 @@ impl Editor { &mut self, cx: &mut Context, ) -> Option<(String, Range)> { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { return None; } if !EditorSettings::get_global(cx).selection_highlight { @@ -6325,7 +6889,7 @@ impl Editor { for (buffer_snapshot, search_range, excerpt_id) in buffer_ranges { match_ranges.extend( regex - .search(&buffer_snapshot, Some(search_range.clone())) + .search(buffer_snapshot, Some(search_range.clone())) .await .into_iter() .filter_map(|match_range| { @@ -6351,7 +6915,7 @@ impl Editor { if !match_ranges.is_empty() { editor.highlight_background::( &match_ranges, - |theme| theme.editor_document_highlight_bracket_background, + |theme| theme.colors().editor_document_highlight_bracket_background, cx, ) } @@ -6360,6 +6924,77 @@ impl Editor { }) } + fn refresh_single_line_folds(&mut self, window: &mut Window, cx: &mut Context) { + struct NewlineFold; + let type_id = std::any::TypeId::of::(); + if !self.mode.is_single_line() { + return; + } + let snapshot = self.snapshot(window, cx); + if snapshot.buffer_snapshot.max_point().row == 0 { + return; + } + let task = cx.background_spawn(async move { + let new_newlines = snapshot + .buffer_chars_at(0) + .filter_map(|(c, i)| { + if c == '\n' { + Some( + snapshot.buffer_snapshot.anchor_after(i) + ..snapshot.buffer_snapshot.anchor_before(i + 1), + ) + } else { + None + } + }) + .collect::>(); + let existing_newlines = snapshot + .folds_in_range(0..snapshot.buffer_snapshot.len()) + .filter_map(|fold| { + if fold.placeholder.type_tag == Some(type_id) { + Some(fold.range.start..fold.range.end) + } else { + None + } + }) + .collect::>(); + + (new_newlines, existing_newlines) + }); + self.folding_newlines = cx.spawn(async move |this, cx| { + let (new_newlines, existing_newlines) = task.await; + if new_newlines == existing_newlines { + return; + } + let placeholder = FoldPlaceholder { + render: Arc::new(move |_, _, cx| { + div() + .bg(cx.theme().status().hint_background) + .border_b_1() + .size_full() + .font(ThemeSettings::get_global(cx).buffer_font.clone()) + .border_color(cx.theme().status().hint) + .child("\\n") + .into_any() + }), + constrain_width: false, + merge_adjacent: false, + type_tag: Some(type_id), + }; + let creases = new_newlines + .into_iter() + .map(|range| Crease::simple(range, placeholder.clone())) + .collect(); + this.update(cx, |this, cx| { + this.display_map.update(cx, |display_map, cx| { + display_map.remove_folds_with_type(existing_newlines, type_id, cx); + display_map.fold(creases, cx); + }); + }) + .ok(); + }); + } + fn refresh_selected_text_highlights( &mut self, on_buffer_edit: bool, @@ -6378,9 +7013,7 @@ impl Editor { || self .quick_selection_highlight_task .as_ref() - .map_or(true, |(prev_anchor_range, _)| { - prev_anchor_range != &query_range - }) + .is_none_or(|(prev_anchor_range, _)| prev_anchor_range != &query_range) { let multi_buffer_visible_start = self .scroll_manager @@ -6409,9 +7042,7 @@ impl Editor { || self .debounced_selection_highlight_task .as_ref() - .map_or(true, |(prev_anchor_range, _)| { - prev_anchor_range != &query_range - }) + .is_none_or(|(prev_anchor_range, _)| prev_anchor_range != &query_range) { let multi_buffer_start = multi_buffer_snapshot .anchor_before(0) @@ -6434,20 +7065,24 @@ impl Editor { } } - pub fn refresh_inline_completion( + pub fn refresh_edit_prediction( &mut self, debounce: bool, user_requested: bool, window: &mut Window, cx: &mut Context, ) -> Option<()> { + if DisableAiSettings::get_global(cx).disable_ai { + return None; + } + let provider = self.edit_prediction_provider()?; let cursor = self.selections.newest_anchor().head(); let (buffer, cursor_buffer_position) = self.buffer.read(cx).text_anchor_for_position(cursor, cx)?; if !self.edit_predictions_enabled_in_buffer(&buffer, cursor_buffer_position, cx) { - self.discard_inline_completion(false, cx); + self.discard_edit_prediction(false, cx); return None; } @@ -6456,11 +7091,11 @@ impl Editor { || !self.is_focused(window) || buffer.read(cx).is_empty()) { - self.discard_inline_completion(false, cx); + self.discard_edit_prediction(false, cx); return None; } - self.update_visible_inline_completion(window, cx); + self.update_visible_edit_prediction(window, cx); provider.refresh( self.project.clone(), buffer, @@ -6496,8 +7131,9 @@ impl Editor { } pub fn update_edit_prediction_settings(&mut self, cx: &mut Context) { - if self.edit_prediction_provider.is_none() { + if self.edit_prediction_provider.is_none() || DisableAiSettings::get_global(cx).disable_ai { self.edit_prediction_settings = EditPredictionSettings::Disabled; + self.discard_edit_prediction(false, cx); } else { let selection = self.selections.newest_anchor(); let cursor = selection.head(); @@ -6518,8 +7154,8 @@ impl Editor { cx: &App, ) -> EditPredictionSettings { if !self.mode.is_full() - || !self.show_inline_completions_override.unwrap_or(true) - || self.inline_completions_disabled_in_scope(buffer, buffer_position, cx) + || !self.show_edit_predictions_override.unwrap_or(true) + || self.edit_predictions_disabled_in_scope(buffer, buffer_position, cx) { return EditPredictionSettings::Disabled; } @@ -6533,17 +7169,15 @@ impl Editor { }; let by_provider = matches!( - self.menu_inline_completions_policy, - MenuInlineCompletionsPolicy::ByProvider + self.menu_edit_predictions_policy, + MenuEditPredictionsPolicy::ByProvider ); let show_in_menu = by_provider && self .edit_prediction_provider .as_ref() - .map_or(false, |provider| { - provider.provider.show_completions_in_menu() - }); + .is_some_and(|provider| provider.provider.show_completions_in_menu()); let preview_requires_modifier = all_language_settings(file, cx).edit_predictions_mode() == EditPredictionsMode::Subtle; @@ -6591,7 +7225,7 @@ impl Editor { return Some(false); } let provider = self.edit_prediction_provider()?; - if !provider.is_enabled(&buffer, buffer_position, cx) { + if !provider.is_enabled(buffer, buffer_position, cx) { return Some(false); } let buffer = buffer.read(cx); @@ -6604,7 +7238,7 @@ impl Editor { .unwrap_or(false) } - fn cycle_inline_completion( + fn cycle_edit_prediction( &mut self, direction: Direction, window: &mut Window, @@ -6614,28 +7248,28 @@ impl Editor { let cursor = self.selections.newest_anchor().head(); let (buffer, cursor_buffer_position) = self.buffer.read(cx).text_anchor_for_position(cursor, cx)?; - if self.inline_completions_hidden_for_vim_mode || !self.should_show_edit_predictions() { + if self.edit_predictions_hidden_for_vim_mode || !self.should_show_edit_predictions() { return None; } provider.cycle(buffer, cursor_buffer_position, direction, cx); - self.update_visible_inline_completion(window, cx); + self.update_visible_edit_prediction(window, cx); Some(()) } - pub fn show_inline_completion( + pub fn show_edit_prediction( &mut self, _: &ShowEditPrediction, window: &mut Window, cx: &mut Context, ) { - if !self.has_active_inline_completion() { - self.refresh_inline_completion(false, true, window, cx); + if !self.has_active_edit_prediction() { + self.refresh_edit_prediction(false, true, window, cx); return; } - self.update_visible_inline_completion(window, cx); + self.update_visible_edit_prediction(window, cx); } pub fn display_cursor_names( @@ -6667,11 +7301,11 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if self.has_active_inline_completion() { - self.cycle_inline_completion(Direction::Next, window, cx); + if self.has_active_edit_prediction() { + self.cycle_edit_prediction(Direction::Next, window, cx); } else { let is_copilot_disabled = self - .refresh_inline_completion(false, true, window, cx) + .refresh_edit_prediction(false, true, window, cx) .is_none(); if is_copilot_disabled { cx.propagate(); @@ -6685,11 +7319,11 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if self.has_active_inline_completion() { - self.cycle_inline_completion(Direction::Prev, window, cx); + if self.has_active_edit_prediction() { + self.cycle_edit_prediction(Direction::Prev, window, cx); } else { let is_copilot_disabled = self - .refresh_inline_completion(false, true, window, cx) + .refresh_edit_prediction(false, true, window, cx) .is_none(); if is_copilot_disabled { cx.propagate(); @@ -6707,18 +7341,14 @@ impl Editor { self.hide_context_menu(window, cx); } - let Some(active_inline_completion) = self.active_inline_completion.as_ref() else { + let Some(active_edit_prediction) = self.active_edit_prediction.as_ref() else { return; }; - self.report_inline_completion_event( - active_inline_completion.completion_id.clone(), - true, - cx, - ); + self.report_edit_prediction_event(active_edit_prediction.completion_id.clone(), true, cx); - match &active_inline_completion.completion { - InlineCompletion::Move { target, .. } => { + match &active_edit_prediction.completion { + EditPrediction::Move { target, .. } => { let target = *target; if let Some(position_map) = &self.last_position_map { @@ -6730,7 +7360,7 @@ impl Editor { self.unfold_ranges(&[target..target], true, false, cx); // Note that this is also done in vim's handler of the Tab action. self.change_selections( - Some(Autoscroll::newest()), + SelectionEffects::scroll(Autoscroll::newest()), window, cx, |selections| { @@ -6760,7 +7390,7 @@ impl Editor { } } } - InlineCompletion::Edit { edits, .. } => { + EditPrediction::Edit { edits, .. } => { if let Some(provider) = self.edit_prediction_provider() { provider.accept(cx); } @@ -6775,7 +7405,7 @@ impl Editor { buffer.edit(edits.iter().cloned(), None, cx) }); - self.change_selections(None, window, cx, |s| { + self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_anchor_ranges([last_edit_end..last_edit_end]); }); @@ -6788,9 +7418,9 @@ impl Editor { } } - self.update_visible_inline_completion(window, cx); - if self.active_inline_completion.is_none() { - self.refresh_inline_completion(true, true, window, cx); + self.update_visible_edit_prediction(window, cx); + if self.active_edit_prediction.is_none() { + self.refresh_edit_prediction(true, true, window, cx); } cx.notify(); @@ -6800,33 +7430,34 @@ impl Editor { self.edit_prediction_requires_modifier_in_indent_conflict = false; } - pub fn accept_partial_inline_completion( + pub fn accept_partial_edit_prediction( &mut self, _: &AcceptPartialEditPrediction, window: &mut Window, cx: &mut Context, ) { - let Some(active_inline_completion) = self.active_inline_completion.as_ref() else { + let Some(active_edit_prediction) = self.active_edit_prediction.as_ref() else { return; }; if self.selections.count() != 1 { return; } - self.report_inline_completion_event( - active_inline_completion.completion_id.clone(), - true, - cx, - ); + self.report_edit_prediction_event(active_edit_prediction.completion_id.clone(), true, cx); - match &active_inline_completion.completion { - InlineCompletion::Move { target, .. } => { + match &active_edit_prediction.completion { + EditPrediction::Move { target, .. } => { let target = *target; - self.change_selections(Some(Autoscroll::newest()), window, cx, |selections| { - selections.select_anchor_ranges([target..target]); - }); + self.change_selections( + SelectionEffects::scroll(Autoscroll::newest()), + window, + cx, + |selections| { + selections.select_anchor_ranges([target..target]); + }, + ); } - InlineCompletion::Edit { edits, .. } => { + EditPrediction::Edit { edits, .. } => { // Find an insertion that starts at the cursor position. let snapshot = self.buffer.read(cx).snapshot(cx); let cursor_offset = self.selections.newest::(cx).head(); @@ -6860,7 +7491,7 @@ impl Editor { self.insert_with_autoindent_mode(&partial_completion, None, window, cx); - self.refresh_inline_completion(true, true, window, cx); + self.refresh_edit_prediction(true, true, window, cx); cx.notify(); } else { self.accept_edit_prediction(&Default::default(), window, cx); @@ -6869,28 +7500,28 @@ impl Editor { } } - fn discard_inline_completion( + fn discard_edit_prediction( &mut self, - should_report_inline_completion_event: bool, + should_report_edit_prediction_event: bool, cx: &mut Context, ) -> bool { - if should_report_inline_completion_event { + if should_report_edit_prediction_event { let completion_id = self - .active_inline_completion + .active_edit_prediction .as_ref() .and_then(|active_completion| active_completion.completion_id.clone()); - self.report_inline_completion_event(completion_id, false, cx); + self.report_edit_prediction_event(completion_id, false, cx); } if let Some(provider) = self.edit_prediction_provider() { provider.discard(cx); } - self.take_active_inline_completion(cx) + self.take_active_edit_prediction(cx) } - fn report_inline_completion_event(&self, id: Option, accepted: bool, cx: &App) { + fn report_edit_prediction_event(&self, id: Option, accepted: bool, cx: &App) { let Some(provider) = self.edit_prediction_provider() else { return; }; @@ -6921,18 +7552,18 @@ impl Editor { ); } - pub fn has_active_inline_completion(&self) -> bool { - self.active_inline_completion.is_some() + pub fn has_active_edit_prediction(&self) -> bool { + self.active_edit_prediction.is_some() } - fn take_active_inline_completion(&mut self, cx: &mut Context) -> bool { - let Some(active_inline_completion) = self.active_inline_completion.take() else { + fn take_active_edit_prediction(&mut self, cx: &mut Context) -> bool { + let Some(active_edit_prediction) = self.active_edit_prediction.take() else { return false; }; - self.splice_inlays(&active_inline_completion.inlay_ids, Default::default(), cx); - self.clear_highlights::(cx); - self.stale_inline_completion_in_menu = Some(active_inline_completion); + self.splice_inlays(&active_edit_prediction.inlay_ids, Default::default(), cx); + self.clear_highlights::(cx); + self.stale_edit_prediction_in_menu = Some(active_edit_prediction); true } @@ -6981,6 +7612,38 @@ impl Editor { ) } + fn multi_cursor_modifier(invert: bool, modifiers: &Modifiers, cx: &mut Context) -> bool { + let multi_cursor_setting = EditorSettings::get_global(cx).multi_cursor_modifier; + if invert { + match multi_cursor_setting { + MultiCursorModifier::Alt => modifiers.alt, + MultiCursorModifier::CmdOrCtrl => modifiers.secondary(), + } + } else { + match multi_cursor_setting { + MultiCursorModifier::Alt => modifiers.secondary(), + MultiCursorModifier::CmdOrCtrl => modifiers.alt, + } + } + } + + fn columnar_selection_mode( + modifiers: &Modifiers, + cx: &mut Context, + ) -> Option { + if modifiers.shift && modifiers.number_of_modifiers() == 2 { + if Self::multi_cursor_modifier(false, modifiers, cx) { + Some(ColumnarMode::FromMouse) + } else if Self::multi_cursor_modifier(true, modifiers, cx) { + Some(ColumnarMode::FromSelection) + } else { + None + } + } else { + None + } + } + fn update_selection_mode( &mut self, modifiers: &Modifiers, @@ -6988,7 +7651,10 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if modifiers != &COLUMNAR_SELECTION_MODIFIERS || self.selections.pending.is_none() { + let Some(mode) = Self::columnar_selection_mode(modifiers, cx) else { + return; + }; + if self.selections.pending.is_none() { return; } @@ -7000,6 +7666,7 @@ impl Editor { SelectPhase::BeginColumnar { position, reset: false, + mode, goal_column: point_for_position.exact_unclipped.column(), }, window, @@ -7013,12 +7680,25 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - let accept_keybind = self.accept_edit_prediction_keybind(window, cx); - let Some(accept_keystroke) = accept_keybind.keystroke() else { - return; + let mut modifiers_held = false; + if let Some(accept_keystroke) = self + .accept_edit_prediction_keybind(false, window, cx) + .keystroke() + { + modifiers_held = modifiers_held + || (&accept_keystroke.modifiers == modifiers + && accept_keystroke.modifiers.modified()); }; + if let Some(accept_partial_keystroke) = self + .accept_edit_prediction_keybind(true, window, cx) + .keystroke() + { + modifiers_held = modifiers_held + || (&accept_partial_keystroke.modifiers == modifiers + && accept_partial_keystroke.modifiers.modified()); + } - if &accept_keystroke.modifiers == modifiers && accept_keystroke.modifiers.modified() { + if modifiers_held { if matches!( self.edit_prediction_preview, EditPredictionPreview::Inactive { .. } @@ -7028,7 +7708,7 @@ impl Editor { since: Instant::now(), }; - self.update_visible_inline_completion(window, cx); + self.update_visible_edit_prediction(window, cx); cx.notify(); } } else if let EditPredictionPreview::Active { @@ -7051,16 +7731,20 @@ impl Editor { released_too_fast: since.elapsed() < Duration::from_millis(200), }; self.clear_row_highlights::(); - self.update_visible_inline_completion(window, cx); + self.update_visible_edit_prediction(window, cx); cx.notify(); } } - fn update_visible_inline_completion( + fn update_visible_edit_prediction( &mut self, _window: &mut Window, cx: &mut Context, ) -> Option<()> { + if DisableAiSettings::get_global(cx).disable_ai { + return None; + } + let selection = self.selections.newest_anchor(); let cursor = selection.head(); let multibuffer = self.buffer.read(cx).snapshot(cx); @@ -7070,24 +7754,24 @@ impl Editor { let show_in_menu = self.show_edit_predictions_in_menu(); let completions_menu_has_precedence = !show_in_menu && (self.context_menu.borrow().is_some() - || (!self.completion_tasks.is_empty() && !self.has_active_inline_completion())); + || (!self.completion_tasks.is_empty() && !self.has_active_edit_prediction())); if completions_menu_has_precedence || !offset_selection.is_empty() || self - .active_inline_completion + .active_edit_prediction .as_ref() - .map_or(false, |completion| { + .is_some_and(|completion| { let invalidation_range = completion.invalidation_range.to_offset(&multibuffer); let invalidation_range = invalidation_range.start..=invalidation_range.end; !invalidation_range.contains(&offset_selection.head()) }) { - self.discard_inline_completion(false, cx); + self.discard_edit_prediction(false, cx); return None; } - self.take_active_inline_completion(cx); + self.take_active_edit_prediction(cx); let Some(provider) = self.edit_prediction_provider() else { self.edit_prediction_settings = EditPredictionSettings::Disabled; return None; @@ -7099,6 +7783,11 @@ impl Editor { self.edit_prediction_settings = self.edit_prediction_settings_at_position(&buffer, cursor_buffer_position, cx); + if let EditPredictionSettings::Disabled = self.edit_prediction_settings { + self.discard_edit_prediction(false, cx); + return None; + }; + self.edit_prediction_indent_conflict = multibuffer.is_line_whitespace_upto(cursor); if self.edit_prediction_indent_conflict { @@ -7106,15 +7795,15 @@ impl Editor { let indents = multibuffer.suggested_indents(cursor_point.row..cursor_point.row + 1, cx); - if let Some((_, indent)) = indents.iter().next() { - if indent.len == cursor_point.column { - self.edit_prediction_indent_conflict = false; - } + if let Some((_, indent)) = indents.iter().next() + && indent.len == cursor_point.column + { + self.edit_prediction_indent_conflict = false; } } - let inline_completion = provider.suggest(&buffer, cursor_buffer_position, cx)?; - let edits = inline_completion + let edit_prediction = provider.suggest(&buffer, cursor_buffer_position, cx)?; + let edits = edit_prediction .edits .into_iter() .flat_map(|(range, new_text)| { @@ -7148,16 +7837,22 @@ impl Editor { } else { None }; - let is_move = - move_invalidation_row_range.is_some() || self.inline_completions_hidden_for_vim_mode; + let supports_jump = self + .edit_prediction_provider + .as_ref() + .map(|provider| provider.provider.supports_jump_to_edit()) + .unwrap_or(true); + + let is_move = supports_jump + && (move_invalidation_row_range.is_some() || self.edit_predictions_hidden_for_vim_mode); let completion = if is_move { invalidation_row_range = move_invalidation_row_range.unwrap_or(edit_start_row..edit_end_row); let target = first_edit_start; - InlineCompletion::Move { target, snapshot } + EditPrediction::Move { target, snapshot } } else { let show_completions_in_buffer = !self.edit_prediction_visible_in_cursor_popover(true) - && !self.inline_completions_hidden_for_vim_mode; + && !self.edit_predictions_hidden_for_vim_mode; if show_completions_in_buffer { if edits @@ -7166,7 +7861,7 @@ impl Editor { { let mut inlays = Vec::new(); for (range, new_text) in &edits { - let inlay = Inlay::inline_completion( + let inlay = Inlay::edit_prediction( post_inc(&mut self.next_inlay_id), range.start, new_text.as_str(), @@ -7178,7 +7873,7 @@ impl Editor { self.splice_inlays(&[], inlays, cx); } else { let background_color = cx.theme().status().deleted_background; - self.highlight_text::( + self.highlight_text::( edits.iter().map(|(range, _)| range.clone()).collect(), HighlightStyle { background_color: Some(background_color), @@ -7201,9 +7896,9 @@ impl Editor { EditDisplayMode::DiffPopover }; - InlineCompletion::Edit { + EditPrediction::Edit { edits, - edit_preview: inline_completion.edit_preview, + edit_preview: edit_prediction.edit_preview, display_mode, snapshot, } @@ -7216,11 +7911,11 @@ impl Editor { multibuffer.line_len(MultiBufferRow(invalidation_row_range.end)), )); - self.stale_inline_completion_in_menu = None; - self.active_inline_completion = Some(InlineCompletionState { + self.stale_edit_prediction_in_menu = None; + self.active_edit_prediction = Some(EditPredictionState { inlay_ids, completion, - completion_id: inline_completion.id, + completion_id: edit_prediction.id, invalidation_range, }); @@ -7229,7 +7924,7 @@ impl Editor { Some(()) } - pub fn edit_prediction_provider(&self) -> Option> { + pub fn edit_prediction_provider(&self) -> Option> { Some(self.edit_prediction_provider.as_ref()?.provider.clone()) } @@ -7266,7 +7961,7 @@ impl Editor { let snapshot = self.snapshot(window, cx); let multi_buffer_snapshot = &snapshot.display_snapshot.buffer_snapshot; - let Some(project) = self.project.as_ref() else { + let Some(project) = self.project() else { return breakpoint_display_points; }; @@ -7295,7 +7990,7 @@ impl Editor { let multi_buffer_anchor = Anchor::in_buffer(excerpt_id, buffer_snapshot.remote_id(), breakpoint.position); let position = multi_buffer_anchor - .to_point(&multi_buffer_snapshot) + .to_point(multi_buffer_snapshot) .to_display_point(&snapshot); breakpoint_display_points.insert( @@ -7358,8 +8053,7 @@ impl Editor { "Set Breakpoint" }; - let run_to_cursor = command_palette_hooks::CommandPaletteFilter::try_global(cx) - .map_or(false, |filter| !filter.is_hidden(&DebuggerRunToCursor)); + let run_to_cursor = window.is_action_available(&RunToCursor, cx); let toggle_state_msg = breakpoint.as_ref().map_or(None, |bp| match bp.1.state { BreakpointState::Enabled => Some("Disable"), @@ -7377,13 +8071,16 @@ impl Editor { this.entry("Run to cursor", None, move |window, cx| { weak_editor .update(cx, |editor, cx| { - editor.change_selections(None, window, cx, |s| { - s.select_ranges([Point::new(row, 0)..Point::new(row, 0)]) - }); + editor.change_selections( + SelectionEffects::no_scroll(), + window, + cx, + |s| s.select_ranges([Point::new(row, 0)..Point::new(row, 0)]), + ); }) .ok(); - window.dispatch_action(Box::new(DebuggerRunToCursor), cx); + window.dispatch_action(Box::new(RunToCursor), cx); }) .separator() }) @@ -7547,8 +8244,6 @@ impl Editor { .icon_color(color) .style(ButtonStyle::Transparent) .on_click(cx.listener({ - let breakpoint = breakpoint.clone(); - move |editor, event: &ClickEvent, window, cx| { let edit_action = if event.modifiers().platform || breakpoint.is_disabled() { BreakpointEditAction::InvertState @@ -7569,7 +8264,7 @@ impl Editor { editor.set_breakpoint_context_menu( row, Some(position), - event.down.position, + event.position(), window, cx, ); @@ -7627,8 +8322,7 @@ impl Editor { return; }; - // Try to find a closest, enclosing node using tree-sitter that has a - // task + // Try to find a closest, enclosing node using tree-sitter that has a task let Some((buffer, buffer_row, tasks)) = self .find_enclosing_node_task(cx) // Or find the task that's closest in row-distance. @@ -7728,26 +8422,33 @@ impl Editor { let color = Color::Muted; let position = breakpoint.as_ref().map(|(anchor, _, _)| *anchor); - IconButton::new(("run_indicator", row.0 as usize), ui::IconName::Play) - .shape(ui::IconButtonShape::Square) - .icon_size(IconSize::XSmall) - .icon_color(color) - .toggle_state(is_active) - .on_click(cx.listener(move |editor, e: &ClickEvent, window, cx| { - let quick_launch = e.down.button == MouseButton::Left; - window.focus(&editor.focus_handle(cx)); - editor.toggle_code_actions( - &ToggleCodeActions { - deployed_from: Some(CodeActionSource::Indicator(row)), - quick_launch, - }, - window, - cx, - ); - })) - .on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| { - editor.set_breakpoint_context_menu(row, position, event.down.position, window, cx); - })) + IconButton::new( + ("run_indicator", row.0 as usize), + ui::IconName::PlayOutlined, + ) + .shape(ui::IconButtonShape::Square) + .icon_size(IconSize::XSmall) + .icon_color(color) + .toggle_state(is_active) + .on_click(cx.listener(move |editor, e: &ClickEvent, window, cx| { + let quick_launch = match e { + ClickEvent::Keyboard(_) => true, + ClickEvent::Mouse(e) => e.down.button == MouseButton::Left, + }; + + window.focus(&editor.focus_handle(cx)); + editor.toggle_code_actions( + &ToggleCodeActions { + deployed_from: Some(CodeActionSource::RunMenu(row)), + quick_launch, + }, + window, + cx, + ); + })) + .on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| { + editor.set_breakpoint_context_menu(row, position, event.position(), window, cx); + })) } pub fn context_menu_visible(&self) -> bool { @@ -7756,7 +8457,7 @@ impl Editor { .context_menu .borrow() .as_ref() - .map_or(false, |menu| menu.visible()) + .is_some_and(|menu| menu.visible()) } pub fn context_menu_origin(&self) -> Option { @@ -7794,14 +8495,14 @@ impl Editor { if self.mode().is_minimap() { return None; } - let active_inline_completion = self.active_inline_completion.as_ref()?; + let active_edit_prediction = self.active_edit_prediction.as_ref()?; if self.edit_prediction_visible_in_cursor_popover(true) { return None; } - match &active_inline_completion.completion { - InlineCompletion::Move { target, .. } => { + match &active_edit_prediction.completion { + EditPrediction::Move { target, .. } => { let target_display_point = target.to_display_point(editor_snapshot); if self.edit_prediction_requires_modifier() { @@ -7838,11 +8539,11 @@ impl Editor { ) } } - InlineCompletion::Edit { + EditPrediction::Edit { display_mode: EditDisplayMode::Inline, .. } => None, - InlineCompletion::Edit { + EditPrediction::Edit { display_mode: EditDisplayMode::TabAccept, edits, .. @@ -7863,7 +8564,7 @@ impl Editor { cx, ) } - InlineCompletion::Edit { + EditPrediction::Edit { edits, edit_preview, display_mode: EditDisplayMode::DiffPopover, @@ -8179,8 +8880,12 @@ impl Editor { return None; } - let highlighted_edits = - crate::inline_completion_edit_text(&snapshot, edits, edit_preview.as_ref()?, false, cx); + let highlighted_edits = if let Some(edit_preview) = edit_preview.as_ref() { + crate::edit_prediction_edit_text(snapshot, edits, edit_preview, false, cx) + } else { + // Fallback for providers without edit_preview + crate::edit_prediction_fallback_text(edits, cx) + }; let styled_text = highlighted_edits.to_styled_text(&style.text); let line_count = highlighted_edits.text.lines().count(); @@ -8196,7 +8901,7 @@ impl Editor { h_flex() .bg(cx.theme().colors().editor_background) .border(BORDER_WIDTH) - .shadow_sm() + .shadow_xs() .border_color(cx.theme().colors().border) .rounded_l_lg() .when(line_count > 1, |el| el.rounded_br_lg()) @@ -8298,9 +9003,8 @@ impl Editor { let end_row = start_row + line_count as u32; visible_row_range.contains(&start_row) && visible_row_range.contains(&end_row) - && cursor_row.map_or(true, |cursor_row| { - !((start_row..end_row).contains(&cursor_row)) - }) + && cursor_row + .is_none_or(|cursor_row| !((start_row..end_row).contains(&cursor_row))) })?; content_origin @@ -8335,7 +9039,7 @@ impl Editor { window: &mut Window, cx: &App, ) -> Option { - let accept_binding = self.accept_edit_prediction_keybind(window, cx); + let accept_binding = self.accept_edit_prediction_keybind(false, window, cx); let accept_keystroke = accept_binding.keystroke()?; let is_platform_style_mac = PlatformStyle::platform() == PlatformStyle::Mac; @@ -8396,7 +9100,7 @@ impl Editor { .border_1() .bg(Self::edit_prediction_line_popover_bg_color(cx)) .border_color(Self::edit_prediction_callout_popover_border_color(cx)) - .shadow_sm() + .shadow_xs() .when(!has_keybind, |el| { let status_colors = cx.theme().status(); @@ -8448,6 +9152,18 @@ impl Editor { let editor_bg_color = cx.theme().colors().editor_background; editor_bg_color.blend(accent_color.opacity(0.6)) } + fn get_prediction_provider_icon_name( + provider: &Option, + ) -> IconName { + match provider { + Some(provider) => match provider.provider.name() { + "copilot" => IconName::Copilot, + "supermaven" => IconName::Supermaven, + _ => IconName::ZedPredict, + }, + None => IconName::ZedPredict, + } + } fn render_edit_prediction_cursor_popover( &self, @@ -8460,57 +9176,15 @@ impl Editor { cx: &mut Context, ) -> Option { let provider = self.edit_prediction_provider.as_ref()?; - - if provider.provider.needs_terms_acceptance(cx) { - return Some( - h_flex() - .min_w(min_width) - .flex_1() - .px_2() - .py_1() - .gap_3() - .elevation_2(cx) - .hover(|style| style.bg(cx.theme().colors().element_hover)) - .id("accept-terms") - .cursor_pointer() - .on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default()) - .on_click(cx.listener(|this, _event, window, cx| { - cx.stop_propagation(); - this.report_editor_event("Edit Prediction Provider ToS Clicked", None, cx); - window.dispatch_action( - zed_actions::OpenZedPredictOnboarding.boxed_clone(), - cx, - ); - })) - .child( - h_flex() - .flex_1() - .gap_2() - .child(Icon::new(IconName::ZedPredict)) - .child(Label::new("Accept Terms of Service")) - .child(div().w_full()) - .child( - Icon::new(IconName::ArrowUpRight) - .color(Color::Muted) - .size(IconSize::Small), - ) - .into_any_element(), - ) - .into_any(), - ); - } + let provider_icon = Self::get_prediction_provider_icon_name(&self.edit_prediction_provider); let is_refreshing = provider.provider.is_refreshing(cx); - fn pending_completion_container() -> Div { - h_flex() - .h_full() - .flex_1() - .gap_2() - .child(Icon::new(IconName::ZedPredict)) + fn pending_completion_container(icon: IconName) -> Div { + h_flex().h_full().flex_1().gap_2().child(Icon::new(icon)) } - let completion = match &self.active_inline_completion { + let completion = match &self.active_edit_prediction { Some(prediction) => { if !self.has_visible_completions_menu() { const RADIUS: Pixels = px(6.); @@ -8528,16 +9202,16 @@ impl Editor { .rounded_tl(px(0.)) .overflow_hidden() .child(div().px_1p5().child(match &prediction.completion { - InlineCompletion::Move { target, snapshot } => { + EditPrediction::Move { target, snapshot } => { use text::ToPoint as _; - if target.text_anchor.to_point(&snapshot).row > cursor_point.row + if target.text_anchor.to_point(snapshot).row > cursor_point.row { Icon::new(IconName::ZedPredictDown) } else { Icon::new(IconName::ZedPredictUp) } } - InlineCompletion::Edit { .. } => Icon::new(IconName::ZedPredict), + EditPrediction::Edit { .. } => Icon::new(provider_icon), })) .child( h_flex() @@ -8596,7 +9270,7 @@ impl Editor { )? } - None if is_refreshing => match &self.stale_inline_completion_in_menu { + None if is_refreshing => match &self.stale_edit_prediction_in_menu { Some(stale_completion) => self.render_edit_prediction_cursor_popover_preview( stale_completion, cursor_point, @@ -8604,15 +9278,15 @@ impl Editor { cx, )?, - None => { - pending_completion_container().child(Label::new("...").size(LabelSize::Small)) - } + None => pending_completion_container(provider_icon) + .child(Label::new("...").size(LabelSize::Small)), }, - None => pending_completion_container().child(Label::new("No Prediction")), + None => pending_completion_container(provider_icon) + .child(Label::new("...").size(LabelSize::Small)), }; - let completion = if is_refreshing { + let completion = if is_refreshing || self.active_edit_prediction.is_none() { completion .with_animation( "loading-completion", @@ -8626,7 +9300,7 @@ impl Editor { completion.into_any_element() }; - let has_completion = self.active_inline_completion.is_some(); + let has_completion = self.active_edit_prediction.is_some(); let is_platform_style_mac = PlatformStyle::platform() == PlatformStyle::Mac; Some( @@ -8685,7 +9359,7 @@ impl Editor { fn render_edit_prediction_cursor_popover_preview( &self, - completion: &InlineCompletionState, + completion: &EditPredictionState, cursor_point: Point, style: &EditorStyle, cx: &mut Context, @@ -8712,40 +9386,51 @@ impl Editor { .child(Icon::new(arrow).color(Color::Muted).size(IconSize::Small)) } - match &completion.completion { - InlineCompletion::Move { - target, snapshot, .. - } => Some( - h_flex() - .px_2() - .gap_2() - .flex_1() - .child( - if target.text_anchor.to_point(&snapshot).row > cursor_point.row { - Icon::new(IconName::ZedPredictDown) - } else { - Icon::new(IconName::ZedPredictUp) - }, - ) - .child(Label::new("Jump to Edit")), - ), + let supports_jump = self + .edit_prediction_provider + .as_ref() + .map(|provider| provider.provider.supports_jump_to_edit()) + .unwrap_or(true); - InlineCompletion::Edit { + match &completion.completion { + EditPrediction::Move { + target, snapshot, .. + } => { + if !supports_jump { + return None; + } + + Some( + h_flex() + .px_2() + .gap_2() + .flex_1() + .child( + if target.text_anchor.to_point(snapshot).row > cursor_point.row { + Icon::new(IconName::ZedPredictDown) + } else { + Icon::new(IconName::ZedPredictUp) + }, + ) + .child(Label::new("Jump to Edit")), + ) + } + + EditPrediction::Edit { edits, edit_preview, snapshot, display_mode: _, } => { - let first_edit_row = edits.first()?.0.start.text_anchor.to_point(&snapshot).row; + let first_edit_row = edits.first()?.0.start.text_anchor.to_point(snapshot).row; - let (highlighted_edits, has_more_lines) = crate::inline_completion_edit_text( - &snapshot, - &edits, - edit_preview.as_ref()?, - true, - cx, - ) - .first_line_preview(); + let (highlighted_edits, has_more_lines) = + if let Some(edit_preview) = edit_preview.as_ref() { + crate::edit_prediction_edit_text(snapshot, edits, edit_preview, true, cx) + .first_line_preview() + } else { + crate::edit_prediction_fallback_text(edits, cx).first_line_preview() + }; let styled_text = gpui::StyledText::new(highlighted_edits.text) .with_default_highlights(&style.text, highlighted_edits.highlights); @@ -8756,11 +9441,13 @@ impl Editor { .child(styled_text) .when(has_more_lines, |parent| parent.child("…")); - let left = if first_edit_row != cursor_point.row { + let left = if supports_jump && first_edit_row != cursor_point.row { render_relative_row_jump("", cursor_point.row, first_edit_row) .into_any_element() } else { - Icon::new(IconName::ZedPredict).into_any_element() + let icon_name = + Editor::get_prediction_provider_icon_name(&self.edit_prediction_provider); + Icon::new(icon_name).into_any_element() }; Some( @@ -8816,12 +9503,12 @@ impl Editor { cx.notify(); self.completion_tasks.clear(); let context_menu = self.context_menu.borrow_mut().take(); - self.stale_inline_completion_in_menu.take(); - self.update_visible_inline_completion(window, cx); - if let Some(CodeContextMenu::Completions(_)) = &context_menu { - if let Some(completion_provider) = &self.completion_provider { - completion_provider.selection_changed(None, window, cx); - } + self.stale_edit_prediction_in_menu.take(); + self.update_visible_edit_prediction(window, cx); + if let Some(CodeContextMenu::Completions(_)) = &context_menu + && let Some(completion_provider) = &self.completion_provider + { + completion_provider.selection_changed(None, window, cx); } context_menu } @@ -8832,26 +9519,34 @@ impl Editor { selection: Range, cx: &mut Context, ) { - if selection.start.buffer_id.is_none() { + let Some((_, buffer, _)) = self + .buffer() + .read(cx) + .excerpt_containing(selection.start, cx) + else { + return; + }; + let Some((_, end_buffer, _)) = self.buffer().read(cx).excerpt_containing(selection.end, cx) + else { + return; + }; + if buffer != end_buffer { + log::error!("expected anchor range to have matching buffer IDs"); return; } - let buffer_id = selection.start.buffer_id.unwrap(); - let buffer = self.buffer().read(cx).buffer(buffer_id); + let id = post_inc(&mut self.next_completion_id); let snippet_sort_order = EditorSettings::get_global(cx).snippet_sort_order; - - if let Some(buffer) = buffer { - *self.context_menu.borrow_mut() = Some(CodeContextMenu::Completions( - CompletionsMenu::new_snippet_choices( - id, - true, - choices, - selection, - buffer, - snippet_sort_order, - ), - )); - } + *self.context_menu.borrow_mut() = Some(CodeContextMenu::Completions( + CompletionsMenu::new_snippet_choices( + id, + true, + choices, + selection, + buffer, + snippet_sort_order, + ), + )); } pub fn insert_snippet( @@ -8873,7 +9568,10 @@ impl Editor { .iter() .cloned() .map(|range| (range, snippet_text.clone())); - buffer.edit(edits, Some(AutoindentMode::EachLine), cx); + let autoindent_mode = AutoindentMode::Block { + original_indent_columns: Vec::new(), + }; + buffer.edit(edits, Some(autoindent_mode), cx); let snapshot = &*buffer.read(cx); let snippet = &snippet; @@ -8881,7 +9579,7 @@ impl Editor { .tabstops .iter() .map(|tabstop| { - let is_end_tabstop = tabstop.ranges.first().map_or(false, |tabstop| { + let is_end_tabstop = tabstop.ranges.first().is_some_and(|tabstop| { tabstop.is_empty() && tabstop.start == snippet.text.len() as isize }); let mut tabstop_ranges = tabstop @@ -8913,14 +9611,16 @@ impl Editor { .collect::>() }); if let Some(tabstop) = tabstops.first() { - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select_ranges(tabstop.ranges.iter().cloned()); + self.change_selections(Default::default(), window, cx, |s| { + // Reverse order so that the first range is the newest created selection. + // Completions will use it and autoscroll will prioritize it. + s.select_ranges(tabstop.ranges.iter().rev().cloned()); }); - if let Some(choices) = &tabstop.choices { - if let Some(selection) = tabstop.ranges.first() { - self.show_snippet_choices(choices, selection.clone(), cx) - } + if let Some(choices) = &tabstop.choices + && let Some(selection) = tabstop.ranges.first() + { + self.show_snippet_choices(choices, selection.clone(), cx) } // If we're already at the last tabstop and it's at the end of the snippet, @@ -8946,27 +9646,46 @@ impl Editor { // Check whether the just-entered snippet ends with an auto-closable bracket. if self.autoclose_regions.is_empty() { let snapshot = self.buffer.read(cx).snapshot(cx); - for selection in &mut self.selections.all::(cx) { + let mut all_selections = self.selections.all::(cx); + for selection in &mut all_selections { let selection_head = selection.head(); let Some(scope) = snapshot.language_scope_at(selection_head) else { continue; }; let mut bracket_pair = None; - let next_chars = snapshot.chars_at(selection_head).collect::(); - let prev_chars = snapshot - .reversed_chars_at(selection_head) - .collect::(); - for (pair, enabled) in scope.brackets() { - if enabled - && pair.close - && prev_chars.starts_with(pair.start.as_str()) - && next_chars.starts_with(pair.end.as_str()) - { - bracket_pair = Some(pair.clone()); - break; + let max_lookup_length = scope + .brackets() + .map(|(pair, _)| { + pair.start + .as_str() + .chars() + .count() + .max(pair.end.as_str().chars().count()) + }) + .max(); + if let Some(max_lookup_length) = max_lookup_length { + let next_text = snapshot + .chars_at(selection_head) + .take(max_lookup_length) + .collect::(); + let prev_text = snapshot + .reversed_chars_at(selection_head) + .take(max_lookup_length) + .collect::(); + + for (pair, enabled) in scope.brackets() { + if enabled + && pair.close + && prev_text.starts_with(pair.start.as_str()) + && next_text.starts_with(pair.end.as_str()) + { + bracket_pair = Some(pair.clone()); + break; + } } } + if let Some(pair) = bracket_pair { let snapshot_settings = snapshot.language_settings_at(selection_head, cx); let autoclose_enabled = @@ -9029,14 +9748,16 @@ impl Editor { } } if let Some(current_ranges) = snippet.ranges.get(snippet.active_index) { - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select_anchor_ranges(current_ranges.iter().cloned()) + self.change_selections(Default::default(), window, cx, |s| { + // Reverse order so that the first range is the newest created selection. + // Completions will use it and autoscroll will prioritize it. + s.select_ranges(current_ranges.iter().rev().cloned()) }); - if let Some(choices) = &snippet.choices[snippet.active_index] { - if let Some(selection) = current_ranges.first() { - self.show_snippet_choices(&choices, selection.clone(), cx); - } + if let Some(choices) = &snippet.choices[snippet.active_index] + && let Some(selection) = current_ranges.first() + { + self.show_snippet_choices(choices, selection.clone(), cx); } // If snippet state is not at the last tabstop, push it back on the stack @@ -9058,7 +9779,10 @@ impl Editor { } pub fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + if self.read_only(cx) { + return; + } + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.transact(window, cx, |this, window, cx| { this.select_autoclose_pair(window, cx); let mut linked_ranges = HashMap::<_, Vec<_>>::default(); @@ -9117,9 +9841,7 @@ impl Editor { } } - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(selections) - }); + this.change_selections(Default::default(), window, cx, |s| s.select(selections)); this.insert("", window, cx); let empty_str: Arc = Arc::from(""); for (buffer, edits) in linked_ranges { @@ -9147,15 +9869,18 @@ impl Editor { this.edit(edits, None, cx); }) } - this.refresh_inline_completion(true, false, window, cx); + this.refresh_edit_prediction(true, false, window, cx); linked_editing_ranges::refresh_linked_ranges(this, window, cx); }); } pub fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + if self.read_only(cx) { + return; + } + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.transact(window, cx, |this, window, cx| { - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + this.change_selections(Default::default(), window, cx, |s| { s.move_with(|map, selection| { if selection.is_empty() { let cursor = movement::right(map, selection.head()); @@ -9166,12 +9891,17 @@ impl Editor { }) }); this.insert("", window, cx); - this.refresh_inline_completion(true, false, window, cx); + this.refresh_edit_prediction(true, false, window, cx); }); } pub fn backtab(&mut self, _: &Backtab, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + if self.mode.is_single_line() { + cx.propagate(); + return; + } + + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); if self.move_to_prev_snippet_tabstop(window, cx) { return; } @@ -9179,14 +9909,19 @@ impl Editor { } pub fn tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context) { + if self.mode.is_single_line() { + cx.propagate(); + return; + } + if self.move_to_next_snippet_tabstop(window, cx) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); return; } if self.read_only(cx) { return; } - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let mut selections = self.selections.all_adjusted(cx); let buffer = self.buffer.read(cx); let snapshot = buffer.snapshot(cx); @@ -9288,10 +10023,8 @@ impl Editor { self.transact(window, cx, |this, window, cx| { this.buffer.update(cx, |b, cx| b.edit(edits, None, cx)); - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(selections) - }); - this.refresh_inline_completion(true, false, window, cx); + this.change_selections(Default::default(), window, cx, |s| s.select(selections)); + this.refresh_edit_prediction(true, false, window, cx); }); } @@ -9299,7 +10032,12 @@ impl Editor { if self.read_only(cx) { return; } - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + if self.mode.is_single_line() { + cx.propagate(); + return; + } + + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let mut selections = self.selections.all::(cx); let mut prev_edited_row = 0; let mut row_delta = 0; @@ -9318,9 +10056,7 @@ impl Editor { self.transact(window, cx, |this, window, cx| { this.buffer.update(cx, |b, cx| b.edit(edits, None, cx)); - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(selections) - }); + this.change_selections(Default::default(), window, cx, |s| s.select(selections)); }); } @@ -9404,7 +10140,12 @@ impl Editor { if self.read_only(cx) { return; } - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + if self.mode.is_single_line() { + cx.propagate(); + return; + } + + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let selections = self.selections.all::(cx); let mut deletion_ranges = Vec::new(); @@ -9419,10 +10160,10 @@ impl Editor { // Avoid re-outdenting a row that has already been outdented by a // previous selection. - if let Some(last_row) = last_outdent { - if last_row == rows.start { - rows.start = rows.start.next_row(); - } + if let Some(last_row) = last_outdent + && last_row == rows.start + { + rows.start = rows.start.next_row(); } let has_multiple_rows = rows.len() > 1; for row in rows.iter_rows() { @@ -9468,9 +10209,7 @@ impl Editor { ); }); let selections = this.selections.all::(cx); - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(selections) - }); + this.change_selections(Default::default(), window, cx, |s| s.select(selections)); }); } @@ -9478,7 +10217,12 @@ impl Editor { if self.read_only(cx) { return; } - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + if self.mode.is_single_line() { + cx.propagate(); + return; + } + + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let selections = self .selections .all::(cx) @@ -9490,14 +10234,12 @@ impl Editor { buffer.autoindent_ranges(selections, cx); }); let selections = this.selections.all::(cx); - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(selections) - }); + this.change_selections(Default::default(), window, cx, |s| s.select(selections)); }); } pub fn delete_line(&mut self, _: &DeleteLine, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let selections = self.selections.all::(cx); @@ -9573,7 +10315,7 @@ impl Editor { }) .collect(); - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + this.change_selections(Default::default(), window, cx, |s| { s.select(new_selections); }); }); @@ -9599,11 +10341,11 @@ impl Editor { MultiBufferRow(selection.end.row) }; - if let Some(last_row_range) = row_ranges.last_mut() { - if start <= last_row_range.end { - last_row_range.end = end; - continue; - } + if let Some(last_row_range) = row_ranges.last_mut() + && start <= last_row_range.end + { + last_row_range.end = end; + continue; } row_ranges.push(start..end); } @@ -9639,14 +10381,14 @@ impl Editor { } } - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + this.change_selections(Default::default(), window, cx, |s| { s.select_anchor_ranges(cursor_positions) }); }); } pub fn join_lines(&mut self, _: &JoinLines, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.join_lines_impl(true, window, cx); } @@ -9656,7 +10398,18 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.manipulate_lines(window, cx, |lines| lines.sort()) + self.manipulate_immutable_lines(window, cx, |lines| lines.sort()) + } + + pub fn sort_lines_by_length( + &mut self, + _: &SortLinesByLength, + window: &mut Window, + cx: &mut Context, + ) { + self.manipulate_immutable_lines(window, cx, |lines| { + lines.sort_by_key(|&line| line.chars().count()) + }) } pub fn sort_lines_case_insensitive( @@ -9665,7 +10418,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.manipulate_lines(window, cx, |lines| { + self.manipulate_immutable_lines(window, cx, |lines| { lines.sort_by_key(|line| line.to_lowercase()) }) } @@ -9676,7 +10429,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.manipulate_lines(window, cx, |lines| { + self.manipulate_immutable_lines(window, cx, |lines| { let mut seen = HashSet::default(); lines.retain(|line| seen.insert(line.to_lowercase())); }) @@ -9688,7 +10441,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.manipulate_lines(window, cx, |lines| { + self.manipulate_immutable_lines(window, cx, |lines| { let mut seen = HashSet::default(); lines.retain(|line| seen.insert(*line)); }) @@ -9708,7 +10461,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let mut buffer_ids = HashSet::default(); let snapshot = self.buffer().read(cx).snapshot(cx); for selection in self.selections.all::(cx) { @@ -9725,7 +10478,7 @@ impl Editor { } pub fn git_restore(&mut self, _: &Restore, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let selections = self .selections .all(cx) @@ -9770,7 +10523,7 @@ impl Editor { ) { if let Some(working_directory) = self.active_excerpt(cx).and_then(|(_, buffer, _)| { let project_path = buffer.read(cx).project_path(cx)?; - let project = self.project.as_ref()?.read(cx); + let project = self.project()?.read(cx); let entry = project.entry_for_path(&project_path, cx)?; let parent = match &entry.canonical_path { Some(canonical_path) => canonical_path.to_path_buf(), @@ -9792,9 +10545,6 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if !cx.has_flag::() { - return; - } let source = self .buffer .read(cx) @@ -9847,7 +10597,6 @@ impl Editor { cloned_prompt.clone().into_any_element() }), priority: 0, - render_in_minimap: true, }]; let focus_handle = bp_prompt.focus_handle(cx); @@ -9877,16 +10626,12 @@ impl Editor { snapshot: &EditorSnapshot, cx: &mut Context, ) -> Option<(Anchor, Breakpoint)> { - let project = self.project.clone()?; - - let buffer_id = breakpoint_position.buffer_id.or_else(|| { - snapshot - .buffer_snapshot - .buffer_id_for_excerpt(breakpoint_position.excerpt_id) - })?; + let buffer = self + .buffer + .read(cx) + .buffer_for_anchor(breakpoint_position, cx)?; let enclosing_excerpt = breakpoint_position.excerpt_id; - let buffer = project.read(cx).buffer_for_id(buffer_id, cx)?; let buffer_snapshot = buffer.read(cx).snapshot(); let row = buffer_snapshot @@ -9898,8 +10643,7 @@ impl Editor { .buffer_snapshot .anchor_after(Point::new(row, line_len)); - let bp = self - .breakpoint_store + self.breakpoint_store .as_ref()? .read_with(cx, |breakpoint_store, cx| { breakpoint_store @@ -9924,8 +10668,7 @@ impl Editor { None } }) - }); - bp + }) } pub fn edit_log_breakpoint( @@ -9961,7 +10704,7 @@ impl Editor { let cursors = self .selections .disjoint_anchors() - .into_iter() + .iter() .map(|selection| { let cursor_position: Point = selection.head().to_point(&snapshot.buffer_snapshot); @@ -10061,21 +10804,11 @@ impl Editor { return; }; - let Some(buffer_id) = breakpoint_position.buffer_id.or_else(|| { - if breakpoint_position == Anchor::min() { - self.buffer() - .read(cx) - .excerpt_buffer_ids() - .into_iter() - .next() - } else { - None - } - }) else { - return; - }; - - let Some(buffer) = self.buffer().read(cx).buffer(buffer_id) else { + let Some(buffer) = self + .buffer + .read(cx) + .buffer_for_anchor(breakpoint_position, cx) + else { return; }; @@ -10134,22 +10867,22 @@ impl Editor { } pub fn reverse_lines(&mut self, _: &ReverseLines, window: &mut Window, cx: &mut Context) { - self.manipulate_lines(window, cx, |lines| lines.reverse()) + self.manipulate_immutable_lines(window, cx, |lines| lines.reverse()) } pub fn shuffle_lines(&mut self, _: &ShuffleLines, window: &mut Window, cx: &mut Context) { - self.manipulate_lines(window, cx, |lines| lines.shuffle(&mut thread_rng())) + self.manipulate_immutable_lines(window, cx, |lines| lines.shuffle(&mut thread_rng())) } - fn manipulate_lines( + fn manipulate_lines( &mut self, window: &mut Window, cx: &mut Context, - mut callback: Fn, + mut manipulate: M, ) where - Fn: FnMut(&mut Vec<&str>), + M: FnMut(&str) -> LineManipulationResult, { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = self.buffer.read(cx).snapshot(cx); @@ -10180,18 +10913,18 @@ impl Editor { .text_for_range(start_point..end_point) .collect::(); - let mut lines = text.split('\n').collect_vec(); + let LineManipulationResult { + new_text, + line_count_before, + line_count_after, + } = manipulate(&text); - let lines_before = lines.len(); - callback(&mut lines); - let lines_after = lines.len(); - - edits.push((start_point..end_point, lines.join("\n"))); + edits.push((start_point..end_point, new_text)); // Selections must change based on added and removed line count let start_row = MultiBufferRow(start_point.row + added_lines as u32 - removed_lines as u32); - let end_row = MultiBufferRow(start_row.0 + lines_after.saturating_sub(1) as u32); + let end_row = MultiBufferRow(start_row.0 + line_count_after.saturating_sub(1) as u32); new_selections.push(Selection { id: selection.id, start: start_row, @@ -10200,10 +10933,10 @@ impl Editor { reversed: selection.reversed, }); - if lines_after > lines_before { - added_lines += lines_after - lines_before; - } else if lines_before > lines_after { - removed_lines += lines_before - lines_after; + if line_count_after > line_count_before { + added_lines += line_count_after - line_count_before; + } else if line_count_before > line_count_after { + removed_lines += line_count_before - line_count_after; } } @@ -10229,7 +10962,7 @@ impl Editor { }) .collect(); - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + this.change_selections(Default::default(), window, cx, |s| { s.select(new_selections); }); @@ -10237,15 +10970,169 @@ impl Editor { }); } - pub fn toggle_case(&mut self, _: &ToggleCase, window: &mut Window, cx: &mut Context) { - self.manipulate_text(window, cx, |text| { - let has_upper_case_characters = text.chars().any(|c| c.is_uppercase()); - if has_upper_case_characters { - text.to_lowercase() - } else { - text.to_uppercase() + fn manipulate_immutable_lines( + &mut self, + window: &mut Window, + cx: &mut Context, + mut callback: Fn, + ) where + Fn: FnMut(&mut Vec<&str>), + { + self.manipulate_lines(window, cx, |text| { + let mut lines: Vec<&str> = text.split('\n').collect(); + let line_count_before = lines.len(); + + callback(&mut lines); + + LineManipulationResult { + new_text: lines.join("\n"), + line_count_before, + line_count_after: lines.len(), } - }) + }); + } + + fn manipulate_mutable_lines( + &mut self, + window: &mut Window, + cx: &mut Context, + mut callback: Fn, + ) where + Fn: FnMut(&mut Vec>), + { + self.manipulate_lines(window, cx, |text| { + let mut lines: Vec> = text.split('\n').map(Cow::from).collect(); + let line_count_before = lines.len(); + + callback(&mut lines); + + LineManipulationResult { + new_text: lines.join("\n"), + line_count_before, + line_count_after: lines.len(), + } + }); + } + + pub fn convert_indentation_to_spaces( + &mut self, + _: &ConvertIndentationToSpaces, + window: &mut Window, + cx: &mut Context, + ) { + let settings = self.buffer.read(cx).language_settings(cx); + let tab_size = settings.tab_size.get() as usize; + + self.manipulate_mutable_lines(window, cx, |lines| { + // Allocates a reasonably sized scratch buffer once for the whole loop + let mut reindented_line = String::with_capacity(MAX_LINE_LEN); + // Avoids recomputing spaces that could be inserted many times + let space_cache: Vec> = (1..=tab_size) + .map(|n| IndentSize::spaces(n as u32).chars().collect()) + .collect(); + + for line in lines.iter_mut().filter(|line| !line.is_empty()) { + let mut chars = line.as_ref().chars(); + let mut col = 0; + let mut changed = false; + + for ch in chars.by_ref() { + match ch { + ' ' => { + reindented_line.push(' '); + col += 1; + } + '\t' => { + // \t are converted to spaces depending on the current column + let spaces_len = tab_size - (col % tab_size); + reindented_line.extend(&space_cache[spaces_len - 1]); + col += spaces_len; + changed = true; + } + _ => { + // If we dont append before break, the character is consumed + reindented_line.push(ch); + break; + } + } + } + + if !changed { + reindented_line.clear(); + continue; + } + // Append the rest of the line and replace old reference with new one + reindented_line.extend(chars); + *line = Cow::Owned(reindented_line.clone()); + reindented_line.clear(); + } + }); + } + + pub fn convert_indentation_to_tabs( + &mut self, + _: &ConvertIndentationToTabs, + window: &mut Window, + cx: &mut Context, + ) { + let settings = self.buffer.read(cx).language_settings(cx); + let tab_size = settings.tab_size.get() as usize; + + self.manipulate_mutable_lines(window, cx, |lines| { + // Allocates a reasonably sized buffer once for the whole loop + let mut reindented_line = String::with_capacity(MAX_LINE_LEN); + // Avoids recomputing spaces that could be inserted many times + let space_cache: Vec> = (1..=tab_size) + .map(|n| IndentSize::spaces(n as u32).chars().collect()) + .collect(); + + for line in lines.iter_mut().filter(|line| !line.is_empty()) { + let mut chars = line.chars(); + let mut spaces_count = 0; + let mut first_non_indent_char = None; + let mut changed = false; + + for ch in chars.by_ref() { + match ch { + ' ' => { + // Keep track of spaces. Append \t when we reach tab_size + spaces_count += 1; + changed = true; + if spaces_count == tab_size { + reindented_line.push('\t'); + spaces_count = 0; + } + } + '\t' => { + reindented_line.push('\t'); + spaces_count = 0; + } + _ => { + // Dont append it yet, we might have remaining spaces + first_non_indent_char = Some(ch); + break; + } + } + } + + if !changed { + reindented_line.clear(); + continue; + } + // Remaining spaces that didn't make a full tab stop + if spaces_count > 0 { + reindented_line.extend(&space_cache[spaces_count - 1]); + } + // If we consume an extra character that was not indentation, add it back + if let Some(extra_char) = first_non_indent_char { + reindented_line.push(extra_char); + } + // Append the rest of the line and replace old reference with new one + reindented_line.extend(chars); + *line = Cow::Owned(reindented_line.clone()); + reindented_line.clear(); + } + }); } pub fn convert_to_upper_case( @@ -10338,6 +11225,26 @@ impl Editor { }) } + pub fn convert_to_sentence_case( + &mut self, + _: &ConvertToSentenceCase, + window: &mut Window, + cx: &mut Context, + ) { + self.manipulate_text(window, cx, |text| text.to_case(Case::Sentence)) + } + + pub fn toggle_case(&mut self, _: &ToggleCase, window: &mut Window, cx: &mut Context) { + self.manipulate_text(window, cx, |text| { + let has_upper_case_characters = text.chars().any(|c| c.is_uppercase()); + if has_upper_case_characters { + text.to_lowercase() + } else { + text.to_uppercase() + } + }) + } + pub fn convert_to_rot13( &mut self, _: &ConvertToRot13, @@ -10378,7 +11285,6 @@ impl Editor { where Fn: FnMut(&str) -> String, { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = self.buffer.read(cx).snapshot(cx); let mut new_selections = Vec::new(); @@ -10389,13 +11295,8 @@ impl Editor { let selection_is_empty = selection.is_empty(); let (start, end) = if selection_is_empty { - let word_range = movement::surrounding_word( - &display_map, - selection.start.to_display_point(&display_map), - ); - let start = word_range.start.to_offset(&display_map, Bias::Left); - let end = word_range.end.to_offset(&display_map, Bias::Left); - (start, end) + let (word_range, _) = buffer.surrounding_word(selection.start, false); + (word_range.start, word_range.end) } else { (selection.start, selection.end) }; @@ -10421,7 +11322,7 @@ impl Editor { buffer.edit(edits, None, cx); }); - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + this.change_selections(Default::default(), window, cx, |s| { s.select(new_selections); }); @@ -10429,6 +11330,44 @@ impl Editor { }); } + pub fn move_selection_on_drop( + &mut self, + selection: &Selection, + target: DisplayPoint, + is_cut: bool, + window: &mut Window, + cx: &mut Context, + ) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = &display_map.buffer_snapshot; + let mut edits = Vec::new(); + let insert_point = display_map + .clip_point(target, Bias::Left) + .to_point(&display_map); + let text = buffer + .text_for_range(selection.start..selection.end) + .collect::(); + if is_cut { + edits.push(((selection.start..selection.end), String::new())); + } + let insert_anchor = buffer.anchor_before(insert_point); + edits.push(((insert_anchor..insert_anchor), text)); + let last_edit_start = insert_anchor.bias_left(buffer); + let last_edit_end = insert_anchor.bias_right(buffer); + self.transact(window, cx, |this, window, cx| { + this.buffer.update(cx, |buffer, cx| { + buffer.edit(edits, None, cx); + }); + this.change_selections(Default::default(), window, cx, |s| { + s.select_anchor_ranges([last_edit_start..last_edit_end]); + }); + }); + } + + pub fn clear_selection_drag_state(&mut self) { + self.selection_drag_state = SelectionDragState::None; + } + pub fn duplicate( &mut self, upwards: bool, @@ -10436,7 +11375,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = &display_map.buffer_snapshot; @@ -10522,7 +11461,11 @@ impl Editor { } pub fn move_line_up(&mut self, _: &MoveLineUp, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); + if self.mode.is_single_line() { + cx.propagate(); + return; + } let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = self.buffer.read(cx).snapshot(cx); @@ -10617,7 +11560,7 @@ impl Editor { } }); this.fold_creases(refold_creases, true, window, cx); - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + this.change_selections(Default::default(), window, cx, |s| { s.select(new_selections); }) }); @@ -10629,7 +11572,11 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); + if self.mode.is_single_line() { + cx.propagate(); + return; + } let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = self.buffer.read(cx).snapshot(cx); @@ -10714,17 +11661,15 @@ impl Editor { } }); this.fold_creases(refold_creases, true, window, cx); - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(new_selections) - }); + this.change_selections(Default::default(), window, cx, |s| s.select(new_selections)); }); } pub fn transpose(&mut self, _: &Transpose, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let text_layout_details = &self.text_layout_details(window); self.transact(window, cx, |this, window, cx| { - let edits = this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + let edits = this.change_selections(Default::default(), window, cx, |s| { let mut edits: Vec<(Range, String)> = Default::default(); s.move_with(|display_map, selection| { if !selection.is_empty() { @@ -10755,7 +11700,7 @@ impl Editor { let transpose_start = display_map .buffer_snapshot .clip_offset(transpose_offset.saturating_sub(1), Bias::Left); - if edits.last().map_or(true, |e| e.0.end <= transpose_start) { + if edits.last().is_none_or(|e| e.0.end <= transpose_start) { let transpose_end = display_map .buffer_snapshot .clip_offset(transpose_offset + 1, Bias::Right); @@ -10772,28 +11717,145 @@ impl Editor { this.buffer .update(cx, |buffer, cx| buffer.edit(edits, None, cx)); let selections = this.selections.all::(cx); - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + this.change_selections(Default::default(), window, cx, |s| { s.select(selections); }); }); } pub fn rewrap(&mut self, _: &Rewrap, _: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); + if self.mode.is_single_line() { + cx.propagate(); + return; + } + self.rewrap_impl(RewrapOptions::default(), cx) } pub fn rewrap_impl(&mut self, options: RewrapOptions, cx: &mut Context) { let buffer = self.buffer.read(cx).snapshot(cx); let selections = self.selections.all::(cx); - let mut selections = selections.iter().peekable(); + + // Split selections to respect paragraph, indent, and comment prefix boundaries. + let wrap_ranges = selections.into_iter().flat_map(|selection| { + let mut non_blank_rows_iter = (selection.start.row..=selection.end.row) + .filter(|row| !buffer.is_line_blank(MultiBufferRow(*row))) + .peekable(); + + let first_row = if let Some(&row) = non_blank_rows_iter.peek() { + row + } else { + return Vec::new(); + }; + + let language_settings = buffer.language_settings_at(selection.head(), cx); + let language_scope = buffer.language_scope_at(selection.head()); + + let indent_and_prefix_for_row = + |row: u32| -> (IndentSize, Option, Option) { + let indent = buffer.indent_size_for_line(MultiBufferRow(row)); + let (comment_prefix, rewrap_prefix) = + if let Some(language_scope) = &language_scope { + let indent_end = Point::new(row, indent.len); + let comment_prefix = language_scope + .line_comment_prefixes() + .iter() + .find(|prefix| buffer.contains_str_at(indent_end, prefix)) + .map(|prefix| prefix.to_string()); + let line_end = Point::new(row, buffer.line_len(MultiBufferRow(row))); + let line_text_after_indent = buffer + .text_for_range(indent_end..line_end) + .collect::(); + let rewrap_prefix = language_scope + .rewrap_prefixes() + .iter() + .find_map(|prefix_regex| { + prefix_regex.find(&line_text_after_indent).map(|mat| { + if mat.start() == 0 { + Some(mat.as_str().to_string()) + } else { + None + } + }) + }) + .flatten(); + (comment_prefix, rewrap_prefix) + } else { + (None, None) + }; + (indent, comment_prefix, rewrap_prefix) + }; + + let mut ranges = Vec::new(); + let from_empty_selection = selection.is_empty(); + + let mut current_range_start = first_row; + let mut prev_row = first_row; + let ( + mut current_range_indent, + mut current_range_comment_prefix, + mut current_range_rewrap_prefix, + ) = indent_and_prefix_for_row(first_row); + + for row in non_blank_rows_iter.skip(1) { + let has_paragraph_break = row > prev_row + 1; + + let (row_indent, row_comment_prefix, row_rewrap_prefix) = + indent_and_prefix_for_row(row); + + let has_indent_change = row_indent != current_range_indent; + let has_comment_change = row_comment_prefix != current_range_comment_prefix; + + let has_boundary_change = has_comment_change + || row_rewrap_prefix.is_some() + || (has_indent_change && current_range_comment_prefix.is_some()); + + if has_paragraph_break || has_boundary_change { + ranges.push(( + language_settings.clone(), + Point::new(current_range_start, 0) + ..Point::new(prev_row, buffer.line_len(MultiBufferRow(prev_row))), + current_range_indent, + current_range_comment_prefix.clone(), + current_range_rewrap_prefix.clone(), + from_empty_selection, + )); + current_range_start = row; + current_range_indent = row_indent; + current_range_comment_prefix = row_comment_prefix; + current_range_rewrap_prefix = row_rewrap_prefix; + } + prev_row = row; + } + + ranges.push(( + language_settings.clone(), + Point::new(current_range_start, 0) + ..Point::new(prev_row, buffer.line_len(MultiBufferRow(prev_row))), + current_range_indent, + current_range_comment_prefix, + current_range_rewrap_prefix, + from_empty_selection, + )); + + ranges + }); let mut edits = Vec::new(); let mut rewrapped_row_ranges = Vec::>::new(); - while let Some(selection) = selections.next() { - let mut start_row = selection.start.row; - let mut end_row = selection.end.row; + for ( + language_settings, + wrap_range, + indent_size, + comment_prefix, + rewrap_prefix, + from_empty_selection, + ) in wrap_ranges + { + let mut start_row = wrap_range.start.row; + let mut end_row = wrap_range.end.row; // Skip selections that overlap with a range that has already been rewrapped. let selection_range = start_row..end_row; @@ -10804,56 +11866,22 @@ impl Editor { continue; } - let tab_size = buffer.language_settings_at(selection.head(), cx).tab_size; - - // Since not all lines in the selection may be at the same indent - // level, choose the indent size that is the most common between all - // of the lines. - // - // If there is a tie, we use the deepest indent. - let (indent_size, indent_end) = { - let mut indent_size_occurrences = HashMap::default(); - let mut rows_by_indent_size = HashMap::>::default(); - - for row in start_row..=end_row { - let indent = buffer.indent_size_for_line(MultiBufferRow(row)); - rows_by_indent_size.entry(indent).or_default().push(row); - *indent_size_occurrences.entry(indent).or_insert(0) += 1; - } - - let indent_size = indent_size_occurrences - .into_iter() - .max_by_key(|(indent, count)| (*count, indent.len_with_expanded_tabs(tab_size))) - .map(|(indent, _)| indent) - .unwrap_or_default(); - let row = rows_by_indent_size[&indent_size][0]; - let indent_end = Point::new(row, indent_size.len); - - (indent_size, indent_end) - }; - - let mut line_prefix = indent_size.chars().collect::(); + let tab_size = language_settings.tab_size; + let indent_prefix = indent_size.chars().collect::(); + let mut line_prefix = indent_prefix.clone(); let mut inside_comment = false; - if let Some(comment_prefix) = - buffer - .language_scope_at(selection.head()) - .and_then(|language| { - language - .line_comment_prefixes() - .iter() - .find(|prefix| buffer.contains_str_at(indent_end, prefix)) - .cloned() - }) - { - line_prefix.push_str(&comment_prefix); + if let Some(prefix) = &comment_prefix { + line_prefix.push_str(prefix); inside_comment = true; } + if let Some(prefix) = &rewrap_prefix { + line_prefix.push_str(prefix); + } - let language_settings = buffer.language_settings_at(selection.head(), cx); let allow_rewrap_based_on_language = match language_settings.allow_rewrap { RewrapBehavior::InComments => inside_comment, - RewrapBehavior::InSelections => !selection.is_empty(), + RewrapBehavior::InSelections => !wrap_range.is_empty(), RewrapBehavior::Anywhere => true, }; @@ -10864,11 +11892,12 @@ impl Editor { continue; } - if selection.is_empty() { + if from_empty_selection { 'expand_upwards: while start_row > 0 { let prev_row = start_row - 1; if buffer.contains_str_at(Point::new(prev_row, 0), &line_prefix) && buffer.line_len(MultiBufferRow(prev_row)) as usize > line_prefix.len() + && !buffer.is_line_blank(MultiBufferRow(prev_row)) { start_row = prev_row; } else { @@ -10880,6 +11909,7 @@ impl Editor { let next_row = end_row + 1; if buffer.contains_str_at(Point::new(next_row, 0), &line_prefix) && buffer.line_len(MultiBufferRow(next_row)) as usize > line_prefix.len() + && !buffer.is_line_blank(MultiBufferRow(next_row)) { end_row = next_row; } else { @@ -10894,12 +11924,18 @@ impl Editor { let selection_text = buffer.text_for_range(start..end).collect::(); let Some(lines_without_prefixes) = selection_text .lines() - .map(|line| { - line.strip_prefix(&line_prefix) - .or_else(|| line.trim_start().strip_prefix(&line_prefix.trim_start())) - .with_context(|| { - format!("line did not start with prefix {line_prefix:?}: {line:?}") - }) + .enumerate() + .map(|(ix, line)| { + let line_trimmed = line.trim_start(); + if rewrap_prefix.is_some() && ix > 0 { + Ok(line_trimmed) + } else { + line_trimmed + .strip_prefix(&line_prefix.trim_start()) + .with_context(|| { + format!("line did not start with prefix {line_prefix:?}: {line:?}") + }) + } }) .collect::, _>>() .log_err() @@ -10912,8 +11948,16 @@ impl Editor { .language_settings_at(Point::new(start_row, 0), cx) .preferred_line_length as usize }); + + let subsequent_lines_prefix = if let Some(rewrap_prefix_str) = &rewrap_prefix { + format!("{}{}", indent_prefix, " ".repeat(rewrap_prefix_str.len())) + } else { + line_prefix.clone() + }; + let wrapped_text = wrap_with_prefix( line_prefix, + subsequent_lines_prefix, lines_without_prefixes.join("\n"), wrap_column, tab_size, @@ -10986,7 +12030,7 @@ impl Editor { } self.transact(window, cx, |this, window, cx| { - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + this.change_selections(Default::default(), window, cx, |s| { s.select(selections); }); this.insert("", window, cx); @@ -10995,14 +12039,14 @@ impl Editor { } pub fn cut(&mut self, _: &Cut, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let item = self.cut_common(window, cx); cx.write_to_clipboard(item); } pub fn kill_ring_cut(&mut self, _: &KillRingCut, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - self.change_selections(None, window, cx, |s| { + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); + self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.move_with(|snapshot, sel| { if sel.is_empty() { sel.end = DisplayPoint::new(sel.end.row(), snapshot.line_len(sel.end.row())) @@ -11019,7 +12063,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let (text, metadata) = if let Some(KillRing(item)) = cx.try_global() { if let Some(ClipboardEntry::String(kill_ring)) = item.entries().first() { (kill_ring.text().to_string(), kill_ring.metadata_json()) @@ -11138,6 +12182,8 @@ impl Editor { let clipboard_text = Cow::Borrowed(text); self.transact(window, cx, |this, window, cx| { + let had_active_edit_prediction = this.has_active_edit_prediction(); + if let Some(mut clipboard_selections) = clipboard_selections { let old_selections = this.selections.all::(cx); let all_selections_were_entire_line = @@ -11206,17 +12252,55 @@ impl Editor { }); let selections = this.selections.all::(cx); - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(selections) - }); + this.change_selections(Default::default(), window, cx, |s| s.select(selections)); } else { this.insert(&clipboard_text, window, cx); } + + let trigger_in_words = + this.show_edit_predictions_in_menu() || !had_active_edit_prediction; + + this.trigger_completion_on_input(text, trigger_in_words, window, cx); }); } + pub fn diff_clipboard_with_selection( + &mut self, + _: &DiffClipboardWithSelection, + window: &mut Window, + cx: &mut Context, + ) { + let selections = self.selections.all::(cx); + + if selections.is_empty() { + log::warn!("There should always be at least one selection in Zed. This is a bug."); + return; + }; + + let clipboard_text = match cx.read_from_clipboard() { + Some(item) => match item.entries().first() { + Some(ClipboardEntry::String(text)) => Some(text.text().to_string()), + _ => None, + }, + None => None, + }; + + let Some(clipboard_text) = clipboard_text else { + log::warn!("Clipboard doesn't contain text."); + return; + }; + + window.dispatch_action( + Box::new(DiffClipboardWithSelectionData { + clipboard_text, + editor: cx.entity(), + }), + cx, + ); + } + pub fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); if let Some(item) = cx.read_from_clipboard() { let entries = item.entries(); @@ -11241,13 +12325,13 @@ impl Editor { return; } - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); if let Some(transaction_id) = self.buffer.update(cx, |buffer, cx| buffer.undo(cx)) { if let Some((selections, _)) = self.selection_history.transaction(transaction_id).cloned() { - self.change_selections(None, window, cx, |s| { + self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_anchors(selections.to_vec()); }); } else { @@ -11260,7 +12344,7 @@ impl Editor { } self.request_autoscroll(Autoscroll::fit(), cx); self.unmark_text(window, cx); - self.refresh_inline_completion(true, false, window, cx); + self.refresh_edit_prediction(true, false, window, cx); cx.emit(EditorEvent::Edited { transaction_id }); cx.emit(EditorEvent::TransactionUndone { transaction_id }); } @@ -11271,13 +12355,13 @@ impl Editor { return; } - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); if let Some(transaction_id) = self.buffer.update(cx, |buffer, cx| buffer.redo(cx)) { if let Some((_, Some(selections))) = self.selection_history.transaction(transaction_id).cloned() { - self.change_selections(None, window, cx, |s| { + self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_anchors(selections.to_vec()); }); } else { @@ -11290,7 +12374,7 @@ impl Editor { } self.request_autoscroll(Autoscroll::fit(), cx); self.unmark_text(window, cx); - self.refresh_inline_completion(true, false, window, cx); + self.refresh_edit_prediction(true, false, window, cx); cx.emit(EditorEvent::Edited { transaction_id }); } } @@ -11306,8 +12390,8 @@ impl Editor { } pub fn move_left(&mut self, _: &MoveLeft, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); + self.change_selections(Default::default(), window, cx, |s| { s.move_with(|map, selection| { let cursor = if selection.is_empty() { movement::left(map, selection.start) @@ -11320,15 +12404,15 @@ impl Editor { } pub fn select_left(&mut self, _: &SelectLeft, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); + self.change_selections(Default::default(), window, cx, |s| { s.move_heads_with(|map, head, _| (movement::left(map, head), SelectionGoal::None)); }) } pub fn move_right(&mut self, _: &MoveRight, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); + self.change_selections(Default::default(), window, cx, |s| { s.move_with(|map, selection| { let cursor = if selection.is_empty() { movement::right(map, selection.end) @@ -11341,8 +12425,8 @@ impl Editor { } pub fn select_right(&mut self, _: &SelectRight, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); + self.change_selections(Default::default(), window, cx, |s| { s.move_heads_with(|map, head, _| (movement::right(map, head), SelectionGoal::None)); }) } @@ -11352,18 +12436,18 @@ impl Editor { return; } - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if self.mode.is_single_line() { cx.propagate(); return; } - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let text_layout_details = &self.text_layout_details(window); let selection_count = self.selections.count(); let first_selection = self.selections.first_anchor(); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.change_selections(Default::default(), window, cx, |s| { s.move_with(|map, selection| { if !selection.is_empty() { selection.goal = SelectionGoal::None; @@ -11395,16 +12479,16 @@ impl Editor { return; } - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if self.mode.is_single_line() { cx.propagate(); return; } - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let text_layout_details = &self.text_layout_details(window); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.change_selections(Default::default(), window, cx, |s| { s.move_with(|map, selection| { if !selection.is_empty() { selection.goal = SelectionGoal::None; @@ -11432,16 +12516,16 @@ impl Editor { return; } - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if self.mode.is_single_line() { cx.propagate(); return; } - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let text_layout_details = &self.text_layout_details(window); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.change_selections(Default::default(), window, cx, |s| { s.move_with(|map, selection| { if !selection.is_empty() { selection.goal = SelectionGoal::None; @@ -11465,9 +12549,9 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let text_layout_details = &self.text_layout_details(window); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.change_selections(Default::default(), window, cx, |s| { s.move_heads_with(|map, head, goal| { movement::down_by_rows(map, head, action.lines, goal, false, text_layout_details) }) @@ -11480,9 +12564,9 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let text_layout_details = &self.text_layout_details(window); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.change_selections(Default::default(), window, cx, |s| { s.move_heads_with(|map, head, goal| { movement::up_by_rows(map, head, action.lines, goal, false, text_layout_details) }) @@ -11499,11 +12583,11 @@ impl Editor { return; }; - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let text_layout_details = &self.text_layout_details(window); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.change_selections(Default::default(), window, cx, |s| { s.move_heads_with(|map, head, goal| { movement::up_by_rows(map, head, row_count, goal, false, text_layout_details) }) @@ -11530,7 +12614,7 @@ impl Editor { return; } - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -11539,17 +12623,17 @@ impl Editor { return; }; - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); - let autoscroll = if action.center_cursor { - Autoscroll::center() + let effects = if action.center_cursor { + SelectionEffects::scroll(Autoscroll::center()) } else { - Autoscroll::fit() + SelectionEffects::default() }; let text_layout_details = &self.text_layout_details(window); - self.change_selections(Some(autoscroll), window, cx, |s| { + self.change_selections(effects, window, cx, |s| { s.move_with(|map, selection| { if !selection.is_empty() { selection.goal = SelectionGoal::None; @@ -11568,9 +12652,9 @@ impl Editor { } pub fn select_up(&mut self, _: &SelectUp, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let text_layout_details = &self.text_layout_details(window); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.change_selections(Default::default(), window, cx, |s| { s.move_heads_with(|map, head, goal| { movement::up(map, head, goal, false, text_layout_details) }) @@ -11580,18 +12664,18 @@ impl Editor { pub fn move_down(&mut self, _: &MoveDown, window: &mut Window, cx: &mut Context) { self.take_rename(true, window, cx); - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if self.mode.is_single_line() { cx.propagate(); return; } - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let text_layout_details = &self.text_layout_details(window); let selection_count = self.selections.count(); let first_selection = self.selections.first_anchor(); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.change_selections(Default::default(), window, cx, |s| { s.move_with(|map, selection| { if !selection.is_empty() { selection.goal = SelectionGoal::None; @@ -11623,11 +12707,11 @@ impl Editor { return; }; - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let text_layout_details = &self.text_layout_details(window); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.change_selections(Default::default(), window, cx, |s| { s.move_heads_with(|map, head, goal| { movement::down_by_rows(map, head, row_count, goal, false, text_layout_details) }) @@ -11654,7 +12738,7 @@ impl Editor { return; } - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -11663,16 +12747,16 @@ impl Editor { return; }; - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); - let autoscroll = if action.center_cursor { - Autoscroll::center() + let effects = if action.center_cursor { + SelectionEffects::scroll(Autoscroll::center()) } else { - Autoscroll::fit() + SelectionEffects::default() }; let text_layout_details = &self.text_layout_details(window); - self.change_selections(Some(autoscroll), window, cx, |s| { + self.change_selections(effects, window, cx, |s| { s.move_with(|map, selection| { if !selection.is_empty() { selection.goal = SelectionGoal::None; @@ -11691,9 +12775,9 @@ impl Editor { } pub fn select_down(&mut self, _: &SelectDown, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let text_layout_details = &self.text_layout_details(window); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.change_selections(Default::default(), window, cx, |s| { s.move_heads_with(|map, head, goal| { movement::down(map, head, goal, false, text_layout_details) }) @@ -11744,14 +12828,46 @@ impl Editor { } } + pub fn signature_help_prev( + &mut self, + _: &SignatureHelpPrevious, + _: &mut Window, + cx: &mut Context, + ) { + if let Some(popover) = self.signature_help_state.popover_mut() { + if popover.current_signature == 0 { + popover.current_signature = popover.signatures.len() - 1; + } else { + popover.current_signature -= 1; + } + cx.notify(); + } + } + + pub fn signature_help_next( + &mut self, + _: &SignatureHelpNext, + _: &mut Window, + cx: &mut Context, + ) { + if let Some(popover) = self.signature_help_state.popover_mut() { + if popover.current_signature + 1 == popover.signatures.len() { + popover.current_signature = 0; + } else { + popover.current_signature += 1; + } + cx.notify(); + } + } + pub fn move_to_previous_word_start( &mut self, _: &MoveToPreviousWordStart, window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); + self.change_selections(Default::default(), window, cx, |s| { s.move_cursors_with(|map, head, _| { ( movement::previous_word_start(map, head), @@ -11767,8 +12883,8 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); + self.change_selections(Default::default(), window, cx, |s| { s.move_cursors_with(|map, head, _| { ( movement::previous_subword_start(map, head), @@ -11784,8 +12900,8 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); + self.change_selections(Default::default(), window, cx, |s| { s.move_heads_with(|map, head, _| { ( movement::previous_word_start(map, head), @@ -11801,8 +12917,8 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); + self.change_selections(Default::default(), window, cx, |s| { s.move_heads_with(|map, head, _| { ( movement::previous_subword_start(map, head), @@ -11818,10 +12934,10 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.transact(window, cx, |this, window, cx| { this.select_autoclose_pair(window, cx); - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + this.change_selections(Default::default(), window, cx, |s| { s.move_with(|map, selection| { if selection.is_empty() { let cursor = if action.ignore_newlines { @@ -11843,10 +12959,10 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.transact(window, cx, |this, window, cx| { this.select_autoclose_pair(window, cx); - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + this.change_selections(Default::default(), window, cx, |s| { s.move_with(|map, selection| { if selection.is_empty() { let cursor = movement::previous_subword_start(map, selection.head()); @@ -11864,8 +12980,8 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); + self.change_selections(Default::default(), window, cx, |s| { s.move_cursors_with(|map, head, _| { (movement::next_word_end(map, head), SelectionGoal::None) }); @@ -11878,8 +12994,8 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); + self.change_selections(Default::default(), window, cx, |s| { s.move_cursors_with(|map, head, _| { (movement::next_subword_end(map, head), SelectionGoal::None) }); @@ -11892,8 +13008,8 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); + self.change_selections(Default::default(), window, cx, |s| { s.move_heads_with(|map, head, _| { (movement::next_word_end(map, head), SelectionGoal::None) }); @@ -11906,8 +13022,8 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); + self.change_selections(Default::default(), window, cx, |s| { s.move_heads_with(|map, head, _| { (movement::next_subword_end(map, head), SelectionGoal::None) }); @@ -11920,9 +13036,9 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.transact(window, cx, |this, window, cx| { - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + this.change_selections(Default::default(), window, cx, |s| { s.move_with(|map, selection| { if selection.is_empty() { let cursor = if action.ignore_newlines { @@ -11944,9 +13060,9 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.transact(window, cx, |this, window, cx| { - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + this.change_selections(Default::default(), window, cx, |s| { s.move_with(|map, selection| { if selection.is_empty() { let cursor = movement::next_subword_end(map, selection.head()); @@ -11964,8 +13080,8 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); + self.change_selections(Default::default(), window, cx, |s| { s.move_cursors_with(|map, head, _| { ( movement::indented_line_beginning( @@ -11986,8 +13102,8 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); + self.change_selections(Default::default(), window, cx, |s| { s.move_heads_with(|map, head, _| { ( movement::indented_line_beginning( @@ -12008,9 +13124,9 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.transact(window, cx, |this, window, cx| { - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + this.change_selections(Default::default(), window, cx, |s| { s.move_with(|_, selection| { selection.reversed = true; }); @@ -12034,8 +13150,8 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); + self.change_selections(Default::default(), window, cx, |s| { s.move_cursors_with(|map, head, _| { ( movement::line_end(map, head, action.stop_at_soft_wraps), @@ -12051,8 +13167,8 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); + self.change_selections(Default::default(), window, cx, |s| { s.move_heads_with(|map, head, _| { ( movement::line_end(map, head, action.stop_at_soft_wraps), @@ -12068,7 +13184,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.transact(window, cx, |this, window, cx| { this.select_to_end_of_line( &SelectToEndOfLine { @@ -12087,7 +13203,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.transact(window, cx, |this, window, cx| { this.select_to_end_of_line( &SelectToEndOfLine { @@ -12106,12 +13222,12 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); + self.change_selections(Default::default(), window, cx, |s| { s.move_with(|map, selection| { selection.collapse_to( movement::start_of_paragraph(map, selection.head(), 1), @@ -12127,12 +13243,12 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); + self.change_selections(Default::default(), window, cx, |s| { s.move_with(|map, selection| { selection.collapse_to( movement::end_of_paragraph(map, selection.head(), 1), @@ -12148,12 +13264,12 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); + self.change_selections(Default::default(), window, cx, |s| { s.move_heads_with(|map, head, _| { ( movement::start_of_paragraph(map, head, 1), @@ -12169,12 +13285,12 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); + self.change_selections(Default::default(), window, cx, |s| { s.move_heads_with(|map, head, _| { ( movement::end_of_paragraph(map, head, 1), @@ -12190,12 +13306,12 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); + self.change_selections(Default::default(), window, cx, |s| { s.move_with(|map, selection| { selection.collapse_to( movement::start_of_excerpt( @@ -12215,12 +13331,12 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.change_selections(Default::default(), window, cx, |s| { s.move_with(|map, selection| { selection.collapse_to( movement::start_of_excerpt( @@ -12240,12 +13356,12 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); + self.change_selections(Default::default(), window, cx, |s| { s.move_with(|map, selection| { selection.collapse_to( movement::end_of_excerpt( @@ -12265,12 +13381,12 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); + self.change_selections(Default::default(), window, cx, |s| { s.move_with(|map, selection| { selection.collapse_to( movement::end_of_excerpt( @@ -12290,12 +13406,12 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); + self.change_selections(Default::default(), window, cx, |s| { s.move_heads_with(|map, head, _| { ( movement::start_of_excerpt(map, head, workspace::searchable::Direction::Prev), @@ -12311,12 +13427,12 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); + self.change_selections(Default::default(), window, cx, |s| { s.move_heads_with(|map, head, _| { ( movement::start_of_excerpt(map, head, workspace::searchable::Direction::Next), @@ -12332,12 +13448,12 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); + self.change_selections(Default::default(), window, cx, |s| { s.move_heads_with(|map, head, _| { ( movement::end_of_excerpt(map, head, workspace::searchable::Direction::Next), @@ -12353,12 +13469,12 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); + self.change_selections(Default::default(), window, cx, |s| { s.move_heads_with(|map, head, _| { ( movement::end_of_excerpt(map, head, workspace::searchable::Direction::Prev), @@ -12374,12 +13490,12 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); + self.change_selections(Default::default(), window, cx, |s| { s.select_ranges(vec![0..0]); }); } @@ -12392,20 +13508,20 @@ impl Editor { ) { let mut selection = self.selections.last::(cx); selection.set_head(Point::zero(), SelectionGoal::None); - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); + self.change_selections(Default::default(), window, cx, |s| { s.select(vec![selection]); }); } pub fn move_to_end(&mut self, _: &MoveToEnd, window: &mut Window, cx: &mut Context) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let cursor = self.buffer.read(cx).read(cx).len(); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.change_selections(Default::default(), window, cx, |s| { s.select_ranges(vec![cursor..cursor]) }); } @@ -12419,7 +13535,13 @@ impl Editor { } pub fn create_nav_history_entry(&mut self, cx: &mut Context) { - self.push_to_nav_history(self.selections.newest_anchor().head(), None, false, cx); + self.push_to_nav_history( + self.selections.newest_anchor().head(), + None, + false, + true, + cx, + ); } fn push_to_nav_history( @@ -12427,6 +13549,7 @@ impl Editor { cursor_anchor: Anchor, new_position: Option, is_deactivate: bool, + always: bool, cx: &mut Context, ) { if let Some(nav_history) = self.nav_history.as_mut() { @@ -12438,7 +13561,7 @@ impl Editor { if let Some(new_position) = new_position { let row_delta = (new_position.row as i64 - cursor_position.row as i64).abs(); - if row_delta < MIN_NAVIGATION_HISTORY_ROW_DELTA { + if row_delta == 0 || (row_delta < MIN_NAVIGATION_HISTORY_ROW_DELTA && !always) { return; } } @@ -12460,25 +13583,25 @@ impl Editor { } pub fn select_to_end(&mut self, _: &SelectToEnd, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let buffer = self.buffer.read(cx).snapshot(cx); let mut selection = self.selections.first::(cx); selection.set_head(buffer.len(), SelectionGoal::None); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.change_selections(Default::default(), window, cx, |s| { s.select(vec![selection]); }); } pub fn select_all(&mut self, _: &SelectAll, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let end = self.buffer.read(cx).read(cx).len(); - self.change_selections(None, window, cx, |s| { + self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges(vec![0..end]); }); } pub fn select_line(&mut self, _: &SelectLine, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut selections = self.selections.all::(cx); let max_point = display_map.buffer_snapshot.max_point(); @@ -12488,14 +13611,14 @@ impl Editor { selection.end = cmp::min(max_point, Point::new(rows.end.0, 0)); selection.reversed = false; } - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.change_selections(Default::default(), window, cx, |s| { s.select(selections); }); } pub fn split_selection_into_lines( &mut self, - _: &SplitSelectionIntoLines, + action: &SplitSelectionIntoLines, window: &mut Window, cx: &mut Context, ) { @@ -12512,8 +13635,21 @@ impl Editor { let buffer = self.buffer.read(cx).read(cx); for selection in selections { for row in selection.start.row..selection.end.row { - let cursor = Point::new(row, buffer.line_len(MultiBufferRow(row))); - new_selection_ranges.push(cursor..cursor); + let line_start = Point::new(row, 0); + let line_end = Point::new(row, buffer.line_len(MultiBufferRow(row))); + + if action.keep_selections { + // Keep the selection range for each line + let selection_start = if row == selection.start.row { + selection.start + } else { + line_start + }; + new_selection_ranges.push(selection_start..line_end); + } else { + // Collapse to cursor at end of line + new_selection_ranges.push(line_end..line_end); + } } let is_multiline_selection = selection.start.row != selection.end.row; @@ -12521,11 +13657,20 @@ impl Editor { // so this action feels more ergonomic when paired with other selection operations let should_skip_last = is_multiline_selection && selection.end.column == 0; if !should_skip_last { - new_selection_ranges.push(selection.end..selection.end); + if action.keep_selections { + if is_multiline_selection { + let line_start = Point::new(selection.end.row, 0); + new_selection_ranges.push(line_start..selection.end); + } else { + new_selection_ranges.push(selection.start..selection.end); + } + } else { + new_selection_ranges.push(selection.end..selection.end); + } } } } - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.change_selections(Default::default(), window, cx, |s| { s.select_ranges(new_selection_ranges); }); } @@ -12549,52 +13694,77 @@ impl Editor { } fn add_selection(&mut self, above: bool, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.selections.all::(cx); + let all_selections = self.selections.all::(cx); let text_layout_details = self.text_layout_details(window); - let mut state = self.add_selections_state.take().unwrap_or_else(|| { - let oldest_selection = selections.iter().min_by_key(|s| s.id).unwrap().clone(); - let range = oldest_selection.display_range(&display_map).sorted(); + let (mut columnar_selections, new_selections_to_columnarize) = { + if let Some(state) = self.add_selections_state.as_ref() { + let columnar_selection_ids: HashSet<_> = state + .groups + .iter() + .flat_map(|group| group.stack.iter()) + .copied() + .collect(); + + all_selections + .into_iter() + .partition(|s| columnar_selection_ids.contains(&s.id)) + } else { + (Vec::new(), all_selections) + } + }; + + let mut state = self + .add_selections_state + .take() + .unwrap_or_else(|| AddSelectionsState { groups: Vec::new() }); + + for selection in new_selections_to_columnarize { + let range = selection.display_range(&display_map).sorted(); let start_x = display_map.x_for_display_point(range.start, &text_layout_details); let end_x = display_map.x_for_display_point(range.end, &text_layout_details); let positions = start_x.min(end_x)..start_x.max(end_x); - - selections.clear(); let mut stack = Vec::new(); for row in range.start.row().0..=range.end.row().0 { if let Some(selection) = self.selections.build_columnar_selection( &display_map, DisplayRow(row), &positions, - oldest_selection.reversed, + selection.reversed, &text_layout_details, ) { stack.push(selection.id); - selections.push(selection); + columnar_selections.push(selection); } } - - if above { - stack.reverse(); + if !stack.is_empty() { + if above { + stack.reverse(); + } + state.groups.push(AddSelectionsGroup { above, stack }); } + } - AddSelectionsState { above, stack } - }); + let mut final_selections = Vec::new(); + let end_row = if above { + DisplayRow(0) + } else { + display_map.max_point().row() + }; - let last_added_selection = *state.stack.last().unwrap(); - let mut new_selections = Vec::new(); - if above == state.above { - let end_row = if above { - DisplayRow(0) - } else { - display_map.max_point().row() - }; + let mut last_added_item_per_group = HashMap::default(); + for group in state.groups.iter_mut() { + if let Some(last_id) = group.stack.last() { + last_added_item_per_group.insert(*last_id, group); + } + } - 'outer: for selection in selections { - if selection.id == last_added_selection { + for selection in columnar_selections { + if let Some(group) = last_added_item_per_group.get_mut(&selection.id) { + if above == group.above { let range = selection.display_range(&display_map).sorted(); debug_assert_eq!(range.start.row(), range.end.row()); let mut row = range.start.row(); @@ -12609,13 +13779,13 @@ impl Editor { start_x.min(end_x)..start_x.max(end_x) }; + let mut maybe_new_selection = None; while row != end_row { if above { row.0 -= 1; } else { row.0 += 1; } - if let Some(new_selection) = self.selections.build_columnar_selection( &display_map, row, @@ -12623,32 +13793,50 @@ impl Editor { selection.reversed, &text_layout_details, ) { - state.stack.push(new_selection.id); - if above { - new_selections.push(new_selection); - new_selections.push(selection); - } else { - new_selections.push(selection); - new_selections.push(new_selection); - } - - continue 'outer; + maybe_new_selection = Some(new_selection); + break; } } - } - new_selections.push(selection); + if let Some(new_selection) = maybe_new_selection { + group.stack.push(new_selection.id); + if above { + final_selections.push(new_selection); + final_selections.push(selection); + } else { + final_selections.push(selection); + final_selections.push(new_selection); + } + } else { + final_selections.push(selection); + } + } else { + group.stack.pop(); + } + } else { + final_selections.push(selection); } - } else { - new_selections = selections; - new_selections.retain(|s| s.id != last_added_selection); - state.stack.pop(); } - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(new_selections); + self.change_selections(Default::default(), window, cx, |s| { + s.select(final_selections); }); - if state.stack.len() > 1 { + + let final_selection_ids: HashSet<_> = self + .selections + .all::(cx) + .iter() + .map(|s| s.id) + .collect(); + state.groups.retain_mut(|group| { + // selections might get merged above so we remove invalid items from stacks + group.stack.retain(|id| final_selection_ids.contains(id)); + + // single selection in stack can be treated as initial state + group.stack.len() > 1 + }); + + if !state.groups.is_empty() { self.add_selections_state = Some(state); } } @@ -12662,8 +13850,18 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.unfold_ranges(&[range.clone()], false, auto_scroll.is_some(), cx); - self.change_selections(auto_scroll, window, cx, |s| { + self.unfold_ranges( + std::slice::from_ref(&range), + false, + auto_scroll.is_some(), + cx, + ); + let effects = if let Some(scroll) = auto_scroll { + SelectionEffects::scroll(scroll) + } else { + SelectionEffects::no_scroll() + }; + self.change_selections(effects, window, cx, |s| { if replace_newest { s.delete(s.newest_anchor().id); } @@ -12708,12 +13906,10 @@ impl Editor { let query_match = query_match.unwrap(); // can only fail due to I/O let offset_range = start_offset + query_match.start()..start_offset + query_match.end(); - let display_range = offset_range.start.to_display_point(display_map) - ..offset_range.end.to_display_point(display_map); if !select_next_state.wordwise - || (!movement::is_inside_word(display_map, display_range.start) - && !movement::is_inside_word(display_map, display_range.end)) + || (!buffer.is_inside_word(offset_range.start, false) + && !buffer.is_inside_word(offset_range.end, false)) { // TODO: This is n^2, because we might check all the selections if !selections @@ -12777,12 +13973,9 @@ impl Editor { if only_carets { for selection in &mut selections { - let word_range = movement::surrounding_word( - display_map, - selection.start.to_display_point(display_map), - ); - selection.start = word_range.start.to_offset(display_map, Bias::Left); - selection.end = word_range.end.to_offset(display_map, Bias::Left); + let (word_range, _) = buffer.surrounding_word(selection.start, false); + selection.start = word_range.start; + selection.end = word_range.end; selection.goal = SelectionGoal::None; selection.reversed = false; self.select_match_ranges( @@ -12836,7 +14029,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) -> Result<()> { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); @@ -12863,20 +14056,24 @@ impl Editor { } else { query_match.start()..query_match.end() }; - let display_range = offset_range.start.to_display_point(&display_map) - ..offset_range.end.to_display_point(&display_map); if !select_next_state.wordwise - || (!movement::is_inside_word(&display_map, display_range.start) - && !movement::is_inside_word(&display_map, display_range.end)) + || (!buffer.is_inside_word(offset_range.start, false) + && !buffer.is_inside_word(offset_range.end, false)) { new_selections.push(offset_range.start..offset_range.end); } } select_next_state.done = true; + + if new_selections.is_empty() { + log::error!("bug: new_selections is empty in select_all_matches"); + return Ok(()); + } + self.unfold_ranges(&new_selections.clone(), false, false, cx); - self.change_selections(None, window, cx, |selections| { + self.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| { selections.select_ranges(new_selections) }); @@ -12889,7 +14086,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) -> Result<()> { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); self.select_next_match_internal( &display_map, @@ -12907,7 +14104,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) -> Result<()> { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = &display_map.buffer_snapshot; let mut selections = self.selections.all::(cx); @@ -12934,12 +14131,10 @@ impl Editor { let query_match = query_match.unwrap(); // can only fail due to I/O let offset_range = end_offset - query_match.end()..end_offset - query_match.start(); - let display_range = offset_range.start.to_display_point(&display_map) - ..offset_range.end.to_display_point(&display_map); if !select_prev_state.wordwise - || (!movement::is_inside_word(&display_map, display_range.start) - && !movement::is_inside_word(&display_map, display_range.end)) + || (!buffer.is_inside_word(offset_range.start, false) + && !buffer.is_inside_word(offset_range.end, false)) { next_selected_range = Some(offset_range); break; @@ -12997,12 +14192,9 @@ impl Editor { if only_carets { for selection in &mut selections { - let word_range = movement::surrounding_word( - &display_map, - selection.start.to_display_point(&display_map), - ); - selection.start = word_range.start.to_offset(&display_map, Bias::Left); - selection.end = word_range.end.to_offset(&display_map, Bias::Left); + let (word_range, _) = buffer.surrounding_word(selection.start, false); + selection.start = word_range.start; + selection.end = word_range.end; selection.goal = SelectionGoal::None; selection.reversed = false; self.select_match_ranges( @@ -13052,7 +14244,7 @@ impl Editor { let selections = self.selections.disjoint_anchors(); match selections.first() { Some(first) if selections.len() >= 2 => { - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.change_selections(Default::default(), window, cx, |s| { s.select_ranges([first.range()]); }); } @@ -13076,7 +14268,7 @@ impl Editor { let selections = self.selections.disjoint_anchors(); match selections.last() { Some(last) if selections.len() >= 2 => { - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.change_selections(Default::default(), window, cx, |s| { s.select_ranges([last.range()]); }); } @@ -13100,7 +14292,7 @@ impl Editor { if self.read_only(cx) { return; } - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let text_layout_details = &self.text_layout_details(window); self.transact(window, cx, |this, window, cx| { let mut selections = this.selections.all::(cx); @@ -13288,8 +14480,11 @@ impl Editor { (position..position, first_prefix.clone()) })); } - } else if let Some((full_comment_prefix, comment_suffix)) = - language.block_comment_delimiters() + } else if let Some(BlockCommentConfig { + start: full_comment_prefix, + end: comment_suffix, + .. + }) = language.block_comment() { let comment_prefix = full_comment_prefix.trim_end_matches(' '); let comment_prefix_whitespace = &full_comment_prefix[comment_prefix.len()..]; @@ -13355,9 +14550,7 @@ impl Editor { } drop(snapshot); - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(selections) - }); + this.change_selections(Default::default(), window, cx, |s| s.select(selections)); let selections = this.selections.all::(cx); let selections_on_single_row = selections.windows(2).all(|selections| { @@ -13371,12 +14564,12 @@ impl Editor { let advance_downwards = action.advance_downwards && selections_on_single_row && !selections_selecting - && !matches!(this.mode, EditorMode::SingleLine { .. }); + && !matches!(this.mode, EditorMode::SingleLine); if advance_downwards { let snapshot = this.buffer.read(cx).snapshot(cx); - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + this.change_selections(Default::default(), window, cx, |s| { s.move_cursors_with(|display_snapshot, display_point, _| { let mut point = display_point.to_point(display_snapshot); point.row += 1; @@ -13400,7 +14593,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let buffer = self.buffer.read(cx).snapshot(cx); let old_selections = self.selections.all::(cx).into_boxed_slice(); @@ -13443,7 +14636,7 @@ impl Editor { .collect::>(); if selected_larger_symbol { - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.change_selections(Default::default(), window, cx, |s| { s.select(new_selections); }); } @@ -13463,7 +14656,7 @@ impl Editor { return; } - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = self.buffer.read(cx).snapshot(cx); @@ -13477,26 +14670,11 @@ impl Editor { if let Some((node, _)) = buffer.syntax_ancestor(old_range.clone()) { // manually select word at selection if ["string_content", "inline"].contains(&node.kind()) { - let word_range = { - let display_point = buffer - .offset_to_point(old_range.start) - .to_display_point(&display_map); - let Range { start, end } = - movement::surrounding_word(&display_map, display_point); - start.to_point(&display_map).to_offset(&buffer) - ..end.to_point(&display_map).to_offset(&buffer) - }; + let (word_range, _) = buffer.surrounding_word(old_range.start, false); // ignore if word is already selected if !word_range.is_empty() && old_range != word_range { - let last_word_range = { - let display_point = buffer - .offset_to_point(old_range.end) - .to_display_point(&display_map); - let Range { start, end } = - movement::surrounding_word(&display_map, display_point); - start.to_point(&display_map).to_offset(&buffer) - ..end.to_point(&display_map).to_offset(&buffer) - }; + let (last_word_range, _) = + buffer.surrounding_word(old_range.end, false); // only select word if start and end point belongs to same word if word_range == last_word_range { selected_larger_node = true; @@ -13558,7 +14736,7 @@ impl Editor { if selected_larger_node { self.select_syntax_node_history.disable_clearing = true; - self.change_selections(None, window, cx, |s| { + self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select(new_selections.clone()); }); self.select_syntax_node_history.disable_clearing = false; @@ -13594,7 +14772,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); if let Some((mut selections, scroll_behavior, is_selection_reversed)) = self.select_syntax_node_history.pop() @@ -13604,7 +14782,7 @@ impl Editor { } self.select_syntax_node_history.disable_clearing = true; - self.change_selections(None, window, cx, |s| { + self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select(selections.to_vec()); }); self.select_syntax_node_history.disable_clearing = false; @@ -13623,12 +14801,94 @@ impl Editor { } } + pub fn unwrap_syntax_node( + &mut self, + _: &UnwrapSyntaxNode, + window: &mut Window, + cx: &mut Context, + ) { + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); + + let buffer = self.buffer.read(cx).snapshot(cx); + let selections = self + .selections + .all::(cx) + .into_iter() + // subtracting the offset requires sorting + .sorted_by_key(|i| i.start); + + let full_edits = selections + .into_iter() + .filter_map(|selection| { + // Only requires two branches once if-let-chains stabilize (#53667) + let child = if !selection.is_empty() { + selection.range() + } else if let Some((_, ancestor_range)) = + buffer.syntax_ancestor(selection.start..selection.end) + { + match ancestor_range { + MultiOrSingleBufferOffsetRange::Single(range) => range, + MultiOrSingleBufferOffsetRange::Multi(range) => range, + } + } else { + selection.range() + }; + + let mut parent = child.clone(); + while let Some((_, ancestor_range)) = buffer.syntax_ancestor(parent.clone()) { + parent = match ancestor_range { + MultiOrSingleBufferOffsetRange::Single(range) => range, + MultiOrSingleBufferOffsetRange::Multi(range) => range, + }; + if parent.start < child.start || parent.end > child.end { + break; + } + } + + if parent == child { + return None; + } + let text = buffer.text_for_range(child).collect::(); + Some((selection.id, parent, text)) + }) + .collect::>(); + + self.transact(window, cx, |this, window, cx| { + this.buffer.update(cx, |buffer, cx| { + buffer.edit( + full_edits + .iter() + .map(|(_, p, t)| (p.clone(), t.clone())) + .collect::>(), + None, + cx, + ); + }); + this.change_selections(Default::default(), window, cx, |s| { + let mut offset = 0; + let mut selections = vec![]; + for (id, parent, text) in full_edits { + let start = parent.start - offset; + offset += parent.len() - text.len(); + selections.push(Selection { + id, + start, + end: start + text.len(), + reversed: false, + goal: Default::default(), + }); + } + s.select(selections); + }); + }); + } + fn refresh_runnables(&mut self, window: &mut Window, cx: &mut Context) -> Task<()> { if !EditorSettings::get_global(cx).gutter.runnables { self.clear_tasks(); return Task::ready(()); } - let project = self.project.as_ref().map(Entity::downgrade); + let project = self.project().map(Entity::downgrade); let task_sources = self.lsp_task_sources(cx); let multi_buffer = self.buffer.downgrade(); cx.spawn_in(window, async move |editor, cx| { @@ -13643,10 +14903,7 @@ impl Editor { }; let hide_runnables = project - .update(cx, |project, cx| { - // Do not display any test indicators in non-dev server remote projects. - project.is_via_collab() && project.ssh_connection_string(cx).is_none() - }) + .update(cx, |project, _| project.is_via_collab()) .unwrap_or(true); if hide_runnables { return; @@ -13726,7 +14983,8 @@ impl Editor { prefer_lsp && !lsp_tasks_by_rows.is_empty(), new_rows, cx.clone(), - ); + ) + .await; editor .update(cx, |editor, _| { editor.clear_tasks(); @@ -13756,35 +15014,40 @@ impl Editor { snapshot: DisplaySnapshot, prefer_lsp: bool, runnable_ranges: Vec, - mut cx: AsyncWindowContext, - ) -> Vec<((BufferId, BufferRow), RunnableTasks)> { - runnable_ranges - .into_iter() - .filter_map(|mut runnable| { - let mut tasks = cx + cx: AsyncWindowContext, + ) -> Task> { + cx.spawn(async move |cx| { + let mut runnable_rows = Vec::with_capacity(runnable_ranges.len()); + for mut runnable in runnable_ranges { + let Some(tasks) = cx .update(|_, cx| Self::templates_with_tags(&project, &mut runnable.runnable, cx)) - .ok()?; + .ok() + else { + continue; + }; + let mut tasks = tasks.await; + if prefer_lsp { tasks.retain(|(task_kind, _)| { !matches!(task_kind, TaskSourceKind::Language { .. }) }); } if tasks.is_empty() { - return None; + continue; } let point = runnable.run_range.start.to_point(&snapshot.buffer_snapshot); - - let row = snapshot + let Some(row) = snapshot .buffer_snapshot - .buffer_line_for_row(MultiBufferRow(point.row))? - .1 - .start - .row; + .buffer_line_for_row(MultiBufferRow(point.row)) + .map(|(_, range)| range.start.row) + else { + continue; + }; let context_range = BufferOffset(runnable.full_range.start)..BufferOffset(runnable.full_range.end); - Some(( + runnable_rows.push(( (runnable.buffer_id, row), RunnableTasks { templates: tasks, @@ -13795,16 +15058,17 @@ impl Editor { column: point.column, extra_variables: runnable.extra_captures, }, - )) - }) - .collect() + )); + } + runnable_rows + }) } fn templates_with_tags( project: &Entity, runnable: &mut Runnable, cx: &mut App, - ) -> Vec<(TaskSourceKind, TaskTemplate)> { + ) -> Task> { let (inventory, worktree_id, file) = project.read_with(cx, |project, cx| { let (worktree_id, file) = project .buffer_for_id(runnable.buffer, cx) @@ -13819,39 +15083,40 @@ impl Editor { ) }); - let mut templates_with_tags = mem::take(&mut runnable.tags) - .into_iter() - .flat_map(|RunnableTag(tag)| { - inventory - .as_ref() - .into_iter() - .flat_map(|inventory| { - inventory.read(cx).list_tasks( - file.clone(), - Some(runnable.language.clone()), - worktree_id, - cx, - ) - }) - .filter(move |(_, template)| { - template.tags.iter().any(|source_tag| source_tag == &tag) - }) - }) - .sorted_by_key(|(kind, _)| kind.to_owned()) - .collect::>(); - if let Some((leading_tag_source, _)) = templates_with_tags.first() { - // Strongest source wins; if we have worktree tag binding, prefer that to - // global and language bindings; - // if we have a global binding, prefer that to language binding. - let first_mismatch = templates_with_tags - .iter() - .position(|(tag_source, _)| tag_source != leading_tag_source); - if let Some(index) = first_mismatch { - templates_with_tags.truncate(index); + let tags = mem::take(&mut runnable.tags); + let language = runnable.language.clone(); + cx.spawn(async move |cx| { + let mut templates_with_tags = Vec::new(); + if let Some(inventory) = inventory { + for RunnableTag(tag) in tags { + let Ok(new_tasks) = inventory.update(cx, |inventory, cx| { + inventory.list_tasks(file.clone(), Some(language.clone()), worktree_id, cx) + }) else { + return templates_with_tags; + }; + templates_with_tags.extend(new_tasks.await.into_iter().filter( + move |(_, template)| { + template.tags.iter().any(|source_tag| source_tag == &tag) + }, + )); + } } - } + templates_with_tags.sort_by_key(|(kind, _)| kind.to_owned()); - templates_with_tags + if let Some((leading_tag_source, _)) = templates_with_tags.first() { + // Strongest source wins; if we have worktree tag binding, prefer that to + // global and language bindings; + // if we have a global binding, prefer that to language binding. + let first_mismatch = templates_with_tags + .iter() + .position(|(tag_source, _)| tag_source != leading_tag_source); + if let Some(index) = first_mismatch { + templates_with_tags.truncate(index); + } + } + + templates_with_tags + }) } pub fn move_to_enclosing_bracket( @@ -13860,8 +15125,8 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); + self.change_selections(Default::default(), window, cx, |s| { s.move_offsets_with(|snapshot, selection| { let Some(enclosing_bracket_ranges) = snapshot.enclosing_bracket_ranges(selection.start..selection.end) @@ -13917,19 +15182,24 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.end_selection(window, cx); - self.selection_history.mode = SelectionHistoryMode::Undoing; + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); if let Some(entry) = self.selection_history.undo_stack.pop_back() { - self.change_selections(None, window, cx, |s| { - s.select_anchors(entry.selections.to_vec()) + self.selection_history.mode = SelectionHistoryMode::Undoing; + self.with_selection_effects_deferred(window, cx, |this, window, cx| { + this.end_selection(window, cx); + this.change_selections( + SelectionEffects::scroll(Autoscroll::newest()), + window, + cx, + |s| s.select_anchors(entry.selections.to_vec()), + ); }); + self.selection_history.mode = SelectionHistoryMode::Normal; + self.select_next_state = entry.select_next_state; self.select_prev_state = entry.select_prev_state; self.add_selections_state = entry.add_selections_state; - self.request_autoscroll(Autoscroll::newest(), cx); } - self.selection_history.mode = SelectionHistoryMode::Normal; } pub fn redo_selection( @@ -13938,19 +15208,24 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.end_selection(window, cx); - self.selection_history.mode = SelectionHistoryMode::Redoing; + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); if let Some(entry) = self.selection_history.redo_stack.pop_back() { - self.change_selections(None, window, cx, |s| { - s.select_anchors(entry.selections.to_vec()) + self.selection_history.mode = SelectionHistoryMode::Redoing; + self.with_selection_effects_deferred(window, cx, |this, window, cx| { + this.end_selection(window, cx); + this.change_selections( + SelectionEffects::scroll(Autoscroll::newest()), + window, + cx, + |s| s.select_anchors(entry.selections.to_vec()), + ); }); + self.selection_history.mode = SelectionHistoryMode::Normal; + self.select_next_state = entry.select_next_state; self.select_prev_state = entry.select_prev_state; self.add_selections_state = entry.add_selections_state; - self.request_autoscroll(Autoscroll::newest(), cx); } - self.selection_history.mode = SelectionHistoryMode::Normal; } pub fn expand_excerpts( @@ -14021,17 +15296,15 @@ impl Editor { if direction == ExpandExcerptDirection::Down { let multi_buffer = self.buffer.read(cx); let snapshot = multi_buffer.snapshot(cx); - if let Some(buffer_id) = snapshot.buffer_id_for_excerpt(excerpt) { - if let Some(buffer) = multi_buffer.buffer(buffer_id) { - if let Some(excerpt_range) = snapshot.buffer_range_for_excerpt(excerpt) { - let buffer_snapshot = buffer.read(cx).snapshot(); - let excerpt_end_row = - Point::from_anchor(&excerpt_range.end, &buffer_snapshot).row; - let last_row = buffer_snapshot.max_point().row; - let lines_below = last_row.saturating_sub(excerpt_end_row); - should_scroll_up = lines_below >= lines_to_expand; - } - } + if let Some(buffer_id) = snapshot.buffer_id_for_excerpt(excerpt) + && let Some(buffer) = multi_buffer.buffer(buffer_id) + && let Some(excerpt_range) = snapshot.buffer_range_for_excerpt(excerpt) + { + let buffer_snapshot = buffer.read(cx).snapshot(); + let excerpt_end_row = Point::from_anchor(&excerpt_range.end, &buffer_snapshot).row; + let last_row = buffer_snapshot.max_point().row; + let lines_below = last_row.saturating_sub(excerpt_end_row); + should_scroll_up = lines_below >= lines_to_expand; } } @@ -14071,34 +15344,44 @@ impl Editor { let Some(end) = multibuffer.buffer_point_to_anchor(&buffer, range.end, cx) else { return; }; - self.change_selections(Some(Autoscroll::center()), window, cx, |s| { - s.select_anchor_ranges([start..end]) - }); + self.change_selections( + SelectionEffects::default().nav_history(true), + window, + cx, + |s| s.select_anchor_ranges([start..end]), + ); } pub fn go_to_diagnostic( &mut self, - _: &GoToDiagnostic, + action: &GoToDiagnostic, window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.go_to_diagnostic_impl(Direction::Next, window, cx) + if !self.diagnostics_enabled() { + return; + } + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); + self.go_to_diagnostic_impl(Direction::Next, action.severity, window, cx) } pub fn go_to_prev_diagnostic( &mut self, - _: &GoToPreviousDiagnostic, + action: &GoToPreviousDiagnostic, window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.go_to_diagnostic_impl(Direction::Prev, window, cx) + if !self.diagnostics_enabled() { + return; + } + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); + self.go_to_diagnostic_impl(Direction::Prev, action.severity, window, cx) } pub fn go_to_diagnostic_impl( &mut self, direction: Direction, + severity: GoToDiagnosticSeverityFilter, window: &mut Window, cx: &mut Context, ) { @@ -14106,17 +15389,19 @@ impl Editor { let selection = self.selections.newest::(cx); let mut active_group_id = None; - if let ActiveDiagnostic::Group(active_group) = &self.active_diagnostics { - if active_group.active_range.start.to_offset(&buffer) == selection.start { - active_group_id = Some(active_group.group_id); - } + if let ActiveDiagnostic::Group(active_group) = &self.active_diagnostics + && active_group.active_range.start.to_offset(&buffer) == selection.start + { + active_group_id = Some(active_group.group_id); } fn filtered( snapshot: EditorSnapshot, + severity: GoToDiagnosticSeverityFilter, diagnostics: impl Iterator>, ) -> impl Iterator> { diagnostics + .filter(move |entry| severity.matches(entry.diagnostic.severity)) .filter(|entry| entry.range.start != entry.range.end) .filter(|entry| !entry.diagnostic.is_unnecessary) .filter(move |entry| !snapshot.intersects_fold(entry.range.start)) @@ -14125,12 +15410,14 @@ impl Editor { let snapshot = self.snapshot(window, cx); let before = filtered( snapshot.clone(), + severity, buffer .diagnostics_in_range(0..selection.start) .filter(|entry| entry.range.start <= selection.start), ); let after = filtered( snapshot, + severity, buffer .diagnostics_in_range(selection.start..buffer.len()) .filter(|entry| entry.range.start >= selection.start), @@ -14164,20 +15451,21 @@ impl Editor { return; }; - let Some(buffer_id) = buffer.anchor_after(next_diagnostic.range.start).buffer_id else { + let next_diagnostic_start = buffer.anchor_after(next_diagnostic.range.start); + let Some(buffer_id) = buffer.buffer_id_for_anchor(next_diagnostic_start) else { return; }; - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.change_selections(Default::default(), window, cx, |s| { s.select_ranges(vec![ next_diagnostic.range.start..next_diagnostic.range.start, ]) }); self.activate_diagnostics(buffer_id, next_diagnostic, window, cx); - self.refresh_inline_completion(false, true, window, cx); + self.refresh_edit_prediction(false, true, window, cx); } pub fn go_to_next_hunk(&mut self, _: &GoToHunk, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let snapshot = self.snapshot(window, cx); let selection = self.selections.newest::(cx); self.go_to_hunk_before_or_after_position( @@ -14209,7 +15497,7 @@ impl Editor { let autoscroll = Autoscroll::center(); self.unfold_ranges(&[destination..destination], false, false, cx); - self.change_selections(Some(autoscroll), window, cx, |s| { + self.change_selections(SelectionEffects::scroll(autoscroll), window, cx, |s| { s.select_ranges([destination..destination]); }); } @@ -14238,7 +15526,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let snapshot = self.snapshot(window, cx); let selection = self.selections.newest::(cx); self.go_to_hunk_before_or_after_position( @@ -14272,7 +15560,7 @@ impl Editor { .next_change(1, Direction::Next) .map(|s| s.to_vec()) { - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.change_selections(Default::default(), window, cx, |s| { let map = s.display_map(); s.select_display_ranges(selections.iter().map(|a| { let point = a.to_display_point(&map); @@ -14293,7 +15581,7 @@ impl Editor { .next_change(1, Direction::Prev) .map(|s| s.to_vec()) { - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.change_selections(Default::default(), window, cx, |s| { let map = s.display_map(); s.select_display_ranges(selections.iter().map(|a| { let point = a.to_display_point(&map); @@ -14434,18 +15722,17 @@ impl Editor { }; let head = self.selections.newest::(cx).head(); let buffer = self.buffer.read(cx); - let (buffer, head) = if let Some(text_anchor) = buffer.text_anchor_for_position(head, cx) { - text_anchor - } else { + let Some((buffer, head)) = buffer.text_anchor_for_position(head, cx) else { return Task::ready(Ok(Navigated::No)); }; - let Some(definitions) = provider.definitions(&buffer, head, kind, cx) else { return Task::ready(Ok(Navigated::No)); }; cx.spawn_in(window, async move |editor, cx| { - let definitions = definitions.await?; + let Some(definitions) = definitions.await? else { + return Ok(Navigated::No); + }; let navigated = editor .update_in(cx, |editor, window, cx| { editor.navigate_to_hover_links( @@ -14544,62 +15831,120 @@ impl Editor { pub(crate) fn navigate_to_hover_links( &mut self, kind: Option, - mut definitions: Vec, + definitions: Vec, split: bool, window: &mut Window, cx: &mut Context, ) -> Task> { - // If there is one definition, just open it directly - if definitions.len() == 1 { - let definition = definitions.pop().unwrap(); - - enum TargetTaskResult { - Location(Option), - AlreadyNavigated, - } - - let target_task = match definition { - HoverLink::Text(link) => { - Task::ready(anyhow::Ok(TargetTaskResult::Location(Some(link.target)))) - } + // Separate out url and file links, we can only handle one of them at most or an arbitrary number of locations + let mut first_url_or_file = None; + let definitions: Vec<_> = definitions + .into_iter() + .filter_map(|def| match def { + HoverLink::Text(link) => Some(Task::ready(anyhow::Ok(Some(link.target)))), HoverLink::InlayHint(lsp_location, server_id) => { let computation = self.compute_target_location(lsp_location, server_id, window, cx); - cx.background_spawn(async move { - let location = computation.await?; - Ok(TargetTaskResult::Location(location)) - }) + Some(cx.background_spawn(computation)) } HoverLink::Url(url) => { - cx.open_url(&url); - Task::ready(Ok(TargetTaskResult::AlreadyNavigated)) + first_url_or_file = Some(Either::Left(url)); + None } HoverLink::File(path) => { - if let Some(workspace) = self.workspace() { - cx.spawn_in(window, async move |_, cx| { - workspace - .update_in(cx, |workspace, window, cx| { - workspace.open_resolved_path(path, window, cx) - })? - .await - .map(|_| TargetTaskResult::AlreadyNavigated) - }) - } else { - Task::ready(Ok(TargetTaskResult::Location(None))) - } + first_url_or_file = Some(Either::Right(path)); + None } - }; - cx.spawn_in(window, async move |editor, cx| { - let target = match target_task.await.context("target resolution task")? { - TargetTaskResult::AlreadyNavigated => return Ok(Navigated::Yes), - TargetTaskResult::Location(None) => return Ok(Navigated::No), - TargetTaskResult::Location(Some(target)) => target, + }) + .collect(); + + let workspace = self.workspace(); + + cx.spawn_in(window, async move |editor, acx| { + let mut locations: Vec = future::join_all(definitions) + .await + .into_iter() + .filter_map(|location| location.transpose()) + .collect::>() + .context("location tasks")?; + + if locations.len() > 1 { + let Some(workspace) = workspace else { + return Ok(Navigated::No); }; - editor.update_in(cx, |editor, window, cx| { - let Some(workspace) = editor.workspace() else { - return Navigated::No; - }; + let tab_kind = match kind { + Some(GotoDefinitionKind::Implementation) => "Implementations", + Some(GotoDefinitionKind::Symbol) | None => "Definitions", + Some(GotoDefinitionKind::Declaration) => "Declarations", + Some(GotoDefinitionKind::Type) => "Types", + }; + let title = editor + .update_in(acx, |_, _, cx| { + let target = locations + .iter() + .map(|location| { + location + .buffer + .read(cx) + .text_for_range(location.range.clone()) + .collect::() + }) + .filter(|text| !text.contains('\n')) + .unique() + .take(3) + .join(", "); + if target.is_empty() { + tab_kind.to_owned() + } else { + format!("{tab_kind} for {target}") + } + }) + .context("buffer title")?; + + let opened = workspace + .update_in(acx, |workspace, window, cx| { + Self::open_locations_in_multibuffer( + workspace, + locations, + title, + split, + MultibufferSelectionMode::First, + window, + cx, + ) + }) + .is_ok(); + + anyhow::Ok(Navigated::from_bool(opened)) + } else if locations.is_empty() { + // If there is one definition, just open it directly + match first_url_or_file { + Some(Either::Left(url)) => { + acx.update(|_, cx| cx.open_url(&url))?; + Ok(Navigated::Yes) + } + Some(Either::Right(path)) => { + let Some(workspace) = workspace else { + return Ok(Navigated::No); + }; + + workspace + .update_in(acx, |workspace, window, cx| { + workspace.open_resolved_path(path, window, cx) + })? + .await?; + Ok(Navigated::Yes) + } + None => Ok(Navigated::No), + } + } else { + let Some(workspace) = workspace else { + return Ok(Navigated::No); + }; + + let target = locations.pop().unwrap(); + editor.update_in(acx, |editor, window, cx| { let pane = workspace.read(cx).active_pane().clone(); let range = target.range.to_point(target.buffer.read(cx)); @@ -14609,7 +15954,7 @@ impl Editor { if !split && Some(&target.buffer) == editor.buffer.read(cx).as_singleton().as_ref() { - editor.go_to_singleton_buffer_range(range.clone(), window, cx); + editor.go_to_singleton_buffer_range(range, window, cx); } else { window.defer(cx, move |window, cx| { let target_editor: Entity = @@ -14640,76 +15985,8 @@ impl Editor { } Navigated::Yes }) - }) - } else if !definitions.is_empty() { - cx.spawn_in(window, async move |editor, cx| { - let (title, location_tasks, workspace) = editor - .update_in(cx, |editor, window, cx| { - let tab_kind = match kind { - Some(GotoDefinitionKind::Implementation) => "Implementations", - _ => "Definitions", - }; - let title = definitions - .iter() - .find_map(|definition| match definition { - HoverLink::Text(link) => link.origin.as_ref().map(|origin| { - let buffer = origin.buffer.read(cx); - format!( - "{} for {}", - tab_kind, - buffer - .text_for_range(origin.range.clone()) - .collect::() - ) - }), - HoverLink::InlayHint(_, _) => None, - HoverLink::Url(_) => None, - HoverLink::File(_) => None, - }) - .unwrap_or(tab_kind.to_string()); - let location_tasks = definitions - .into_iter() - .map(|definition| match definition { - HoverLink::Text(link) => Task::ready(Ok(Some(link.target))), - HoverLink::InlayHint(lsp_location, server_id) => editor - .compute_target_location(lsp_location, server_id, window, cx), - HoverLink::Url(_) => Task::ready(Ok(None)), - HoverLink::File(_) => Task::ready(Ok(None)), - }) - .collect::>(); - (title, location_tasks, editor.workspace().clone()) - }) - .context("location tasks preparation")?; - - let locations = future::join_all(location_tasks) - .await - .into_iter() - .filter_map(|location| location.transpose()) - .collect::>() - .context("location tasks")?; - - let Some(workspace) = workspace else { - return Ok(Navigated::No); - }; - let opened = workspace - .update_in(cx, |workspace, window, cx| { - Self::open_locations_in_multibuffer( - workspace, - locations, - title, - split, - MultibufferSelectionMode::First, - window, - cx, - ) - }) - .ok(); - - anyhow::Ok(Navigated::from_bool(opened.is_some())) - }) - } else { - Task::ready(Ok(Navigated::No)) - } + } + }) } fn compute_target_location( @@ -14726,38 +16003,24 @@ impl Editor { cx.spawn_in(window, async move |editor, cx| { let location_task = editor.update(cx, |_, cx| { project.update(cx, |project, cx| { - let language_server_name = project - .language_server_statuses(cx) - .find(|(id, _)| server_id == *id) - .map(|(_, status)| LanguageServerName::from(status.name.as_str())); - language_server_name.map(|language_server_name| { - project.open_local_buffer_via_lsp( - lsp_location.uri.clone(), - server_id, - language_server_name, - cx, - ) - }) + project.open_local_buffer_via_lsp(lsp_location.uri.clone(), server_id, cx) }) })?; - let location = match location_task { - Some(task) => Some({ - let target_buffer_handle = task.await.context("open local buffer")?; - let range = target_buffer_handle.read_with(cx, |target_buffer, _| { - let target_start = target_buffer - .clip_point_utf16(point_from_lsp(lsp_location.range.start), Bias::Left); - let target_end = target_buffer - .clip_point_utf16(point_from_lsp(lsp_location.range.end), Bias::Left); - target_buffer.anchor_after(target_start) - ..target_buffer.anchor_before(target_end) - })?; - Location { - buffer: target_buffer_handle, - range, - } - }), - None => None, - }; + let location = Some({ + let target_buffer_handle = location_task.await.context("open local buffer")?; + let range = target_buffer_handle.read_with(cx, |target_buffer, _| { + let target_start = target_buffer + .clip_point_utf16(point_from_lsp(lsp_location.range.start), Bias::Left); + let target_end = target_buffer + .clip_point_utf16(point_from_lsp(lsp_location.range.end), Bias::Left); + target_buffer.anchor_after(target_start) + ..target_buffer.anchor_before(target_end) + })?; + Location { + buffer: target_buffer_handle, + range, + } + }); Ok(location) }) } @@ -14811,25 +16074,32 @@ impl Editor { } }); - let locations = references.await?; + let Some(locations) = references.await? else { + return anyhow::Ok(Navigated::No); + }; if locations.is_empty() { return anyhow::Ok(Navigated::No); } workspace.update_in(cx, |workspace, window, cx| { - let title = locations - .first() - .as_ref() + let target = locations + .iter() .map(|location| { - let buffer = location.buffer.read(cx); - format!( - "References to `{}`", - buffer - .text_for_range(location.range.clone()) - .collect::() - ) + location + .buffer + .read(cx) + .text_for_range(location.range.clone()) + .collect::() }) - .unwrap(); + .filter(|text| !text.contains('\n')) + .unique() + .take(3) + .join(", "); + let title = if target.is_empty() { + "References".to_owned() + } else { + format!("References to {target}") + }; Self::open_locations_in_multibuffer( workspace, locations, @@ -14854,6 +16124,11 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { + if locations.is_empty() { + log::error!("bug: open_locations_in_multibuffer called with empty list of locations"); + return; + } + // If there are multiple definitions, open them in a multibuffer locations.sort_by_key(|location| location.buffer.read(cx).remote_id()); let mut locations = locations.into_iter().peekable(); @@ -14903,22 +16178,33 @@ impl Editor { match multibuffer_selection_mode { MultibufferSelectionMode::First => { if let Some(first_range) = ranges.first() { - editor.change_selections(None, window, cx, |selections| { - selections.clear_disjoint(); - selections.select_anchor_ranges(std::iter::once(first_range.clone())); - }); + editor.change_selections( + SelectionEffects::no_scroll(), + window, + cx, + |selections| { + selections.clear_disjoint(); + selections + .select_anchor_ranges(std::iter::once(first_range.clone())); + }, + ); } editor.highlight_background::( &ranges, - |theme| theme.editor_highlighted_line_background, + |theme| theme.colors().editor_highlighted_line_background, cx, ); } MultibufferSelectionMode::All => { - editor.change_selections(None, window, cx, |selections| { - selections.clear_disjoint(); - selections.select_anchor_ranges(ranges); - }); + editor.change_selections( + SelectionEffects::no_scroll(), + window, + cx, + |selections| { + selections.clear_disjoint(); + selections.select_anchor_ranges(ranges); + }, + ); } } editor.register_buffers_with_language_servers(cx); @@ -14928,24 +16214,22 @@ impl Editor { let item_id = item.item_id(); if split { - workspace.split_item(SplitDirection::Right, item.clone(), window, cx); - } else { - if PreviewTabsSettings::get_global(cx).enable_preview_from_code_navigation { - let (preview_item_id, preview_item_idx) = - workspace.active_pane().read_with(cx, |pane, _| { - (pane.preview_item_id(), pane.preview_item_idx()) - }); + workspace.split_item(SplitDirection::Right, item, window, cx); + } else if PreviewTabsSettings::get_global(cx).enable_preview_from_code_navigation { + let (preview_item_id, preview_item_idx) = + workspace.active_pane().read_with(cx, |pane, _| { + (pane.preview_item_id(), pane.preview_item_idx()) + }); - workspace.add_item_to_active_pane(item.clone(), preview_item_idx, true, window, cx); + workspace.add_item_to_active_pane(item, preview_item_idx, true, window, cx); - if let Some(preview_item_id) = preview_item_id { - workspace.active_pane().update(cx, |pane, cx| { - pane.remove_item(preview_item_id, false, false, window, cx); - }); - } - } else { - workspace.add_item_to_active_pane(item.clone(), None, true, window, cx); + if let Some(preview_item_id) = preview_item_id { + workspace.active_pane().update(cx, |pane, cx| { + pane.remove_item(preview_item_id, false, false, window, cx); + }); } + } else { + workspace.add_item_to_active_pane(item, None, true, window, cx); } workspace.active_pane().update(cx, |pane, cx| { pane.set_preview_item_id(Some(item_id), cx); @@ -15052,7 +16336,7 @@ impl Editor { if rename_selection_range.end > old_name.len() { editor.select_all(&SelectAll, window, cx); } else { - editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + editor.change_selections(Default::default(), window, cx, |s| { s.select_ranges([rename_selection_range]); }); } @@ -15116,7 +16400,7 @@ impl Editor { font_weight: Some(FontWeight::BOLD), ..make_inlay_hints_style(cx.app) }, - inline_completion_styles: make_suggestion_styles( + edit_prediction_styles: make_suggestion_styles( cx.app, ), ..EditorStyle::default() @@ -15126,7 +16410,6 @@ impl Editor { } }), priority: 0, - render_in_minimap: true, }], Some(Autoscroll::fit()), cx, @@ -15225,7 +16508,7 @@ impl Editor { .min(rename_range.end); drop(snapshot); - self.change_selections(None, window, cx, |s| { + self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges(vec![cursor_in_editor..cursor_in_editor]) }); } else { @@ -15245,7 +16528,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) -> Option>> { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let project = match &self.project { Some(project) => project.clone(), @@ -15255,7 +16538,7 @@ impl Editor { Some(self.perform_format( project, FormatTrigger::Manual, - FormatTarget::Buffers, + FormatTarget::Buffers(self.buffer.read(cx).all_buffers()), window, cx, )) @@ -15267,7 +16550,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) -> Option>> { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let project = match &self.project { Some(project) => project.clone(), @@ -15300,13 +16583,7 @@ impl Editor { ) -> Task> { let buffer = self.buffer.clone(); let (buffers, target) = match target { - FormatTarget::Buffers => { - let mut buffers = buffer.read(cx).all_buffers(); - if trigger == FormatTrigger::Save { - buffers.retain(|buffer| buffer.read(cx).is_dirty()); - } - (buffers, LspFormatTarget::Buffers) - } + FormatTarget::Buffers(buffers) => (buffers, LspFormatTarget::Buffers), FormatTarget::Ranges(selection_ranges) => { let multi_buffer = buffer.read(cx); let snapshot = multi_buffer.read(cx); @@ -15342,10 +16619,7 @@ impl Editor { .transaction(transaction_id_prev) .map(|t| t.0.clone()) }) - .unwrap_or_else(|| { - log::info!("Failed to determine selections from before format. Falling back to selections when format was initiated"); - self.selections.disjoint_anchors() - }); + .unwrap_or_else(|| self.selections.disjoint_anchors()); let mut timeout = cx.background_executor().timer(FORMAT_TIMEOUT).fuse(); let format = project.update(cx, |project, cx| { @@ -15363,10 +16637,10 @@ impl Editor { buffer .update(cx, |buffer, cx| { - if let Some(transaction) = transaction { - if !buffer.is_singleton() { - buffer.push_transaction(&transaction.0, cx); - } + if let Some(transaction) = transaction + && !buffer.is_singleton() + { + buffer.push_transaction(&transaction.0, cx); } cx.notify(); }) @@ -15395,7 +16669,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) -> Option>> { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let project = match &self.project { Some(project) => project.clone(), None => return None, @@ -15432,10 +16706,10 @@ impl Editor { buffer .update(cx, |buffer, cx| { // check if we need this - if let Some(transaction) = transaction { - if !buffer.is_singleton() { - buffer.push_transaction(&transaction.0, cx); - } + if let Some(transaction) = transaction + && !buffer.is_singleton() + { + buffer.push_transaction(&transaction.0, cx); } cx.notify(); }) @@ -15444,7 +16718,7 @@ impl Editor { }) } - fn restart_language_server( + pub fn restart_language_server( &mut self, _: &RestartLanguageServer, _: &mut Window, @@ -15455,6 +16729,7 @@ impl Editor { project.update(cx, |project, cx| { project.restart_language_servers_for_buffers( multi_buffer.all_buffers().into_iter().collect(), + HashSet::default(), cx, ); }); @@ -15462,7 +16737,7 @@ impl Editor { } } - fn stop_language_server( + pub fn stop_language_server( &mut self, _: &StopLanguageServer, _: &mut Window, @@ -15473,6 +16748,7 @@ impl Editor { project.update(cx, |project, cx| { project.stop_language_servers_for_buffers( multi_buffer.all_buffers().into_iter().collect(), + HashSet::default(), cx, ); cx.emit(project::Event::RefreshInlayHints); @@ -15509,7 +16785,7 @@ impl Editor { } fn refresh_active_diagnostics(&mut self, cx: &mut Context) { - if self.mode.is_minimap() { + if !self.diagnostics_enabled() { return; } @@ -15540,6 +16816,9 @@ impl Editor { } pub fn set_all_diagnostics_active(&mut self, cx: &mut Context) { + if !self.diagnostics_enabled() { + return; + } self.dismiss_diagnostics(cx); self.active_diagnostics = ActiveDiagnostic::All; } @@ -15551,7 +16830,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if matches!(self.active_diagnostics, ActiveDiagnostic::All) { + if !self.diagnostics_enabled() || matches!(self.active_diagnostics, ActiveDiagnostic::All) { return; } self.dismiss_diagnostics(cx); @@ -15602,12 +16881,19 @@ impl Editor { self.inline_diagnostics.clear(); } + pub fn disable_diagnostics(&mut self, cx: &mut Context) { + self.diagnostics_enabled = false; + self.dismiss_diagnostics(cx); + self.inline_diagnostics_update = Task::ready(()); + self.inline_diagnostics.clear(); + } + pub fn diagnostics_enabled(&self) -> bool { - self.mode.is_full() + self.diagnostics_enabled && self.mode.is_full() } pub fn inline_diagnostics_enabled(&self) -> bool { - self.diagnostics_enabled() && self.inline_diagnostics_enabled + self.inline_diagnostics_enabled && self.diagnostics_enabled() } pub fn show_inline_diagnostics(&self) -> bool { @@ -15703,15 +16989,14 @@ impl Editor { None }; self.inline_diagnostics_update = cx.spawn_in(window, async move |editor, cx| { - let editor = editor.upgrade().unwrap(); - if let Some(debounce) = debounce { cx.background_executor().timer(debounce).await; } - let Some(snapshot) = editor - .update(cx, |editor, cx| editor.buffer().read(cx).snapshot(cx)) - .ok() - else { + let Some(snapshot) = editor.upgrade().and_then(|editor| { + editor + .update(cx, |editor, cx| editor.buffer().read(cx).snapshot(cx)) + .ok() + }) else { return; }; @@ -15758,6 +17043,68 @@ impl Editor { }); } + fn pull_diagnostics( + &mut self, + buffer_id: Option, + window: &Window, + cx: &mut Context, + ) -> Option<()> { + if !self.mode().is_full() { + return None; + } + let pull_diagnostics_settings = ProjectSettings::get_global(cx) + .diagnostics + .lsp_pull_diagnostics; + if !pull_diagnostics_settings.enabled { + return None; + } + let project = self.project()?.downgrade(); + let debounce = Duration::from_millis(pull_diagnostics_settings.debounce_ms); + let mut buffers = self.buffer.read(cx).all_buffers(); + if let Some(buffer_id) = buffer_id { + buffers.retain(|buffer| buffer.read(cx).remote_id() == buffer_id); + } + + self.pull_diagnostics_task = cx.spawn_in(window, async move |editor, cx| { + cx.background_executor().timer(debounce).await; + + let Ok(mut pull_diagnostics_tasks) = cx.update(|_, cx| { + buffers + .into_iter() + .filter_map(|buffer| { + project + .update(cx, |project, cx| { + project.lsp_store().update(cx, |lsp_store, cx| { + lsp_store.pull_diagnostics_for_buffer(buffer, cx) + }) + }) + .ok() + }) + .collect::>() + }) else { + return; + }; + + while let Some(pull_task) = pull_diagnostics_tasks.next().await { + match pull_task { + Ok(()) => { + if editor + .update_in(cx, |editor, window, cx| { + editor.update_diagnostics_state(window, cx); + }) + .is_err() + { + return; + } + } + Err(e) => log::error!("Failed to update project diagnostics: {e:#}"), + } + } + }); + + Some(()) + } + pub fn set_selections_from_remote( &mut self, selections: Vec>, @@ -15774,7 +17121,13 @@ impl Editor { s.clear_pending(); } }); - self.selections_did_change(false, &old_cursor_position, true, window, cx); + self.selections_did_change( + false, + &old_cursor_position, + SelectionEffects::default(), + window, + cx, + ); } pub fn transact( @@ -15795,7 +17148,7 @@ impl Editor { now: Instant, window: &mut Window, cx: &mut Context, - ) { + ) -> Option { self.end_selection(window, cx); if let Some(tx_id) = self .buffer @@ -15805,7 +17158,10 @@ impl Editor { .insert_transaction(tx_id, self.selections.disjoint_anchors()); cx.emit(EditorEvent::TransactionBegun { transaction_id: tx_id, - }) + }); + Some(tx_id) + } else { + None } } @@ -15833,9 +17189,20 @@ impl Editor { } } + pub fn modify_transaction_selection_history( + &mut self, + transaction_id: TransactionId, + modify: impl FnOnce(&mut (Arc<[Selection]>, Option]>>)), + ) -> bool { + self.selection_history + .transaction_mut(transaction_id) + .map(modify) + .is_some() + } + pub fn set_mark(&mut self, _: &actions::SetMark, window: &mut Window, cx: &mut Context) { if self.selection_mark_mode { - self.change_selections(None, window, cx, |s| { + self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.move_with(|_, sel| { sel.collapse_to(sel.head(), SelectionGoal::None); }); @@ -15851,7 +17218,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.change_selections(None, window, cx, |s| { + self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.move_with(|_, sel| { if sel.start != sel.end { sel.reversed = !sel.reversed @@ -15862,6 +17229,18 @@ impl Editor { cx.notify(); } + pub fn toggle_focus( + workspace: &mut Workspace, + _: &actions::ToggleFocus, + window: &mut Window, + cx: &mut Context, + ) { + let Some(item) = workspace.recent_active_item_by_type::(cx) else { + return; + }; + workspace.activate_item(&item, true, true, window, cx); + } + pub fn toggle_fold( &mut self, _: &actions::ToggleFold, @@ -15962,12 +17341,12 @@ impl Editor { } for row in (0..=range.start.row).rev() { - if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row)) { - if crease.range().end.row >= buffer_start_row { - to_fold.push(crease); - if row <= range.start.row { - break; - } + if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row)) + && crease.range().end.row >= buffer_start_row + { + to_fold.push(crease); + if row <= range.start.row { + break; } } } @@ -15987,6 +17366,46 @@ impl Editor { } } + pub fn toggle_fold_all( + &mut self, + _: &actions::ToggleFoldAll, + window: &mut Window, + cx: &mut Context, + ) { + if self.buffer.read(cx).is_singleton() { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let has_folds = display_map + .folds_in_range(0..display_map.buffer_snapshot.len()) + .next() + .is_some(); + + if has_folds { + self.unfold_all(&actions::UnfoldAll, window, cx); + } else { + self.fold_all(&actions::FoldAll, window, cx); + } + } else { + let buffer_ids = self.buffer.read(cx).excerpt_buffer_ids(); + let should_unfold = buffer_ids + .iter() + .any(|buffer_id| self.is_buffer_folded(*buffer_id, cx)); + + self.toggle_fold_multiple_buffers = cx.spawn_in(window, async move |editor, cx| { + editor + .update_in(cx, |editor, _, cx| { + for buffer_id in buffer_ids { + if should_unfold { + editor.unfold_buffer(buffer_id, cx); + } else { + editor.fold_buffer(buffer_id, cx); + } + } + }) + .ok(); + }); + } + } + fn fold_at_level( &mut self, fold_at: &FoldAtLevel, @@ -16277,16 +17696,6 @@ impl Editor { return; } - let mut buffers_affected = HashSet::default(); - let multi_buffer = self.buffer().read(cx); - for crease in &creases { - if let Some((_, buffer, _)) = - multi_buffer.excerpt_containing(crease.range().start.clone(), cx) - { - buffers_affected.insert(buffer.read(cx).remote_id()); - }; - } - self.display_map.update(cx, |map, cx| map.fold(creases, cx)); if auto_scroll { @@ -16402,9 +17811,9 @@ impl Editor { self.active_indent_guides_state.dirty = true; } - pub fn update_fold_widths( + pub fn update_renderer_widths( &mut self, - widths: impl IntoIterator, + widths: impl IntoIterator, cx: &mut Context, ) -> bool { self.display_map @@ -16465,7 +17874,7 @@ impl Editor { ranges: &[Range], snapshot: &MultiBufferSnapshot, ) -> bool { - let mut hunks = self.diff_hunks_in_ranges(ranges, &snapshot); + let mut hunks = self.diff_hunks_in_ranges(ranges, snapshot); hunks.any(|hunk| hunk.status().has_secondary_hunk()) } @@ -16600,7 +18009,7 @@ impl Editor { let autoscroll = Autoscroll::center(); self.unfold_ranges(&[destination..destination], false, false, cx); - self.change_selections(Some(autoscroll), window, cx, |s| { + self.change_selections(SelectionEffects::scroll(autoscroll), window, cx, |s| { s.select_ranges([destination..destination]); }); } @@ -16613,7 +18022,7 @@ impl Editor { hunks: impl Iterator, cx: &mut App, ) -> Option<()> { - let project = self.project.as_ref()?; + let project = self.project()?; let buffer = project.read(cx).buffer_for_id(buffer_id, cx)?; let diff = self.buffer.read(cx).diff_for(buffer_id)?; let buffer_snapshot = buffer.read(cx).snapshot(); @@ -16686,7 +18095,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let buffers = self.buffer.read(cx).all_buffers(); for branch_buffer in buffers { @@ -16696,7 +18105,16 @@ impl Editor { } if let Some(project) = self.project.clone() { - self.save(true, project, window, cx).detach_and_log_err(cx); + self.save( + SaveOptions { + format: true, + autosave: false, + }, + project, + window, + cx, + ) + .detach_and_log_err(cx); } } @@ -16706,7 +18124,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let snapshot = self.snapshot(window, cx); let hunks = snapshot.hunks_for_ranges(self.selections.ranges(cx)); let mut ranges_by_buffer = HashMap::default(); @@ -16728,7 +18146,16 @@ impl Editor { }); if let Some(project) = self.project.clone() { - self.save(true, project, window, cx).detach_and_log_err(cx); + self.save( + SaveOptions { + format: true, + autosave: false, + }, + project, + window, + cx, + ) + .detach_and_log_err(cx); } } @@ -16908,7 +18335,7 @@ impl Editor { parent: cx.weak_entity(), }, self.buffer.clone(), - self.project.clone(), + None, Some(self.display_map.clone()), window, cx, @@ -17229,10 +18656,10 @@ impl Editor { pub fn working_directory(&self, cx: &App) -> Option { if let Some(buffer) = self.buffer().read(cx).as_singleton() { - if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) { - if let Some(dir) = file.abs_path(cx).parent() { - return Some(dir.to_owned()); - } + if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) + && let Some(dir) = file.abs_path(cx).parent() + { + return Some(dir.to_owned()); } if let Some(project_path) = buffer.read(cx).project_path(cx) { @@ -17255,7 +18682,7 @@ impl Editor { self.active_excerpt(cx).and_then(|(_, buffer, _)| { let buffer = buffer.read(cx); if let Some(project_path) = buffer.project_path(cx) { - let project = self.project.as_ref()?.read(cx); + let project = self.project()?.read(cx); project.absolute_path(&project_path, cx) } else { buffer @@ -17268,7 +18695,7 @@ impl Editor { fn target_file_path(&self, cx: &mut Context) -> Option { self.active_excerpt(cx).and_then(|(_, buffer, _)| { let project_path = buffer.read(cx).project_path(cx)?; - let project = self.project.as_ref()?.read(cx); + let project = self.project()?.read(cx); let entry = project.entry_for_path(&project_path, cx)?; let path = entry.path.to_path_buf(); Some(path) @@ -17292,10 +18719,10 @@ impl Editor { _window: &mut Window, cx: &mut Context, ) { - if let Some(path) = self.target_file_abs_path(cx) { - if let Some(path) = path.to_str() { - cx.write_to_clipboard(ClipboardItem::new_string(path.to_string())); - } + if let Some(path) = self.target_file_abs_path(cx) + && let Some(path) = path.to_str() + { + cx.write_to_clipboard(ClipboardItem::new_string(path.to_string())); } } @@ -17305,10 +18732,10 @@ impl Editor { _window: &mut Window, cx: &mut Context, ) { - if let Some(path) = self.target_file_path(cx) { - if let Some(path) = path.to_str() { - cx.write_to_clipboard(ClipboardItem::new_string(path.to_string())); - } + if let Some(path) = self.target_file_path(cx) + && let Some(path) = path.to_str() + { + cx.write_to_clipboard(ClipboardItem::new_string(path.to_string())); } } @@ -17377,22 +18804,20 @@ impl Editor { _: &mut Window, cx: &mut Context, ) { - if let Some(file) = self.target_file(cx) { - if let Some(file_stem) = file.path().file_stem() { - if let Some(name) = file_stem.to_str() { - cx.write_to_clipboard(ClipboardItem::new_string(name.to_string())); - } - } + if let Some(file) = self.target_file(cx) + && let Some(file_stem) = file.path().file_stem() + && let Some(name) = file_stem.to_str() + { + cx.write_to_clipboard(ClipboardItem::new_string(name.to_string())); } } pub fn copy_file_name(&mut self, _: &CopyFileName, _: &mut Window, cx: &mut Context) { - if let Some(file) = self.target_file(cx) { - if let Some(file_name) = file.path().file_name() { - if let Some(name) = file_name.to_str() { - cx.write_to_clipboard(ClipboardItem::new_string(name.to_string())); - } - } + if let Some(file) = self.target_file(cx) + && let Some(file_name) = file.path().file_name() + && let Some(name) = file_name.to_str() + { + cx.write_to_clipboard(ClipboardItem::new_string(name.to_string())); } } @@ -17489,7 +18914,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if let Some(project) = self.project.as_ref() { + if let Some(project) = self.project() { let Some(buffer) = self.buffer().read(cx).as_singleton() else { return; }; @@ -17566,7 +18991,7 @@ impl Editor { fn has_blame_entries(&self, cx: &App) -> bool { self.blame() - .map_or(false, |blame| blame.read(cx).has_generated_entries()) + .is_some_and(|blame| blame.read(cx).has_generated_entries()) } fn newest_selection_head_on_empty_line(&self, cx: &App) -> bool { @@ -17593,19 +19018,16 @@ impl Editor { buffer_ranges.last() }?; - let selection = text::ToPoint::to_point(&range.start, &buffer).row - ..text::ToPoint::to_point(&range.end, &buffer).row; - Some(( - multi_buffer.buffer(buffer.remote_id()).unwrap().clone(), - selection, - )) + let selection = text::ToPoint::to_point(&range.start, buffer).row + ..text::ToPoint::to_point(&range.end, buffer).row; + Some((multi_buffer.buffer(buffer.remote_id()).unwrap(), selection)) }); let Some((buffer, selection)) = buffer_and_selection else { return Task::ready(Err(anyhow!("failed to determine buffer and selection"))); }; - let Some(project) = self.project.as_ref() else { + let Some(project) = self.project() else { return Task::ready(Err(anyhow!("editor does not have project"))); }; @@ -17662,10 +19084,10 @@ impl Editor { cx: &mut Context, ) { let selection = self.selections.newest::(cx).start.row + 1; - if let Some(file) = self.target_file(cx) { - if let Some(path) = file.path().to_str() { - cx.write_to_clipboard(ClipboardItem::new_string(format!("{path}:{selection}"))); - } + if let Some(file) = self.target_file(cx) + && let Some(path) = file.path().to_str() + { + cx.write_to_clipboard(ClipboardItem::new_string(format!("{path}:{selection}"))); } } @@ -17729,7 +19151,7 @@ impl Editor { } fn insert_uuid(&mut self, version: UuidVersion, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); + self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.transact(window, cx, |this, window, cx| { let edits = this .selections @@ -17744,7 +19166,7 @@ impl Editor { (selection.range(), uuid.to_string()) }); this.edit(edits, cx); - this.refresh_inline_completion(true, false, window, cx); + this.refresh_edit_prediction(true, false, window, cx); }); } @@ -17764,25 +19186,18 @@ impl Editor { return; }; + let title = multibuffer.title(cx).to_string(); + let locations = self .selections - .disjoint_anchors() + .all_anchors(cx) .iter() - .map(|selection| { - let range = if selection.reversed { - selection.end.text_anchor..selection.start.text_anchor - } else { - selection.start.text_anchor..selection.end.text_anchor - }; - Location { - buffer: buffer.clone(), - range, - } + .map(|selection| Location { + buffer: buffer.clone(), + range: selection.start.text_anchor..selection.end.text_anchor, }) .collect::>(); - let title = multibuffer.title(cx).to_string(); - cx.spawn_in(window, async move |_, cx| { workspace.update_in(cx, |workspace, window, cx| { Self::open_locations_in_multibuffer( @@ -17847,7 +19262,7 @@ impl Editor { row_highlights.insert( ix, RowHighlight { - range: range.clone(), + range, index, color, options, @@ -17993,7 +19408,7 @@ impl Editor { pub fn set_search_within_ranges(&mut self, ranges: &[Range], cx: &mut Context) { self.highlight_background::( ranges, - |colors| colors.editor_document_highlight_read_background, + |colors| colors.colors().editor_document_highlight_read_background, cx, ) } @@ -18009,11 +19424,28 @@ impl Editor { pub fn highlight_background( &mut self, ranges: &[Range], - color_fetcher: fn(&ThemeColors) -> Hsla, + color_fetcher: fn(&Theme) -> Hsla, cx: &mut Context, ) { - self.background_highlights - .insert(TypeId::of::(), (color_fetcher, Arc::from(ranges))); + self.background_highlights.insert( + HighlightKey::Type(TypeId::of::()), + (color_fetcher, Arc::from(ranges)), + ); + self.scrollbar_marker_state.dirty = true; + cx.notify(); + } + + pub fn highlight_background_key( + &mut self, + key: usize, + ranges: &[Range], + color_fetcher: fn(&Theme) -> Hsla, + cx: &mut Context, + ) { + self.background_highlights.insert( + HighlightKey::TypePlus(TypeId::of::(), key), + (color_fetcher, Arc::from(ranges)), + ); self.scrollbar_marker_state.dirty = true; cx.notify(); } @@ -18022,7 +19454,9 @@ impl Editor { &mut self, cx: &mut Context, ) -> Option { - let text_highlights = self.background_highlights.remove(&TypeId::of::())?; + let text_highlights = self + .background_highlights + .remove(&HighlightKey::Type(TypeId::of::()))?; if !text_highlights.1.is_empty() { self.scrollbar_marker_state.dirty = true; cx.notify(); @@ -18032,12 +19466,12 @@ impl Editor { pub fn highlight_gutter( &mut self, - ranges: &[Range], + ranges: impl Into>>, color_fetcher: fn(&App) -> Hsla, cx: &mut Context, ) { self.gutter_highlights - .insert(TypeId::of::(), (color_fetcher, Arc::from(ranges))); + .insert(TypeId::of::(), (color_fetcher, ranges.into())); cx.notify(); } @@ -18049,6 +19483,89 @@ impl Editor { self.gutter_highlights.remove(&TypeId::of::()) } + pub fn insert_gutter_highlight( + &mut self, + range: Range, + color_fetcher: fn(&App) -> Hsla, + cx: &mut Context, + ) { + let snapshot = self.buffer().read(cx).snapshot(cx); + let mut highlights = self + .gutter_highlights + .remove(&TypeId::of::()) + .map(|(_, highlights)| highlights) + .unwrap_or_default(); + let ix = highlights.binary_search_by(|highlight| { + Ordering::Equal + .then_with(|| highlight.start.cmp(&range.start, &snapshot)) + .then_with(|| highlight.end.cmp(&range.end, &snapshot)) + }); + if let Err(ix) = ix { + highlights.insert(ix, range); + } + self.gutter_highlights + .insert(TypeId::of::(), (color_fetcher, highlights)); + } + + pub fn remove_gutter_highlights( + &mut self, + ranges_to_remove: Vec>, + cx: &mut Context, + ) { + let snapshot = self.buffer().read(cx).snapshot(cx); + let Some((color_fetcher, mut gutter_highlights)) = + self.gutter_highlights.remove(&TypeId::of::()) + else { + return; + }; + let mut ranges_to_remove = ranges_to_remove.iter().peekable(); + gutter_highlights.retain(|highlight| { + while let Some(range_to_remove) = ranges_to_remove.peek() { + match range_to_remove.end.cmp(&highlight.start, &snapshot) { + Ordering::Less | Ordering::Equal => { + ranges_to_remove.next(); + } + Ordering::Greater => { + match range_to_remove.start.cmp(&highlight.end, &snapshot) { + Ordering::Less | Ordering::Equal => { + return false; + } + Ordering::Greater => break, + } + } + } + } + + true + }); + self.gutter_highlights + .insert(TypeId::of::(), (color_fetcher, gutter_highlights)); + } + + #[cfg(feature = "test-support")] + pub fn all_text_highlights( + &self, + window: &mut Window, + cx: &mut Context, + ) -> Vec<(HighlightStyle, Vec>)> { + let snapshot = self.snapshot(window, cx); + self.display_map.update(cx, |display_map, _| { + display_map + .all_text_highlights() + .map(|highlight| { + let (style, ranges) = highlight.as_ref(); + ( + *style, + ranges + .iter() + .map(|range| range.clone().to_display_points(&snapshot)) + .collect(), + ) + }) + .collect() + }) + } + #[cfg(feature = "test-support")] pub fn all_text_background_highlights( &self, @@ -18059,8 +19576,7 @@ impl Editor { let buffer = &snapshot.buffer_snapshot; let start = buffer.anchor_before(0); let end = buffer.anchor_after(buffer.len()); - let theme = cx.theme().colors(); - self.background_highlights_in_range(start..end, &snapshot, theme) + self.background_highlights_in_range(start..end, &snapshot, cx.theme()) } #[cfg(feature = "test-support")] @@ -18069,7 +19585,9 @@ impl Editor { let highlights = self .background_highlights - .get(&TypeId::of::()); + .get(&HighlightKey::Type(TypeId::of::< + items::BufferSearchHighlights, + >())); if let Some((_color, ranges)) = highlights { ranges @@ -18088,11 +19606,11 @@ impl Editor { ) -> impl 'a + Iterator> { let read_highlights = self .background_highlights - .get(&TypeId::of::()) + .get(&HighlightKey::Type(TypeId::of::())) .map(|h| &h.1); let write_highlights = self .background_highlights - .get(&TypeId::of::()) + .get(&HighlightKey::Type(TypeId::of::())) .map(|h| &h.1); let left_position = position.bias_left(buffer); let right_position = position.bias_right(buffer); @@ -18119,15 +19637,15 @@ impl Editor { pub fn has_background_highlights(&self) -> bool { self.background_highlights - .get(&TypeId::of::()) - .map_or(false, |(_, highlights)| !highlights.is_empty()) + .get(&HighlightKey::Type(TypeId::of::())) + .is_some_and(|(_, highlights)| !highlights.is_empty()) } pub fn background_highlights_in_range( &self, search_range: Range, display_snapshot: &DisplaySnapshot, - theme: &ThemeColors, + theme: &Theme, ) -> Vec<(Range, Hsla)> { let mut results = Vec::new(); for (color_fetcher, ranges) in self.background_highlights.values() { @@ -18168,7 +19686,10 @@ impl Editor { count: usize, ) -> Vec> { let mut results = Vec::new(); - let Some((_, ranges)) = self.background_highlights.get(&TypeId::of::()) else { + let Some((_, ranges)) = self + .background_highlights + .get(&HighlightKey::Type(TypeId::of::())) + else { return vec![]; }; @@ -18206,10 +19727,10 @@ impl Editor { break; } let end = range.end.to_point(&display_snapshot.buffer_snapshot); - if let Some(current_row) = &end_row { - if end.row == current_row.row { - continue; - } + if let Some(current_row) = &end_row + && end.row == current_row.row + { + continue; } let start = range.start.to_point(&display_snapshot.buffer_snapshot); if start_row.is_none() { @@ -18305,6 +19826,23 @@ impl Editor { .collect() } + pub fn highlight_text_key( + &mut self, + key: usize, + ranges: Vec>, + style: HighlightStyle, + cx: &mut Context, + ) { + self.display_map.update(cx, |map, _| { + map.highlight_text( + HighlightKey::TypePlus(TypeId::of::(), key), + ranges, + style, + ); + }); + cx.notify(); + } + pub fn highlight_text( &mut self, ranges: Vec>, @@ -18312,7 +19850,7 @@ impl Editor { cx: &mut Context, ) { self.display_map.update(cx, |map, _| { - map.highlight_text(TypeId::of::(), ranges, style) + map.highlight_text(HighlightKey::Type(TypeId::of::()), ranges, style) }); cx.notify(); } @@ -18365,11 +19903,8 @@ impl Editor { event: &SessionEvent, cx: &mut Context, ) { - match event { - SessionEvent::InvalidateInlineValue => { - self.refresh_inline_values(cx); - } - _ => {} + if let SessionEvent::InvalidateInlineValue = event { + self.refresh_inline_values(cx); } } @@ -18387,7 +19922,7 @@ impl Editor { let current_execution_position = self .highlighted_rows .get(&TypeId::of::()) - .and_then(|lines| lines.last().map(|line| line.range.start)); + .and_then(|lines| lines.last().map(|line| line.range.end)); self.inline_value_cache.refresh_task = cx.spawn(async move |editor, cx| { let inline_values = editor @@ -18441,13 +19976,14 @@ impl Editor { .into_iter() .flatten() .for_each(|hint| { - let inlay = Inlay::debugger_hint( + let inlay = Inlay::debugger( post_inc(&mut editor.next_inlay_id), Anchor::in_buffer(excerpt_id, buffer_id, hint.position), hint.text(), ); - - new_inlays.push(inlay); + if !inlay.text.chars().contains(&'\n') { + new_inlays.push(inlay); + } }); } @@ -18471,33 +20007,42 @@ impl Editor { match event { multi_buffer::Event::Edited { singleton_buffer_edited, - edited_buffer: buffer_edited, + edited_buffer, } => { self.scrollbar_marker_state.dirty = true; self.active_indent_guides_state.dirty = true; self.refresh_active_diagnostics(cx); self.refresh_code_actions(window, cx); self.refresh_selected_text_highlights(true, window, cx); + self.refresh_single_line_folds(window, cx); refresh_matching_bracket_highlights(self, window, cx); - if self.has_active_inline_completion() { - self.update_visible_inline_completion(window, cx); + if self.has_active_edit_prediction() { + self.update_visible_edit_prediction(window, cx); } - if let Some(buffer) = buffer_edited { - let buffer_id = buffer.read(cx).remote_id(); - if !self.registered_buffers.contains_key(&buffer_id) { - if let Some(project) = self.project.as_ref() { - project.update(cx, |project, cx| { - self.registered_buffers.insert( - buffer_id, - project.register_buffer_with_language_servers(&buffer, cx), - ); - }) - } - } + if let Some(project) = self.project.as_ref() + && let Some(edited_buffer) = edited_buffer + { + project.update(cx, |project, cx| { + self.registered_buffers + .entry(edited_buffer.read(cx).remote_id()) + .or_insert_with(|| { + project.register_buffer_with_language_servers(edited_buffer, cx) + }); + }); } cx.emit(EditorEvent::BufferEdited); cx.emit(SearchEvent::MatchesInvalidated); + + if let Some(buffer) = edited_buffer { + self.update_lsp_data(false, Some(buffer.read(cx).remote_id()), window, cx); + } + if *singleton_buffer_edited { + if let Some(buffer) = edited_buffer + && buffer.read(cx).file().is_none() + { + cx.emit(EditorEvent::TitleChanged); + } if let Some(project) = &self.project { #[allow(clippy::mutable_key_type)] let languages_affected = multibuffer.update(cx, |multibuffer, cx| { @@ -18542,18 +20087,19 @@ impl Editor { } => { self.tasks_update_task = Some(self.refresh_runnables(window, cx)); let buffer_id = buffer.read(cx).remote_id(); - if self.buffer.read(cx).diff_for(buffer_id).is_none() { - if let Some(project) = &self.project { - update_uncommitted_diff_for_buffer( - cx.entity(), - project, - [buffer.clone()], - self.buffer.clone(), - cx, - ) - .detach(); - } + if self.buffer.read(cx).diff_for(buffer_id).is_none() + && let Some(project) = &self.project + { + update_uncommitted_diff_for_buffer( + cx.entity(), + project, + [buffer.clone()], + self.buffer.clone(), + cx, + ) + .detach(); } + self.update_lsp_data(false, Some(buffer_id), window, cx); cx.emit(EditorEvent::ExcerptsAdded { buffer: buffer.clone(), predecessor: *predecessor, @@ -18573,7 +20119,7 @@ impl Editor { cx.emit(EditorEvent::ExcerptsRemoved { ids: ids.clone(), removed_buffer_ids: removed_buffer_ids.clone(), - }) + }); } multi_buffer::Event::ExcerptsEdited { excerpt_ids, @@ -18584,7 +20130,7 @@ impl Editor { }); cx.emit(EditorEvent::ExcerptsEdited { ids: excerpt_ids.clone(), - }) + }); } multi_buffer::Event::ExcerptsExpanded { ids } => { self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx); @@ -18612,15 +20158,22 @@ impl Editor { | multi_buffer::Event::BufferDiffChanged => cx.emit(EditorEvent::TitleChanged), multi_buffer::Event::Closed => cx.emit(EditorEvent::Closed), multi_buffer::Event::DiagnosticsUpdated => { - self.refresh_active_diagnostics(cx); - self.refresh_inline_diagnostics(true, window, cx); - self.scrollbar_marker_state.dirty = true; - cx.notify(); + self.update_diagnostics_state(window, cx); } _ => {} }; } + fn update_diagnostics_state(&mut self, window: &mut Window, cx: &mut Context<'_, Editor>) { + if !self.diagnostics_enabled() { + return; + } + self.refresh_active_diagnostics(cx); + self.refresh_inline_diagnostics(true, window, cx); + self.scrollbar_marker_state.dirty = true; + cx.notify(); + } + pub fn start_temporary_diff_override(&mut self) { self.load_diff_task.take(); self.temporary_diff_override = true; @@ -18657,17 +20210,16 @@ impl Editor { } fn settings_changed(&mut self, window: &mut Window, cx: &mut Context) { - let new_severity = if self.diagnostics_enabled() { - EditorSettings::get_global(cx) + if self.diagnostics_enabled() { + let new_severity = EditorSettings::get_global(cx) .diagnostics_max_severity - .unwrap_or(DiagnosticSeverity::Hint) - } else { - DiagnosticSeverity::Off - }; - self.set_max_diagnostics_severity(new_severity, cx); + .unwrap_or(DiagnosticSeverity::Hint); + self.set_max_diagnostics_severity(new_severity, cx); + } self.tasks_update_task = Some(self.refresh_runnables(window, cx)); self.update_edit_prediction_settings(cx); - self.refresh_inline_completion(true, false, window, cx); + self.refresh_edit_prediction(true, false, window, cx); + self.refresh_inline_values(cx); self.refresh_inlay_hints( InlayHintRefreshReason::SettingsChange(inlay_hint_settings( self.selections.newest_anchor().head(), @@ -18678,6 +20230,7 @@ impl Editor { ); let old_cursor_shape = self.cursor_shape; + let old_show_breadcrumbs = self.show_breadcrumbs; { let editor_settings = EditorSettings::get_global(cx); @@ -18691,6 +20244,10 @@ impl Editor { cx.emit(EditorEvent::CursorShapeChanged); } + if old_show_breadcrumbs != self.show_breadcrumbs { + cx.emit(EditorEvent::BreadcrumbsChanged); + } + let project_settings = ProjectSettings::get_global(cx); self.serialize_dirty_buffers = !self.mode.is_minimap() && project_settings.session.restore_unsaved_buffers; @@ -18725,6 +20282,15 @@ impl Editor { } } + if let Some(inlay_splice) = self.colors.as_mut().and_then(|colors| { + colors.render_mode_updated(EditorSettings::get_global(cx).lsp_document_colors) + }) { + if !inlay_splice.to_insert.is_empty() || !inlay_splice.to_remove.is_empty() { + self.splice_inlays(&inlay_splice.to_remove, inlay_splice.to_insert, cx); + } + self.refresh_colors(false, None, window, cx); + } + cx.notify(); } @@ -18879,11 +20445,8 @@ impl Editor { .range_to_buffer_ranges_with_deleted_hunks(selection.range()) { if let Some(anchor) = anchor { - // selection is in a deleted hunk - let Some(buffer_id) = anchor.buffer_id else { - continue; - }; - let Some(buffer_handle) = multi_buffer.buffer(buffer_id) else { + let Some(buffer_handle) = multi_buffer.buffer_for_anchor(anchor, cx) + else { continue; }; let offset = text::ToOffset::to_offset( @@ -18973,9 +20536,14 @@ impl Editor { None => Autoscroll::newest(), }; let nav_history = editor.nav_history.take(); - editor.change_selections(Some(autoscroll), window, cx, |s| { - s.select_ranges(ranges); - }); + editor.change_selections( + SelectionEffects::scroll(autoscroll), + window, + cx, + |s| { + s.select_ranges(ranges); + }, + ); editor.nav_history = nav_history; }); } @@ -18986,7 +20554,7 @@ impl Editor { // For now, don't allow opening excerpts in buffers that aren't backed by // regular project files. fn can_open_excerpts_in_file(file: Option<&Arc>) -> bool { - file.map_or(true, |file| project::File::from_dyn(Some(file)).is_some()) + file.is_none_or(|file| project::File::from_dyn(Some(file)).is_some()) } fn marked_text_ranges(&self, cx: &App) -> Option>> { @@ -19029,7 +20597,7 @@ impl Editor { fn report_editor_event( &self, - event_type: &'static str, + reported_event: ReportEditorEvent, file_extension: Option, cx: &App, ) { @@ -19063,15 +20631,30 @@ impl Editor { .show_edit_predictions; let project = project.read(cx); - telemetry::event!( - event_type, - file_extension, - vim_mode, - copilot_enabled, - copilot_enabled_for_language, - edit_predictions_provider, - is_via_ssh = project.is_via_ssh(), - ); + let event_type = reported_event.event_type(); + + if let ReportEditorEvent::Saved { auto_saved } = reported_event { + telemetry::event!( + event_type, + type = if auto_saved {"autosave"} else {"manual"}, + file_extension, + vim_mode, + copilot_enabled, + copilot_enabled_for_language, + edit_predictions_provider, + is_via_ssh = project.is_via_ssh(), + ); + } else { + telemetry::event!( + event_type, + file_extension, + vim_mode, + copilot_enabled, + copilot_enabled_for_language, + edit_predictions_provider, + is_via_ssh = project.is_via_ssh(), + ); + }; } /// Copy the highlighted chunks to the clipboard as JSON. The format is an array of lines, @@ -19115,11 +20698,11 @@ impl Editor { let mut chunk_lines = chunk.text.split('\n').peekable(); while let Some(text) = chunk_lines.next() { let mut merged_with_last_token = false; - if let Some(last_token) = line.back_mut() { - if last_token.highlight == highlight { - last_token.text.push_str(text); - merged_with_last_token = true; - } + if let Some(last_token) = line.back_mut() + && last_token.highlight == highlight + { + last_token.text.push_str(text); + merged_with_last_token = true; } if !merged_with_last_token { @@ -19176,7 +20759,7 @@ impl Editor { } if let Some(relative_utf16_range) = relative_utf16_range { let selections = self.selections.all::(cx); - self.change_selections(None, window, cx, |s| { + self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { let new_ranges = selections.into_iter().map(|range| { let start = OffsetUtf16( range @@ -19261,6 +20844,7 @@ impl Editor { if event.blurred != self.focus_handle { self.last_focused_descendant = Some(event.blurred); } + self.selection_drag_state = SelectionDragState::None; self.refresh_inlay_hints(InlayHintRefreshReason::ModifiersChanged(false), cx); } @@ -19283,11 +20867,110 @@ impl Editor { { self.hide_context_menu(window, cx); } - self.discard_inline_completion(false, cx); + self.discard_edit_prediction(false, cx); cx.emit(EditorEvent::Blurred); cx.notify(); } + pub fn observe_pending_input(&mut self, window: &mut Window, cx: &mut Context) { + let mut pending: String = window + .pending_input_keystrokes() + .into_iter() + .flatten() + .filter_map(|keystroke| { + if keystroke.modifiers.is_subset_of(&Modifiers::shift()) { + keystroke.key_char.clone() + } else { + None + } + }) + .collect(); + + if !self.input_enabled || self.read_only || !self.focus_handle.is_focused(window) { + pending = "".to_string(); + } + + let existing_pending = self + .text_highlights::(cx) + .map(|(_, ranges)| ranges.to_vec()); + if existing_pending.is_none() && pending.is_empty() { + return; + } + let transaction = + self.transact(window, cx, |this, window, cx| { + let selections = this.selections.all::(cx); + let edits = selections + .iter() + .map(|selection| (selection.end..selection.end, pending.clone())); + this.edit(edits, cx); + this.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges(selections.into_iter().enumerate().map(|(ix, sel)| { + sel.start + ix * pending.len()..sel.end + ix * pending.len() + })); + }); + if let Some(existing_ranges) = existing_pending { + let edits = existing_ranges.iter().map(|range| (range.clone(), "")); + this.edit(edits, cx); + } + }); + + let snapshot = self.snapshot(window, cx); + let ranges = self + .selections + .all::(cx) + .into_iter() + .map(|selection| { + snapshot.buffer_snapshot.anchor_after(selection.end) + ..snapshot + .buffer_snapshot + .anchor_before(selection.end + pending.len()) + }) + .collect(); + + if pending.is_empty() { + self.clear_highlights::(cx); + } else { + self.highlight_text::( + ranges, + HighlightStyle { + underline: Some(UnderlineStyle { + thickness: px(1.), + color: None, + wavy: false, + }), + ..Default::default() + }, + cx, + ); + } + + self.ime_transaction = self.ime_transaction.or(transaction); + if let Some(transaction) = self.ime_transaction { + self.buffer.update(cx, |buffer, cx| { + buffer.group_until_transaction(transaction, cx); + }); + } + + if self.text_highlights::(cx).is_none() { + self.ime_transaction.take(); + } + } + + pub fn register_action_renderer( + &mut self, + listener: impl Fn(&Editor, &mut Window, &mut Context) + 'static, + ) -> Subscription { + let id = self.next_editor_action_id.post_inc(); + self.editor_actions + .borrow_mut() + .insert(id, Box::new(listener)); + + let editor_actions = self.editor_actions.clone(); + Subscription::new(move || { + editor_actions.borrow_mut().remove(&id); + }) + } + pub fn register_action( &mut self, listener: impl Fn(&A, &mut Window, &mut App) + 'static, @@ -19296,7 +20979,7 @@ impl Editor { let listener = Arc::new(listener); self.editor_actions.borrow_mut().insert( id, - Box::new(move |window, _| { + Box::new(move |_, window, _| { let listener = listener.clone(); window.on_action(TypeId::of::(), move |action, phase, window, cx| { let action = action.downcast_ref().unwrap(); @@ -19324,7 +21007,7 @@ impl Editor { cx: &mut Context, ) { let workspace = self.workspace(); - let project = self.project.as_ref(); + let project = self.project(); let save_tasks = self.buffer().update(cx, |multi_buffer, cx| { let mut tasks = Vec::new(); for (buffer_id, changes) in revert_changes { @@ -19362,7 +21045,7 @@ impl Editor { }; if let Some((workspace, path)) = workspace.as_ref().zip(path) { let Some(task) = cx - .update_window_entity(&workspace, |workspace, window, cx| { + .update_window_entity(workspace, |workspace, window, cx| { workspace .open_path_preview(path, None, false, false, false, window, cx) }) @@ -19376,7 +21059,9 @@ impl Editor { } }) .detach(); - self.change_selections(None, window, cx, |selections| selections.refresh()); + self.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| { + selections.refresh() + }); } pub fn to_pixel_point( @@ -19412,7 +21097,7 @@ impl Editor { pub fn has_visible_completions_menu(&self) -> bool { !self.edit_prediction_preview_is_active() - && self.context_menu.borrow().as_ref().map_or(false, |menu| { + && self.context_menu.borrow().as_ref().is_some_and(|menu| { menu.visible() && matches!(menu, CodeContextMenu::Completions(_)) }) } @@ -19443,15 +21128,20 @@ impl Editor { .and_then(|item| item.to_any_mut()?.downcast_mut::()) } - fn character_size(&self, window: &mut Window) -> gpui::Size { + fn character_dimensions(&self, window: &mut Window) -> CharacterDimensions { let text_layout_details = self.text_layout_details(window); let style = &text_layout_details.editor_style; let font_id = window.text_system().resolve_font(&style.text.font()); let font_size = style.text.font_size.to_pixels(window.rem_size()); let line_height = style.text.line_height_in_pixels(window.rem_size()); let em_width = window.text_system().em_width(font_id, font_size).unwrap(); + let em_advance = window.text_system().em_advance(font_id, font_size).unwrap(); - gpui::Size::new(em_width, line_height) + CharacterDimensions { + em_width, + em_advance, + line_height, + } } pub fn wait_for_diff_to_load(&self) -> Option>> { @@ -19471,41 +21161,53 @@ impl Editor { { let buffer_snapshot = OnceCell::new(); - if let Some(folds) = DB.get_editor_folds(item_id, workspace_id).log_err() { - if !folds.is_empty() { - let snapshot = - buffer_snapshot.get_or_init(|| self.buffer.read(cx).snapshot(cx)); - self.fold_ranges( - folds - .into_iter() - .map(|(start, end)| { - snapshot.clip_offset(start, Bias::Left) - ..snapshot.clip_offset(end, Bias::Right) - }) - .collect(), - false, - window, - cx, - ); - } - } - - if let Some(selections) = DB.get_editor_selections(item_id, workspace_id).log_err() { - if !selections.is_empty() { - let snapshot = - buffer_snapshot.get_or_init(|| self.buffer.read(cx).snapshot(cx)); - self.change_selections(None, window, cx, |s| { - s.select_ranges(selections.into_iter().map(|(start, end)| { + if let Some(folds) = DB.get_editor_folds(item_id, workspace_id).log_err() + && !folds.is_empty() + { + let snapshot = buffer_snapshot.get_or_init(|| self.buffer.read(cx).snapshot(cx)); + self.fold_ranges( + folds + .into_iter() + .map(|(start, end)| { snapshot.clip_offset(start, Bias::Left) ..snapshot.clip_offset(end, Bias::Right) - })); - }); - } + }) + .collect(), + false, + window, + cx, + ); + } + + if let Some(selections) = DB.get_editor_selections(item_id, workspace_id).log_err() + && !selections.is_empty() + { + let snapshot = buffer_snapshot.get_or_init(|| self.buffer.read(cx).snapshot(cx)); + // skip adding the initial selection to selection history + self.selection_history.mode = SelectionHistoryMode::Skipping; + self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges(selections.into_iter().map(|(start, end)| { + snapshot.clip_offset(start, Bias::Left) + ..snapshot.clip_offset(end, Bias::Right) + })); + }); + self.selection_history.mode = SelectionHistoryMode::Normal; }; } self.read_scroll_position_from_db(item_id, workspace_id, window, cx); } + + fn update_lsp_data( + &mut self, + ignore_cache: bool, + for_buffer: Option, + window: &mut Window, + cx: &mut Context<'_, Self>, + ) { + self.pull_diagnostics(for_buffer, window, cx); + self.refresh_colors(ignore_cache, for_buffer, window, cx); + } } fn vim_enabled(cx: &App) -> bool { @@ -19515,79 +21217,148 @@ fn vim_enabled(cx: &App) -> bool { == Some(&serde_json::Value::Bool(true)) } -// Consider user intent and default settings -fn choose_completion_range( +fn process_completion_for_edit( completion: &Completion, intent: CompletionIntent, buffer: &Entity, + cursor_position: &text::Anchor, cx: &mut Context, -) -> Range { - fn should_replace( - completion: &Completion, - insert_range: &Range, - intent: CompletionIntent, - completion_mode_setting: LspInsertMode, - buffer: &Buffer, - ) -> bool { - // specific actions take precedence over settings - match intent { - CompletionIntent::CompleteWithInsert => return false, - CompletionIntent::CompleteWithReplace => return true, - CompletionIntent::Complete | CompletionIntent::Compose => {} - } - - match completion_mode_setting { - LspInsertMode::Insert => false, - LspInsertMode::Replace => true, - LspInsertMode::ReplaceSubsequence => { - let mut text_to_replace = buffer.chars_for_range( - buffer.anchor_before(completion.replace_range.start) - ..buffer.anchor_after(completion.replace_range.end), - ); - let mut completion_text = completion.new_text.chars(); - - // is `text_to_replace` a subsequence of `completion_text` - text_to_replace - .all(|needle_ch| completion_text.any(|haystack_ch| haystack_ch == needle_ch)) - } - LspInsertMode::ReplaceSuffix => { - let range_after_cursor = insert_range.end..completion.replace_range.end; - - let text_after_cursor = buffer - .text_for_range( - buffer.anchor_before(range_after_cursor.start) - ..buffer.anchor_after(range_after_cursor.end), - ) - .collect::(); - completion.new_text.ends_with(&text_after_cursor) - } - } - } - +) -> CompletionEdit { let buffer = buffer.read(cx); - - if let CompletionSource::Lsp { - insert_range: Some(insert_range), - .. - } = &completion.source - { - let completion_mode_setting = - language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx) - .completions - .lsp_insert_mode; - - if !should_replace( - completion, - &insert_range, - intent, - completion_mode_setting, - buffer, - ) { - return insert_range.to_offset(buffer); + let buffer_snapshot = buffer.snapshot(); + let (snippet, new_text) = if completion.is_snippet() { + // Workaround for typescript language server issues so that methods don't expand within + // strings and functions with type expressions. The previous point is used because the query + // for function identifier doesn't match when the cursor is immediately after. See PR #30312 + let mut snippet_source = completion.new_text.clone(); + let mut previous_point = text::ToPoint::to_point(cursor_position, buffer); + previous_point.column = previous_point.column.saturating_sub(1); + if let Some(scope) = buffer_snapshot.language_scope_at(previous_point) + && scope.prefers_label_for_snippet_in_completion() + && let Some(label) = completion.label() + && matches!( + completion.kind(), + Some(CompletionItemKind::FUNCTION) | Some(CompletionItemKind::METHOD) + ) + { + snippet_source = label; } + match Snippet::parse(&snippet_source).log_err() { + Some(parsed_snippet) => (Some(parsed_snippet.clone()), parsed_snippet.text), + None => (None, completion.new_text.clone()), + } + } else { + (None, completion.new_text.clone()) + }; + + let mut range_to_replace = { + let replace_range = &completion.replace_range; + if let CompletionSource::Lsp { + insert_range: Some(insert_range), + .. + } = &completion.source + { + debug_assert_eq!( + insert_range.start, replace_range.start, + "insert_range and replace_range should start at the same position" + ); + debug_assert!( + insert_range + .start + .cmp(cursor_position, &buffer_snapshot) + .is_le(), + "insert_range should start before or at cursor position" + ); + debug_assert!( + replace_range + .start + .cmp(cursor_position, &buffer_snapshot) + .is_le(), + "replace_range should start before or at cursor position" + ); + + let should_replace = match intent { + CompletionIntent::CompleteWithInsert => false, + CompletionIntent::CompleteWithReplace => true, + CompletionIntent::Complete | CompletionIntent::Compose => { + let insert_mode = + language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx) + .completions + .lsp_insert_mode; + match insert_mode { + LspInsertMode::Insert => false, + LspInsertMode::Replace => true, + LspInsertMode::ReplaceSubsequence => { + let mut text_to_replace = buffer.chars_for_range( + buffer.anchor_before(replace_range.start) + ..buffer.anchor_after(replace_range.end), + ); + let mut current_needle = text_to_replace.next(); + for haystack_ch in completion.label.text.chars() { + if let Some(needle_ch) = current_needle + && haystack_ch.eq_ignore_ascii_case(&needle_ch) + { + current_needle = text_to_replace.next(); + } + } + current_needle.is_none() + } + LspInsertMode::ReplaceSuffix => { + if replace_range + .end + .cmp(cursor_position, &buffer_snapshot) + .is_gt() + { + let range_after_cursor = *cursor_position..replace_range.end; + let text_after_cursor = buffer + .text_for_range( + buffer.anchor_before(range_after_cursor.start) + ..buffer.anchor_after(range_after_cursor.end), + ) + .collect::() + .to_ascii_lowercase(); + completion + .label + .text + .to_ascii_lowercase() + .ends_with(&text_after_cursor) + } else { + true + } + } + } + } + }; + + if should_replace { + replace_range.clone() + } else { + insert_range.clone() + } + } else { + replace_range.clone() + } + }; + + if range_to_replace + .end + .cmp(cursor_position, &buffer_snapshot) + .is_lt() + { + range_to_replace.end = *cursor_position; } - completion.replace_range.to_offset(buffer) + CompletionEdit { + new_text, + replace_range: range_to_replace.to_offset(buffer), + snippet, + } +} + +struct CompletionEdit { + new_text: String, + replace_range: Range, + snippet: Option, } fn insert_extra_newline_brackets( @@ -19749,9 +21520,9 @@ fn is_grapheme_whitespace(text: &str) -> bool { } fn should_stay_with_preceding_ideograph(text: &str) -> bool { - text.chars().next().map_or(false, |ch| { - matches!(ch, '。' | '、' | ',' | '?' | '!' | ':' | ';' | '…') - }) + text.chars() + .next() + .is_some_and(|ch| matches!(ch, '。' | '、' | ',' | '?' | '!' | ':' | ';' | '…')) } #[derive(PartialEq, Eq, Debug, Clone, Copy)] @@ -19781,20 +21552,20 @@ impl<'a> Iterator for WordBreakingTokenizer<'a> { offset += first_grapheme.len(); grapheme_len += 1; if is_grapheme_ideographic(first_grapheme) && !is_whitespace { - if let Some(grapheme) = iter.peek().copied() { - if should_stay_with_preceding_ideograph(grapheme) { - offset += grapheme.len(); - grapheme_len += 1; - } + if let Some(grapheme) = iter.peek().copied() + && should_stay_with_preceding_ideograph(grapheme) + { + offset += grapheme.len(); + grapheme_len += 1; } } else { let mut words = self.input[offset..].split_word_bound_indices().peekable(); let mut next_word_bound = words.peek().copied(); - if next_word_bound.map_or(false, |(i, _)| i == 0) { + if next_word_bound.is_some_and(|(i, _)| i == 0) { next_word_bound = words.next(); } while let Some(grapheme) = iter.peek().copied() { - if next_word_bound.map_or(false, |(i, _)| i == offset) { + if next_word_bound.is_some_and(|(i, _)| i == offset) { break; }; if is_grapheme_whitespace(grapheme) != is_whitespace @@ -19904,18 +21675,22 @@ fn test_word_breaking_tokenizer() { } fn wrap_with_prefix( - line_prefix: String, + first_line_prefix: String, + subsequent_lines_prefix: String, unwrapped_text: String, wrap_column: usize, tab_size: NonZeroU32, preserve_existing_whitespace: bool, ) -> String { - let line_prefix_len = char_len_with_expanded_tabs(0, &line_prefix, tab_size); + let first_line_prefix_len = char_len_with_expanded_tabs(0, &first_line_prefix, tab_size); + let subsequent_lines_prefix_len = + char_len_with_expanded_tabs(0, &subsequent_lines_prefix, tab_size); let mut wrapped_text = String::new(); - let mut current_line = line_prefix.clone(); + let mut current_line = first_line_prefix; + let mut is_first_line = true; let tokenizer = WordBreakingTokenizer::new(&unwrapped_text); - let mut current_line_len = line_prefix_len; + let mut current_line_len = first_line_prefix_len; let mut in_whitespace = false; for token in tokenizer { let have_preceding_whitespace = in_whitespace; @@ -19925,13 +21700,19 @@ fn wrap_with_prefix( grapheme_len, } => { in_whitespace = false; + let current_prefix_len = if is_first_line { + first_line_prefix_len + } else { + subsequent_lines_prefix_len + }; if current_line_len + grapheme_len > wrap_column - && current_line_len != line_prefix_len + && current_line_len != current_prefix_len { wrapped_text.push_str(current_line.trim_end()); wrapped_text.push('\n'); - current_line.truncate(line_prefix.len()); - current_line_len = line_prefix_len; + is_first_line = false; + current_line = subsequent_lines_prefix.clone(); + current_line_len = subsequent_lines_prefix_len; } current_line.push_str(token); current_line_len += grapheme_len; @@ -19948,32 +21729,46 @@ fn wrap_with_prefix( token = " "; grapheme_len = 1; } + let current_prefix_len = if is_first_line { + first_line_prefix_len + } else { + subsequent_lines_prefix_len + }; if current_line_len + grapheme_len > wrap_column { wrapped_text.push_str(current_line.trim_end()); wrapped_text.push('\n'); - current_line.truncate(line_prefix.len()); - current_line_len = line_prefix_len; - } else if current_line_len != line_prefix_len || preserve_existing_whitespace { + is_first_line = false; + current_line = subsequent_lines_prefix.clone(); + current_line_len = subsequent_lines_prefix_len; + } else if current_line_len != current_prefix_len || preserve_existing_whitespace { current_line.push_str(token); current_line_len += grapheme_len; } } WordBreakToken::Newline => { in_whitespace = true; + let current_prefix_len = if is_first_line { + first_line_prefix_len + } else { + subsequent_lines_prefix_len + }; if preserve_existing_whitespace { wrapped_text.push_str(current_line.trim_end()); wrapped_text.push('\n'); - current_line.truncate(line_prefix.len()); - current_line_len = line_prefix_len; + is_first_line = false; + current_line = subsequent_lines_prefix.clone(); + current_line_len = subsequent_lines_prefix_len; } else if have_preceding_whitespace { continue; - } else if current_line_len + 1 > wrap_column && current_line_len != line_prefix_len + } else if current_line_len + 1 > wrap_column + && current_line_len != current_prefix_len { wrapped_text.push_str(current_line.trim_end()); wrapped_text.push('\n'); - current_line.truncate(line_prefix.len()); - current_line_len = line_prefix_len; - } else if current_line_len != line_prefix_len { + is_first_line = false; + current_line = subsequent_lines_prefix.clone(); + current_line_len = subsequent_lines_prefix_len; + } else if current_line_len != current_prefix_len { current_line.push(' '); current_line_len += 1; } @@ -19991,6 +21786,7 @@ fn wrap_with_prefix( fn test_wrap_with_prefix() { assert_eq!( wrap_with_prefix( + "# ".to_string(), "# ".to_string(), "abcdefg".to_string(), 4, @@ -20001,6 +21797,7 @@ fn test_wrap_with_prefix() { ); assert_eq!( wrap_with_prefix( + "".to_string(), "".to_string(), "\thello world".to_string(), 8, @@ -20011,6 +21808,7 @@ fn test_wrap_with_prefix() { ); assert_eq!( wrap_with_prefix( + "// ".to_string(), "// ".to_string(), "xx \nyy zz aa bb cc".to_string(), 12, @@ -20021,6 +21819,7 @@ fn test_wrap_with_prefix() { ); assert_eq!( wrap_with_prefix( + String::new(), String::new(), "这是什么 \n 钢笔".to_string(), 3, @@ -20059,7 +21858,7 @@ pub trait SemanticsProvider { buffer: &Entity, position: text::Anchor, cx: &mut App, - ) -> Option>>; + ) -> Option>>>; fn inline_values( &self, @@ -20098,7 +21897,7 @@ pub trait SemanticsProvider { position: text::Anchor, kind: GotoDefinitionKind, cx: &mut App, - ) -> Option>>>; + ) -> Option>>>>; fn range_for_rename( &self, @@ -20125,15 +21924,17 @@ pub trait CompletionProvider { trigger: CompletionContext, window: &mut Window, cx: &mut Context, - ) -> Task>>>; + ) -> Task>>; fn resolve_completions( &self, - buffer: Entity, - completion_indices: Vec, - completions: Rc>>, - cx: &mut Context, - ) -> Task>; + _buffer: Entity, + _completion_indices: Vec, + _completions: Rc>>, + _cx: &mut Context, + ) -> Task> { + Task::ready(Ok(false)) + } fn apply_additional_edits_for_completion( &self, @@ -20152,6 +21953,7 @@ pub trait CompletionProvider { position: language::Anchor, text: &str, trigger_in_words: bool, + menu_is_open: bool, cx: &mut Context, ) -> bool; @@ -20201,14 +22003,20 @@ impl CodeActionProvider for Entity { cx: &mut App, ) -> Task>> { self.update(cx, |project, cx| { - let code_lens = project.code_lens(buffer, range.clone(), cx); + let code_lens_actions = project.code_lens_actions(buffer, range.clone(), cx); let code_actions = project.code_actions(buffer, range, None, cx); cx.background_spawn(async move { - let (code_lens, code_actions) = join(code_lens, code_actions).await; - Ok(code_lens + let (code_lens_actions, code_actions) = join(code_lens_actions, code_actions).await; + Ok(code_lens_actions .context("code lens fetch")? .into_iter() - .chain(code_actions.context("code action fetch")?) + .flatten() + .chain( + code_actions + .context("code action fetch")? + .into_iter() + .flatten(), + ) .collect()) }) }) @@ -20234,7 +22042,7 @@ fn snippet_completions( buffer: &Entity, buffer_position: text::Anchor, cx: &mut App, -) -> Task>> { +) -> Task> { let languages = buffer.read(cx).languages_at(buffer_position); let snippet_store = project.snippets().read(cx); @@ -20253,7 +22061,10 @@ fn snippet_completions( .collect(); if scopes.is_empty() { - return Task::ready(Ok(vec![])); + return Task::ready(Ok(CompletionResponse { + completions: vec![], + is_incomplete: false, + })); } let snapshot = buffer.read(cx).text_snapshot(); @@ -20263,7 +22074,8 @@ fn snippet_completions( let executor = cx.background_executor().clone(); cx.background_spawn(async move { - let mut all_results: Vec = Vec::new(); + let mut is_incomplete = false; + let mut completions: Vec = Vec::new(); for (scope, snippets) in scopes.into_iter() { let classifier = CharClassifier::new(Some(scope)).for_completion(true); let mut last_word = chars @@ -20273,7 +22085,10 @@ fn snippet_completions( last_word = last_word.chars().rev().collect(); if last_word.is_empty() { - return Ok(vec![]); + return Ok(CompletionResponse { + completions: vec![], + is_incomplete: true, + }); } let as_offset = text::ToOffset::to_offset(&buffer_position, &snapshot); @@ -20290,20 +22105,26 @@ fn snippet_completions( snippet .prefix .iter() - .map(move |prefix| StringMatchCandidate::new(ix, &prefix)) + .map(move |prefix| StringMatchCandidate::new(ix, prefix)) }) .collect::>(); + const MAX_RESULTS: usize = 100; let mut matches = fuzzy::match_strings( &candidates, &last_word, last_word.chars().any(|c| c.is_uppercase()), - 100, + true, + MAX_RESULTS, &Default::default(), executor.clone(), ) .await; + if matches.len() >= MAX_RESULTS { + is_incomplete = true; + } + // Remove all candidates where the query's start does not match the start of any word in the candidate if let Some(query_start) = last_word.chars().next() { matches.retain(|string_match| { @@ -20323,76 +22144,72 @@ fn snippet_completions( .map(|m| m.string) .collect::>(); - let mut result: Vec = snippets - .iter() - .filter_map(|snippet| { - let matching_prefix = snippet - .prefix - .iter() - .find(|prefix| matched_strings.contains(*prefix))?; - let start = as_offset - last_word.len(); - let start = snapshot.anchor_before(start); - let range = start..buffer_position; - let lsp_start = to_lsp(&start); - let lsp_range = lsp::Range { - start: lsp_start, - end: lsp_end, - }; - Some(Completion { - replace_range: range, - new_text: snippet.body.clone(), - source: CompletionSource::Lsp { - insert_range: None, - server_id: LanguageServerId(usize::MAX), - resolved: true, - lsp_completion: Box::new(lsp::CompletionItem { - label: snippet.prefix.first().unwrap().clone(), - kind: Some(CompletionItemKind::SNIPPET), - label_details: snippet.description.as_ref().map(|description| { - lsp::CompletionItemLabelDetails { - detail: Some(description.clone()), - description: None, - } - }), - insert_text_format: Some(InsertTextFormat::SNIPPET), - text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace( - lsp::InsertReplaceEdit { - new_text: snippet.body.clone(), - insert: lsp_range, - replace: lsp_range, - }, - )), - filter_text: Some(snippet.body.clone()), - sort_text: Some(char::MAX.to_string()), - ..lsp::CompletionItem::default() + completions.extend(snippets.iter().filter_map(|snippet| { + let matching_prefix = snippet + .prefix + .iter() + .find(|prefix| matched_strings.contains(*prefix))?; + let start = as_offset - last_word.len(); + let start = snapshot.anchor_before(start); + let range = start..buffer_position; + let lsp_start = to_lsp(&start); + let lsp_range = lsp::Range { + start: lsp_start, + end: lsp_end, + }; + Some(Completion { + replace_range: range, + new_text: snippet.body.clone(), + source: CompletionSource::Lsp { + insert_range: None, + server_id: LanguageServerId(usize::MAX), + resolved: true, + lsp_completion: Box::new(lsp::CompletionItem { + label: snippet.prefix.first().unwrap().clone(), + kind: Some(CompletionItemKind::SNIPPET), + label_details: snippet.description.as_ref().map(|description| { + lsp::CompletionItemLabelDetails { + detail: Some(description.clone()), + description: None, + } }), - lsp_defaults: None, - }, - label: CodeLabel { - text: matching_prefix.clone(), - runs: Vec::new(), - filter_range: 0..matching_prefix.len(), - }, - icon_path: None, - documentation: Some( - CompletionDocumentation::SingleLineAndMultiLinePlainText { - single_line: snippet.name.clone().into(), - plain_text: snippet - .description - .clone() - .map(|description| description.into()), - }, - ), - insert_text_mode: None, - confirm: None, - }) + insert_text_format: Some(InsertTextFormat::SNIPPET), + text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace( + lsp::InsertReplaceEdit { + new_text: snippet.body.clone(), + insert: lsp_range, + replace: lsp_range, + }, + )), + filter_text: Some(snippet.body.clone()), + sort_text: Some(char::MAX.to_string()), + ..lsp::CompletionItem::default() + }), + lsp_defaults: None, + }, + label: CodeLabel { + text: matching_prefix.clone(), + runs: Vec::new(), + filter_range: 0..matching_prefix.len(), + }, + icon_path: None, + documentation: Some(CompletionDocumentation::SingleLineAndMultiLinePlainText { + single_line: snippet.name.clone().into(), + plain_text: snippet + .description + .clone() + .map(|description| description.into()), + }), + insert_text_mode: None, + confirm: None, }) - .collect(); - - all_results.append(&mut result); + })) } - Ok(all_results) + Ok(CompletionResponse { + completions, + is_incomplete, + }) }) } @@ -20405,25 +22222,17 @@ impl CompletionProvider for Entity { options: CompletionContext, _window: &mut Window, cx: &mut Context, - ) -> Task>>> { + ) -> Task>> { self.update(cx, |project, cx| { let snippets = snippet_completions(project, buffer, buffer_position, cx); let project_completions = project.completions(buffer, buffer_position, options, cx); cx.background_spawn(async move { - let snippets_completions = snippets.await?; - match project_completions.await? { - Some(mut completions) => { - completions.extend(snippets_completions); - Ok(Some(completions)) - } - None => { - if snippets_completions.is_empty() { - Ok(None) - } else { - Ok(Some(snippets_completions)) - } - } + let mut responses = project_completions.await?; + let snippets = snippets.await?; + if !snippets.completions.is_empty() { + responses.push(snippets); } + Ok(responses) }) }) } @@ -20469,6 +22278,7 @@ impl CompletionProvider for Entity { position: language::Anchor, text: &str, trigger_in_words: bool, + menu_is_open: bool, cx: &mut Context, ) -> bool { let mut chars = text.chars(); @@ -20483,7 +22293,7 @@ impl CompletionProvider for Entity { let buffer = buffer.read(cx); let snapshot = buffer.snapshot(); - if !snapshot.settings_at(position, cx).show_completions_on_input { + if !menu_is_open && !snapshot.settings_at(position, cx).show_completions_on_input { return false; } let classifier = snapshot.char_classifier_at(position).for_completion(true); @@ -20501,7 +22311,7 @@ impl SemanticsProvider for Entity { buffer: &Entity, position: text::Anchor, cx: &mut App, - ) -> Option>> { + ) -> Option>>> { Some(self.update(cx, |project, cx| project.hover(buffer, position, cx))) } @@ -20522,17 +22332,16 @@ impl SemanticsProvider for Entity { position: text::Anchor, kind: GotoDefinitionKind, cx: &mut App, - ) -> Option>>> { + ) -> Option>>>> { Some(self.update(cx, |project, cx| match kind { - GotoDefinitionKind::Symbol => project.definition(&buffer, position, cx), - GotoDefinitionKind::Declaration => project.declaration(&buffer, position, cx), - GotoDefinitionKind::Type => project.type_definition(&buffer, position, cx), - GotoDefinitionKind::Implementation => project.implementation(&buffer, position, cx), + GotoDefinitionKind::Symbol => project.definitions(buffer, position, cx), + GotoDefinitionKind::Declaration => project.declarations(buffer, position, cx), + GotoDefinitionKind::Type => project.type_definitions(buffer, position, cx), + GotoDefinitionKind::Implementation => project.implementations(buffer, position, cx), })) } fn supports_inlay_hints(&self, buffer: &Entity, cx: &mut App) -> bool { - // TODO: make this work for remote projects self.update(cx, |project, cx| { if project .active_debug_session(cx) @@ -20550,7 +22359,6 @@ impl SemanticsProvider for Entity { fn inline_values( &self, buffer_handle: Entity, - range: Range, cx: &mut App, ) -> Option>>> { @@ -20601,7 +22409,7 @@ impl SemanticsProvider for Entity { // Fallback on using TreeSitter info to determine identifier range buffer.read_with(cx, |buffer, _| { let snapshot = buffer.snapshot(); - let (range, kind) = snapshot.surrounding_word(position); + let (range, kind) = snapshot.surrounding_word(position, false); if kind != Some(CharKind::Word) { return None; } @@ -20646,7 +22454,7 @@ fn consume_contiguous_rows( selections: &mut Peekable>>, ) -> (MultiBufferRow, MultiBufferRow) { contiguous_row_selections.push(selection.clone()); - let start_row = MultiBufferRow(selection.start.row); + let start_row = starting_row(selection, display_map); let mut end_row = ending_row(selection, display_map); while let Some(next_selection) = selections.peek() { @@ -20660,6 +22468,14 @@ fn consume_contiguous_rows( (start_row, end_row) } +fn starting_row(selection: &Selection, display_map: &DisplaySnapshot) -> MultiBufferRow { + if selection.start.column > 0 { + MultiBufferRow(display_map.prev_line_boundary(selection.start).0.row) + } else { + MultiBufferRow(selection.start.row) + } +} + fn ending_row(next_selection: &Selection, display_map: &DisplaySnapshot) -> MultiBufferRow { if next_selection.end.column > 0 || next_selection.is_empty() { MultiBufferRow(display_map.next_line_boundary(next_selection.end).0.row + 1) @@ -20827,8 +22643,8 @@ impl EditorSnapshot { return None; } - let em_width = cx.text_system().em_width(font_id, font_size).log_err()?; - let em_advance = cx.text_system().em_advance(font_id, font_size).log_err()?; + let ch_width = cx.text_system().ch_width(font_id, font_size).log_err()?; + let ch_advance = cx.text_system().ch_advance(font_id, font_size).log_err()?; let show_git_gutter = self.show_git_diff_gutter.unwrap_or_else(|| { matches!( @@ -20841,8 +22657,10 @@ impl EditorSnapshot { .show_line_numbers .unwrap_or(gutter_settings.line_numbers); let line_gutter_width = if show_line_numbers { - // Avoid flicker-like gutter resizes when the line number gains another digit and only resize the gutter on files with N*10^5 lines. - let min_width_for_number_on_gutter = em_advance * MIN_LINE_NUMBER_DIGITS as f32; + // Avoid flicker-like gutter resizes when the line number gains another digit by + // only resizing the gutter on files with > 10**min_line_number_digits lines. + let min_width_for_number_on_gutter = + ch_advance * gutter_settings.min_line_number_digits as f32; max_line_number_width.max(min_width_for_number_on_gutter) } else { 0.0.into() @@ -20865,20 +22683,20 @@ impl EditorSnapshot { + MAX_RELATIVE_TIMESTAMP.len() + SPACING_WIDTH; - em_advance * max_char_count + ch_advance * max_char_count }); let is_singleton = self.buffer_snapshot.is_singleton(); let mut left_padding = git_blame_entries_width.unwrap_or(Pixels::ZERO); left_padding += if !is_singleton { - em_width * 4.0 + ch_width * 4.0 } else if show_runnables || show_breakpoints { - em_width * 3.0 + ch_width * 3.0 } else if show_git_gutter && show_line_numbers { - em_width * 2.0 + ch_width * 2.0 } else if show_git_gutter || show_line_numbers { - em_width + ch_width } else { px(0.) }; @@ -20886,11 +22704,11 @@ impl EditorSnapshot { let shows_folds = is_singleton && gutter_settings.folds; let right_padding = if shows_folds && show_line_numbers { - em_width * 4.0 + ch_width * 4.0 } else if shows_folds || (!is_singleton && show_line_numbers) { - em_width * 3.0 + ch_width * 3.0 } else if show_line_numbers { - em_width + ch_width } else { px(0.) }; @@ -21050,6 +22868,7 @@ pub enum EditorEvent { }, Reloaded, CursorShapeChanged, + BreadcrumbsChanged, PushedToNavHistory { anchor: Anchor, is_deactivate: bool, @@ -21069,7 +22888,7 @@ impl Render for Editor { let settings = ThemeSettings::get_global(cx); let mut text_style = match self.mode { - EditorMode::SingleLine { .. } | EditorMode::AutoHeight { .. } => TextStyle { + EditorMode::SingleLine | EditorMode::AutoHeight { .. } => TextStyle { color: cx.theme().colors().editor_foreground, font_family: settings.ui_font.family.clone(), font_features: settings.ui_font.features.clone(), @@ -21095,8 +22914,8 @@ impl Render for Editor { } let background = match self.mode { - EditorMode::SingleLine { .. } => cx.theme().system().transparent, - EditorMode::AutoHeight { max_lines: _ } => cx.theme().system().transparent, + EditorMode::SingleLine => cx.theme().system().transparent, + EditorMode::AutoHeight { .. } => cx.theme().system().transparent, EditorMode::Full { .. } => cx.theme().colors().editor_background, EditorMode::Minimap { .. } => cx.theme().colors().editor_background.opacity(0.7), }; @@ -21105,15 +22924,16 @@ impl Render for Editor { &cx.entity(), EditorStyle { background, + border: cx.theme().colors().border, local_player: cx.theme().players().local(), text: text_style, scrollbar_width: EditorElement::SCROLLBAR_WIDTH, syntax: cx.theme().syntax().clone(), status: cx.theme().status().clone(), inlay_hints_style: make_inlay_hints_style(cx), - inline_completion_styles: make_suggestion_styles(cx), + edit_prediction_styles: make_suggestion_styles(cx), unnecessary_code_fade: ThemeSettings::get_global(cx).unnecessary_code_fade, - show_underlines: !self.mode.is_minimap(), + show_underlines: self.diagnostics_enabled(), }, ) } @@ -21212,7 +23032,7 @@ impl EntityInputHandler for Editor { }); if let Some(new_selected_ranges) = new_selected_ranges { - this.change_selections(None, window, cx, |selections| { + this.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| { selections.select_ranges(new_selected_ranges) }); this.backspace(&Default::default(), window, cx); @@ -21287,7 +23107,9 @@ impl EntityInputHandler for Editor { }); if let Some(ranges) = ranges_to_replace { - this.change_selections(None, window, cx, |s| s.select_ranges(ranges)); + this.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges(ranges) + }); } let marked_ranges = { @@ -21341,7 +23163,7 @@ impl EntityInputHandler for Editor { .collect::>(); drop(snapshot); - this.change_selections(None, window, cx, |selections| { + this.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| { selections.select_ranges(new_selected_ranges) }); } @@ -21367,19 +23189,19 @@ impl EntityInputHandler for Editor { cx: &mut Context, ) -> Option> { let text_layout_details = self.text_layout_details(window); - let gpui::Size { - width: em_width, - height: line_height, - } = self.character_size(window); + let CharacterDimensions { + em_width, + em_advance, + line_height, + } = self.character_dimensions(window); let snapshot = self.snapshot(window, cx); let scroll_position = snapshot.scroll_position(); - let scroll_left = scroll_position.x * em_width; + let scroll_left = scroll_position.x * em_advance; let start = OffsetUtf16(range_utf16.start).to_display_point(&snapshot); let x = snapshot.x_for_display_point(start, &text_layout_details) - scroll_left - + self.gutter_dimensions.width - + self.gutter_dimensions.margin; + + self.gutter_dimensions.full_width(); let y = line_height * (start.row().as_f32() - scroll_position.y); Some(Bounds { @@ -21504,7 +23326,7 @@ impl InvalidationRegion for SnippetState { } } -fn inline_completion_edit_text( +fn edit_prediction_edit_text( current_snapshot: &BufferSnapshot, edits: &[(Range, String)], edit_preview: &EditPreview, @@ -21524,6 +23346,33 @@ fn inline_completion_edit_text( edit_preview.highlight_edits(current_snapshot, &edits, include_deletions, cx) } +fn edit_prediction_fallback_text(edits: &[(Range, String)], cx: &App) -> HighlightedText { + // Fallback for providers that don't provide edit_preview (like Copilot/Supermaven) + // Just show the raw edit text with basic styling + let mut text = String::new(); + let mut highlights = Vec::new(); + + let insertion_highlight_style = HighlightStyle { + color: Some(cx.theme().colors().text), + ..Default::default() + }; + + for (_, edit_text) in edits { + let start_offset = text.len(); + text.push_str(edit_text); + let end_offset = text.len(); + + if start_offset < end_offset { + highlights.push((start_offset..end_offset, insertion_highlight_style)); + } + } + + HighlightedText { + text: text.into(), + highlights, + } +} + pub fn diagnostic_style(severity: lsp::DiagnosticSeverity, colors: &StatusColors) -> Hsla { match severity { lsp::DiagnosticSeverity::ERROR => colors.error, @@ -21754,7 +23603,8 @@ impl BreakpointPromptEditor { let prompt = cx.new(|cx| { let mut prompt = Editor::new( EditorMode::AutoHeight { - max_lines: Self::MAX_LINES as usize, + min_lines: 1, + max_lines: Some(Self::MAX_LINES as usize), }, buffer, None, @@ -21896,7 +23746,7 @@ fn all_edits_insertions_or_deletions( let mut all_deletions = true; for (range, new_text) in edits.iter() { - let range_is_empty = range.to_offset(&snapshot).is_empty(); + let range_is_empty = range.to_offset(snapshot).is_empty(); let text_is_empty = new_text.is_empty(); if range_is_empty != text_is_empty { @@ -21958,6 +23808,12 @@ pub struct LineHighlight { pub type_id: Option, } +struct LineManipulationResult { + pub new_text: String, + pub line_count_before: usize, + pub line_count_after: usize, +} + fn render_diff_hunk_controls( row: u32, status: &DiffHunkStatus, diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index 57459dfc94..1d7e04cae0 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -1,3 +1,6 @@ +use core::num; +use std::num::NonZeroU32; + use gpui::App; use language::CursorShape; use project::project_settings::DiagnosticSeverity; @@ -17,6 +20,7 @@ pub struct EditorSettings { pub lsp_highlight_debounce: u64, pub hover_popover_enabled: bool, pub hover_popover_delay: u64, + pub status_bar: StatusBar, pub toolbar: Toolbar, pub scrollbar: Scrollbar, pub minimap: Minimap, @@ -49,6 +53,23 @@ pub struct EditorSettings { #[serde(default)] pub diagnostics_max_severity: Option, pub inline_code_actions: bool, + pub drag_and_drop_selection: DragAndDropSelection, + pub lsp_document_colors: DocumentColorsRenderMode, +} + +/// How to render LSP `textDocument/documentColor` colors in the editor. +#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum DocumentColorsRenderMode { + /// Do not query and render document colors. + None, + /// Render document colors as inlay hints near the color text. + #[default] + Inlay, + /// Draw a border around the color text. + Border, + /// Draw a background behind the color text. + Background, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] @@ -105,6 +126,18 @@ pub struct JupyterContent { pub enabled: Option, } +#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +pub struct StatusBar { + /// Whether to display the active language button in the status bar. + /// + /// Default: true + pub active_language_button: bool, + /// Whether to show the cursor position button in the status bar. + /// + /// Default: true + pub cursor_position_button: bool, +} + #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] pub struct Toolbar { pub breadcrumbs: bool, @@ -129,9 +162,11 @@ pub struct Scrollbar { #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] pub struct Minimap { pub show: ShowMinimap, + pub display_in: DisplayIn, pub thumb: MinimapThumb, pub thumb_border: MinimapThumbBorder, pub current_line_highlight: Option, + pub max_width_columns: num::NonZeroU32, } impl Minimap { @@ -139,6 +174,11 @@ impl Minimap { self.show != ShowMinimap::Never } + #[inline] + pub fn on_active_editor(&self) -> bool { + self.display_in == DisplayIn::ActiveEditor + } + pub fn with_show_override(self) -> Self { Self { show: ShowMinimap::Always, @@ -149,6 +189,7 @@ impl Minimap { #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] pub struct Gutter { + pub min_line_number_digits: usize, pub line_numbers: bool, pub runnables: bool, pub breakpoints: bool, @@ -187,6 +228,19 @@ pub enum ShowMinimap { Never, } +/// Where to show the minimap in the editor. +/// +/// Default: all_editors +#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum DisplayIn { + /// Show on all open editors. + AllEditors, + /// Show the minimap on the active editor only. + #[default] + ActiveEditor, +} + /// When to show the minimap thumb. /// /// Default: always @@ -234,6 +288,26 @@ pub struct ScrollbarAxes { pub vertical: bool, } +/// Whether to allow drag and drop text selection in buffer. +#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +pub struct DragAndDropSelection { + /// When true, enables drag and drop text selection in buffer. + /// + /// Default: true + #[serde(default = "default_true")] + pub enabled: bool, + + /// The delay in milliseconds that must elapse before drag and drop is allowed. Otherwise, a new text selection is created. + /// + /// Default: 300 + #[serde(default = "default_drag_and_drop_selection_delay_ms")] + pub delay: u64, +} + +fn default_drag_and_drop_selection_delay_ms() -> u64 { + 300 +} + /// Which diagnostic indicators to show in the scrollbar. /// /// Default: all @@ -334,10 +408,11 @@ pub enum SnippetSortOrder { Inline, /// Place snippets at the bottom of the completion list Bottom, + /// Do not show snippets in the completion list + None, } #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] -#[schemars(deny_unknown_fields)] pub struct EditorSettingsContent { /// Whether the cursor blinks in the editor. /// @@ -378,6 +453,8 @@ pub struct EditorSettingsContent { /// /// Default: 300 pub hover_popover_delay: Option, + /// Status bar related settings + pub status_bar: Option, /// Toolbar related settings pub toolbar: Option, /// Scrollbar related settings @@ -422,7 +499,7 @@ pub struct EditorSettingsContent { /// Default: always pub seed_search_query_from_cursor: Option, pub use_smartcase_search: Option, - /// The key to use for adding multiple cursors + /// Determines the modifier to be used to add multiple cursors with the mouse. The open hover link mouse gestures will adapt such that it do not conflict with the multicursor modifier. /// /// Default: alt pub multi_cursor_modifier: Option, @@ -495,6 +572,27 @@ pub struct EditorSettingsContent { /// /// Default: true pub inline_code_actions: Option, + + /// Drag and drop related settings + pub drag_and_drop_selection: Option, + + /// How to render LSP `textDocument/documentColor` colors in the editor. + /// + /// Default: [`DocumentColorsRenderMode::Inlay`] + pub lsp_document_colors: Option, +} + +// Status bar related settings +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +pub struct StatusBarContent { + /// Whether to display the active language button in the status bar. + /// + /// Default: true + pub active_language_button: Option, + /// Whether to show the cursor position button in the status bar. + /// + /// Default: true + pub cursor_position_button: Option, } // Toolbar related settings @@ -566,6 +664,11 @@ pub struct MinimapContent { /// Default: never pub show: Option, + /// Where to show the minimap in the editor. + /// + /// Default: [`DisplayIn::ActiveEditor`] + pub display_in: Option, + /// When to show the minimap thumb. /// /// Default: always @@ -580,6 +683,11 @@ pub struct MinimapContent { /// /// Default: inherits editor line highlights setting pub current_line_highlight: Option>, + + /// Maximum number of columns to display in the minimap. + /// + /// Default: 80 + pub max_width_columns: Option, } /// Forcefully enable or disable the scrollbar for each axis @@ -603,6 +711,10 @@ pub struct GutterContent { /// /// Default: true pub line_numbers: Option, + /// Minimum number of characters to reserve space for in the gutter. + /// + /// Default: 4 + pub min_line_number_digits: Option, /// Whether to show runnable buttons in the gutter. /// /// Default: true @@ -698,10 +810,8 @@ impl Settings for EditorSettings { if gutter.line_numbers.is_some() { old_gutter.line_numbers = gutter.line_numbers } - } else { - if gutter != GutterContent::default() { - current.gutter = Some(gutter) - } + } else if gutter != GutterContent::default() { + current.gutter = Some(gutter) } if let Some(b) = vscode.read_bool("editor.scrollBeyondLastLine") { current.scroll_beyond_last_line = Some(if b { @@ -798,6 +908,8 @@ impl Settings for EditorSettings { let mut minimap = MinimapContent::default(); let minimap_enabled = vscode.read_bool("editor.minimap.enabled").unwrap_or(true); let autohide = vscode.read_bool("editor.minimap.autohide"); + let mut max_width_columns: Option = None; + vscode.u32_setting("editor.minimap.maxColumn", &mut max_width_columns); if minimap_enabled { if let Some(false) = autohide { minimap.show = Some(ShowMinimap::Always); @@ -807,6 +919,9 @@ impl Settings for EditorSettings { } else { minimap.show = Some(ShowMinimap::Never); } + if let Some(max_width_columns) = max_width_columns { + minimap.max_width_columns = NonZeroU32::new(max_width_columns); + } vscode.enum_setting( "editor.minimap.showSlider", diff --git a/crates/editor/src/editor_settings_controls.rs b/crates/editor/src/editor_settings_controls.rs index 54bb865520..91022d94a8 100644 --- a/crates/editor/src/editor_settings_controls.rs +++ b/crates/editor/src/editor_settings_controls.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use gpui::{App, FontFeatures, FontWeight}; use project::project_settings::{InlineBlameSettings, ProjectSettings}; use settings::{EditableSettingControl, Settings}; -use theme::{FontFamilyCache, ThemeSettings}; +use theme::{FontFamilyCache, FontFamilyName, ThemeSettings}; use ui::{ CheckboxWithLabel, ContextMenu, DropdownMenu, NumericStepper, SettingsContainer, SettingsGroup, prelude::*, @@ -75,7 +75,7 @@ impl EditableSettingControl for BufferFontFamilyControl { value: Self::Value, _cx: &App, ) { - settings.buffer_font_family = Some(value.to_string()); + settings.buffer_font_family = Some(FontFamilyName(value.into())); } } @@ -88,7 +88,7 @@ impl RenderOnce for BufferFontFamilyControl { .child(Icon::new(IconName::Font)) .child(DropdownMenu::new( "buffer-font-family", - value.clone(), + value, ContextMenu::build(window, cx, |mut menu, _, cx| { let font_family_cache = FontFamilyCache::global(cx); diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index c7af25f48d..2cfdb92593 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -1,7 +1,8 @@ use super::*; use crate::{ JoinLines, - inline_completion_tests::FakeInlineCompletionProvider, + code_context_menus::CodeContextMenu, + edit_prediction_tests::FakeEditPredictionProvider, linked_editing_ranges::LinkedEditingRanges, scroll::scroll_amount::ScrollAmount, test::{ @@ -14,22 +15,22 @@ use crate::{ use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind}; use futures::StreamExt; use gpui::{ - BackgroundExecutor, DismissEvent, SemanticVersion, TestAppContext, UpdateGlobal, + BackgroundExecutor, DismissEvent, Rgba, SemanticVersion, TestAppContext, UpdateGlobal, VisualTestContext, WindowBounds, WindowOptions, div, }; use indoc::indoc; use language::{ BracketPairConfig, Capability::ReadWrite, - FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, LanguageName, - Override, Point, + DiagnosticSourceKind, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, + LanguageName, Override, Point, language_settings::{ - AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings, - LanguageSettingsContent, LspInsertMode, PrettierSettings, + AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings, FormatterList, + LanguageSettingsContent, LspInsertMode, PrettierSettings, SelectedFormatter, }, tree_sitter_python, }; -use language_settings::{Formatter, FormatterList, IndentGuideSettings}; +use language_settings::{Formatter, IndentGuideSettings}; use lsp::CompletionParams; use multi_buffer::{IndentGuide, PathKey}; use parking_lot::Mutex; @@ -54,8 +55,11 @@ use util::{ uri, }; use workspace::{ - CloseActiveItem, CloseAllItems, CloseInactiveItems, NavigationEntry, OpenOptions, ViewId, - item::{FollowEvent, FollowableItem, Item, ItemHandle}, + CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry, + OpenOptions, ViewId, + invalid_buffer_view::InvalidBufferView, + item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions}, + register_project_item, }; #[gpui::test] @@ -72,7 +76,7 @@ fn test_edit_events(cx: &mut TestAppContext) { let editor1 = cx.add_window({ let events = events.clone(); |window, cx| { - let entity = cx.entity().clone(); + let entity = cx.entity(); cx.subscribe_in( &entity, window, @@ -93,7 +97,7 @@ fn test_edit_events(cx: &mut TestAppContext) { let events = events.clone(); |window, cx| { cx.subscribe_in( - &cx.entity().clone(), + &cx.entity(), window, move |_, _, event: &EditorEvent, _, _| match event { EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")), @@ -178,7 +182,9 @@ fn test_edit_events(cx: &mut TestAppContext) { // No event is emitted when the mutation is a no-op. _ = editor2.update(cx, |editor, window, cx| { - editor.change_selections(None, window, cx, |s| s.select_ranges([0..0])); + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges([0..0]) + }); editor.backspace(&Backspace, window, cx); }); @@ -201,7 +207,9 @@ fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) { _ = editor.update(cx, |editor, window, cx| { editor.start_transaction_at(now, window, cx); - editor.change_selections(None, window, cx, |s| s.select_ranges([2..4])); + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges([2..4]) + }); editor.insert("cd", window, cx); editor.end_transaction_at(now, cx); @@ -209,14 +217,18 @@ fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) { assert_eq!(editor.selections.ranges(cx), vec![4..4]); editor.start_transaction_at(now, window, cx); - editor.change_selections(None, window, cx, |s| s.select_ranges([4..5])); + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges([4..5]) + }); editor.insert("e", window, cx); editor.end_transaction_at(now, cx); assert_eq!(editor.text(cx), "12cde6"); assert_eq!(editor.selections.ranges(cx), vec![5..5]); now += group_interval + Duration::from_millis(1); - editor.change_selections(None, window, cx, |s| s.select_ranges([2..2])); + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges([2..2]) + }); // Simulate an edit in another editor buffer.update(cx, |buffer, cx| { @@ -324,7 +336,7 @@ fn test_ime_composition(cx: &mut TestAppContext) { assert_eq!(editor.marked_text_ranges(cx), None); // Start a new IME composition with multiple cursors. - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([ OffsetUtf16(1)..OffsetUtf16(1), OffsetUtf16(3)..OffsetUtf16(3), @@ -622,7 +634,7 @@ fn test_clone(cx: &mut TestAppContext) { }); _ = editor.update(cx, |editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges(selection_ranges.clone()) }); editor.fold_creases( @@ -698,7 +710,7 @@ async fn test_navigation_history(cx: &mut TestAppContext) { _ = workspace.update(cx, |_v, window, cx| { cx.new(|cx| { let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx); - let mut editor = build_editor(buffer.clone(), window, cx); + let mut editor = build_editor(buffer, window, cx); let handle = cx.entity(); editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle))); @@ -708,12 +720,12 @@ async fn test_navigation_history(cx: &mut TestAppContext) { // Move the cursor a small distance. // Nothing is added to the navigation history. - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0) ]) }); - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0) ]) @@ -722,7 +734,7 @@ async fn test_navigation_history(cx: &mut TestAppContext) { // Move the cursor a large distance. // The history can jump back to the previous position. - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3) ]) @@ -888,11 +900,11 @@ fn test_fold_action(cx: &mut TestAppContext) { .unindent(), cx, ); - build_editor(buffer.clone(), window, cx) + build_editor(buffer, window, cx) }); _ = editor.update(cx, |editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0) ]); @@ -979,11 +991,11 @@ fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) { .unindent(), cx, ); - build_editor(buffer.clone(), window, cx) + build_editor(buffer, window, cx) }); _ = editor.update(cx, |editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0) ]); @@ -1064,11 +1076,11 @@ fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) { .unindent(), cx, ); - build_editor(buffer.clone(), window, cx) + build_editor(buffer, window, cx) }); _ = editor.update(cx, |editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0) ]); @@ -1163,7 +1175,7 @@ fn test_fold_at_level(cx: &mut TestAppContext) { .unindent(), cx, ); - build_editor(buffer.clone(), window, cx) + build_editor(buffer, window, cx) }); _ = editor.update(cx, |editor, window, cx| { @@ -1300,7 +1312,7 @@ fn test_move_cursor(cx: &mut TestAppContext) { &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)] ); - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2) ]); @@ -1325,7 +1337,7 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) { let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx); - build_editor(buffer.clone(), window, cx) + build_editor(buffer, window, cx) }); assert_eq!('🟥'.len_utf8(), 4); @@ -1442,10 +1454,10 @@ fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) { let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx); - build_editor(buffer.clone(), window, cx) + build_editor(buffer, window, cx) }); _ = editor.update(cx, |editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]); }); @@ -1535,7 +1547,7 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) { build_editor(buffer, window, cx) }); _ = editor.update(cx, |editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1), DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4), @@ -1730,7 +1742,7 @@ fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) { // First, let's assert behavior on the first line, that was not soft-wrapped. // Start the cursor at the `k` on the first line - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7) ]); @@ -1752,7 +1764,7 @@ fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) { // Now, let's assert behavior on the second line, that ended up being soft-wrapped. // Start the cursor at the last line (`y` that was wrapped to a new line) - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0) ]); @@ -1818,7 +1830,7 @@ fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) { }); _ = editor.update(cx, |editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1), DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4), @@ -1891,6 +1903,51 @@ fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) { }); } +#[gpui::test] +fn test_beginning_of_line_with_cursor_between_line_start_and_indent(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let move_to_beg = MoveToBeginningOfLine { + stop_at_soft_wraps: true, + stop_at_indent: true, + }; + + let editor = cx.add_window(|window, cx| { + let buffer = MultiBuffer::build_simple(" hello\nworld", cx); + build_editor(buffer, window, cx) + }); + + _ = editor.update(cx, |editor, window, cx| { + // test cursor between line_start and indent_start + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3) + ]); + }); + + // cursor should move to line_start + editor.move_to_beginning_of_line(&move_to_beg, window, cx); + assert_eq!( + editor.selections.display_ranges(cx), + &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)] + ); + + // cursor should move to indent_start + editor.move_to_beginning_of_line(&move_to_beg, window, cx); + assert_eq!( + editor.selections.display_ranges(cx), + &[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 4)] + ); + + // cursor should move to back to line_start + editor.move_to_beginning_of_line(&move_to_beg, window, cx); + assert_eq!( + editor.selections.display_ranges(cx), + &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)] + ); + }); +} + #[gpui::test] fn test_prev_next_word_boundary(cx: &mut TestAppContext) { init_test(cx, |_| {}); @@ -1900,55 +1957,54 @@ fn test_prev_next_word_boundary(cx: &mut TestAppContext) { build_editor(buffer, window, cx) }); _ = editor.update(cx, |editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11), DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4), ]) }); - editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx); assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx); editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx); - assert_selection_ranges("use stdˇ::str::{foo, bar}\n\n ˇ{baz.qux()}", editor, cx); + assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx); editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx); - assert_selection_ranges("use ˇstd::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx); - - editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx); - assert_selection_ranges("ˇuse std::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx); + assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx); editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx); assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx); - editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx); - assert_selection_ranges("useˇ std::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx); + editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx); + assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx); editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx); - assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx); + assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx); editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx); - assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx); + assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx); + + editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx); + assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx); editor.move_right(&MoveRight, window, cx); editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx); assert_selection_ranges( - "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}", + "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}", editor, cx, ); editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx); assert_selection_ranges( - "use std«ˇ::s»tr::{foo, bar}\n\n «ˇ{b»az.qux()}", + "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}", editor, cx, ); editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx); assert_selection_ranges( - "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}", + "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}", editor, cx, ); @@ -1971,7 +2027,7 @@ fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) { "use one::{\n two::three::\n four::five\n};" ); - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7) ]); @@ -2234,7 +2290,7 @@ async fn test_autoscroll(cx: &mut TestAppContext) { // on screen, the editor autoscrolls to reveal the newest cursor, and // allows the vertical scroll margin below that cursor. cx.update_editor(|editor, window, cx| { - editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| { + editor.change_selections(Default::default(), window, cx, |selections| { selections.select_ranges([ Point::new(0, 0)..Point::new(0, 0), Point::new(6, 0)..Point::new(6, 0), @@ -2262,7 +2318,7 @@ async fn test_autoscroll(cx: &mut TestAppContext) { // Add a cursor above the visible area. Since both cursors fit on screen, // the editor scrolls to show both. cx.update_editor(|editor, window, cx| { - editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| { + editor.change_selections(Default::default(), window, cx, |selections| { selections.select_ranges([ Point::new(1, 0)..Point::new(1, 0), Point::new(6, 0)..Point::new(6, 0), @@ -2425,11 +2481,11 @@ fn test_delete_to_word_boundary(cx: &mut TestAppContext) { let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("one two three four", cx); - build_editor(buffer.clone(), window, cx) + build_editor(buffer, window, cx) }); _ = editor.update(cx, |editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_display_ranges([ // an empty selection - the preceding word fragment is deleted DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2), @@ -2448,7 +2504,7 @@ fn test_delete_to_word_boundary(cx: &mut TestAppContext) { }); _ = editor.update(cx, |editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_display_ranges([ // an empty selection - the following word fragment is deleted DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3), @@ -2473,7 +2529,7 @@ fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) { let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx); - build_editor(buffer.clone(), window, cx) + build_editor(buffer, window, cx) }); let del_to_prev_word_start = DeleteToPreviousWordStart { ignore_newlines: false, @@ -2483,7 +2539,7 @@ fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) { }; _ = editor.update(cx, |editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1) ]) @@ -2509,7 +2565,7 @@ fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) { let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx); - build_editor(buffer.clone(), window, cx) + build_editor(buffer, window, cx) }); let del_to_next_word_end = DeleteToNextWordEnd { ignore_newlines: false, @@ -2519,7 +2575,7 @@ fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) { }; _ = editor.update(cx, |editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0) ]) @@ -2554,11 +2610,11 @@ fn test_newline(cx: &mut TestAppContext) { let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx); - build_editor(buffer.clone(), window, cx) + build_editor(buffer, window, cx) }); _ = editor.update(cx, |editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2), DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2), @@ -2590,8 +2646,8 @@ fn test_newline_with_old_selections(cx: &mut TestAppContext) { .as_str(), cx, ); - let mut editor = build_editor(buffer.clone(), window, cx); - editor.change_selections(None, window, cx, |s| { + let mut editor = build_editor(buffer, window, cx); + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([ Point::new(2, 4)..Point::new(2, 5), Point::new(5, 4)..Point::new(5, 5), @@ -2866,11 +2922,11 @@ async fn test_newline_documentation_comments(cx: &mut TestAppContext) { let language = Arc::new( Language::new( LanguageConfig { - documentation: Some(language::DocumentationConfig { + documentation_comment: Some(language::BlockCommentConfig { start: "/**".into(), end: "*/".into(), prefix: "* ".into(), - tab_size: NonZeroU32::new(1).unwrap(), + tab_size: 1, }), ..LanguageConfig::default() @@ -3071,14 +3127,58 @@ async fn test_newline_documentation_comments(cx: &mut TestAppContext) { "}); } +#[gpui::test] +async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) { + init_test(cx, |settings| { + settings.defaults.tab_size = NonZeroU32::new(4) + }); + + let lua_language = Arc::new(Language::new( + LanguageConfig { + line_comments: vec!["--".into()], + block_comment: Some(language::BlockCommentConfig { + start: "--[[".into(), + prefix: "".into(), + end: "]]".into(), + tab_size: 0, + }), + ..LanguageConfig::default() + }, + None, + )); + + let mut cx = EditorTestContext::new(cx).await; + cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx)); + + // Line with line comment should extend + cx.set_state(indoc! {" + --ˇ + "}); + cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx)); + cx.assert_editor_state(indoc! {" + -- + --ˇ + "}); + + // Line with block comment that matches line comment should not extend + cx.set_state(indoc! {" + --[[ˇ + "}); + cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx)); + cx.assert_editor_state(indoc! {" + --[[ + ˇ + "}); +} + #[gpui::test] fn test_insert_with_old_selections(cx: &mut TestAppContext) { init_test(cx, |_| {}); let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx); - let mut editor = build_editor(buffer.clone(), window, cx); - editor.change_selections(None, window, cx, |s| { + let mut editor = build_editor(buffer, window, cx); + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([3..4, 11..12, 19..20]) }); editor @@ -3459,6 +3559,70 @@ async fn test_indent_outdent(cx: &mut TestAppContext) { "}); } +#[gpui::test] +async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) { + // This is a regression test for issue #33761 + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into()); + cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx)); + + cx.set_state( + r#"ˇ# ingress: +ˇ# api: +ˇ# enabled: false +ˇ# pathType: Prefix +ˇ# console: +ˇ# enabled: false +ˇ# pathType: Prefix +"#, + ); + + // Press tab to indent all lines + cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx)); + + cx.assert_editor_state( + r#" ˇ# ingress: + ˇ# api: + ˇ# enabled: false + ˇ# pathType: Prefix + ˇ# console: + ˇ# enabled: false + ˇ# pathType: Prefix +"#, + ); +} + +#[gpui::test] +async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) { + // This is a test to make sure our fix for issue #33761 didn't break anything + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into()); + cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx)); + + cx.set_state( + r#"ˇingress: +ˇ api: +ˇ enabled: false +ˇ pathType: Prefix +"#, + ); + + // Press tab to indent all lines + cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx)); + + cx.assert_editor_state( + r#"ˇingress: + ˇapi: + ˇenabled: false + ˇpathType: Prefix +"#, + ); +} + #[gpui::test] async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) { init_test(cx, |settings| { @@ -3558,7 +3722,7 @@ async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) { #[gpui::test] fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) { init_test(cx, |settings| { - settings.languages.extend([ + settings.languages.0.extend([ ( "TOML".into(), LanguageSettingsContent { @@ -3727,7 +3891,7 @@ fn test_delete_line(cx: &mut TestAppContext) { build_editor(buffer, window, cx) }); _ = editor.update(cx, |editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1), DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1), @@ -3750,7 +3914,7 @@ fn test_delete_line(cx: &mut TestAppContext) { build_editor(buffer, window, cx) }); _ = editor.update(cx, |editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1) ]) @@ -3787,7 +3951,7 @@ fn test_join_lines_with_single_selection(cx: &mut TestAppContext) { ); // When multiple lines are selected, remove newlines that are spanned by the selection - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([Point::new(0, 5)..Point::new(2, 2)]) }); editor.join_lines(&JoinLines, window, cx); @@ -3806,7 +3970,7 @@ fn test_join_lines_with_single_selection(cx: &mut TestAppContext) { ); // When joining an empty line don't insert a space - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([Point::new(2, 1)..Point::new(2, 2)]) }); editor.join_lines(&JoinLines, window, cx); @@ -3846,7 +4010,7 @@ fn test_join_lines_with_single_selection(cx: &mut TestAppContext) { // We remove any leading spaces assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td"); - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([Point::new(0, 1)..Point::new(0, 1)]) }); editor.join_lines(&JoinLines, window, cx); @@ -3873,7 +4037,7 @@ fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) { let mut editor = build_editor(buffer.clone(), window, cx); let buffer = buffer.read(cx).as_singleton().unwrap(); - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([ Point::new(0, 2)..Point::new(1, 1), Point::new(1, 2)..Point::new(1, 2), @@ -3976,7 +4140,7 @@ async fn test_custom_newlines_cause_no_false_positive_diffs( } #[gpui::test] -async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) { +async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) { init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx).await; @@ -4002,6 +4166,29 @@ async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) { Zˇ» "}); + // Test sort_lines_by_length() + // + // Demonstrates: + // - ∞ is 3 bytes UTF-8, but sorted by its char count (1) + // - sort is stable + cx.set_state(indoc! {" + «123 + æ + 12 + ∞ + 1 + æˇ» + "}); + cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx)); + cx.assert_editor_state(indoc! {" + «æ + ∞ + 1 + æ + 12 + 123ˇ» + "}); + // Test reverse_lines() cx.set_state(indoc! {" «5 @@ -4021,8 +4208,8 @@ async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) { // Skip testing shuffle_line() - // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive() - // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines) + // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive() + // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines) // Don't manipulate when cursor is on single line, but expand the selection cx.set_state(indoc! {" @@ -4089,7 +4276,7 @@ async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) { bbˇ»b "}); cx.update_editor(|e, window, cx| { - e.manipulate_lines(window, cx, |lines| lines.push("added_line")) + e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line")) }); cx.assert_editor_state(indoc! {" «aaa @@ -4103,7 +4290,7 @@ async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) { bbbˇ» "}); cx.update_editor(|e, window, cx| { - e.manipulate_lines(window, cx, |lines| { + e.manipulate_immutable_lines(window, cx, |lines| { lines.pop(); }) }); @@ -4117,7 +4304,7 @@ async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) { bbbˇ» "}); cx.update_editor(|e, window, cx| { - e.manipulate_lines(window, cx, |lines| { + e.manipulate_immutable_lines(window, cx, |lines| { lines.drain(..); }) }); @@ -4217,7 +4404,7 @@ async fn test_unique_lines_single_selection(cx: &mut TestAppContext) { } #[gpui::test] -async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) { +async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) { init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx).await; @@ -4277,7 +4464,7 @@ async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) { aaaˇ»aa "}); cx.update_editor(|e, window, cx| { - e.manipulate_lines(window, cx, |lines| lines.push("added line")) + e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line")) }); cx.assert_editor_state(indoc! {" «2 @@ -4298,7 +4485,7 @@ async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) { aaaˇ»aa "}); cx.update_editor(|e, window, cx| { - e.manipulate_lines(window, cx, |lines| { + e.manipulate_immutable_lines(window, cx, |lines| { lines.pop(); }) }); @@ -4309,6 +4496,246 @@ async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) { "}); } +#[gpui::test] +async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) { + init_test(cx, |settings| { + settings.defaults.tab_size = NonZeroU32::new(3) + }); + + let mut cx = EditorTestContext::new(cx).await; + + // MULTI SELECTION + // Ln.1 "«" tests empty lines + // Ln.9 tests just leading whitespace + cx.set_state(indoc! {" + « + abc // No indentationˇ» + «\tabc // 1 tabˇ» + \t\tabc « ˇ» // 2 tabs + \t ab«c // Tab followed by space + \tabc // Space followed by tab (3 spaces should be the result) + \t \t \t \tabc // Mixed indentation (tab conversion depends on the column) + abˇ»ˇc ˇ ˇ // Already space indented« + \t + \tabc\tdef // Only the leading tab is manipulatedˇ» + "}); + cx.update_editor(|e, window, cx| { + e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx); + }); + cx.assert_editor_state( + indoc! {" + « + abc // No indentation + abc // 1 tab + abc // 2 tabs + abc // Tab followed by space + abc // Space followed by tab (3 spaces should be the result) + abc // Mixed indentation (tab conversion depends on the column) + abc // Already space indented + · + abc\tdef // Only the leading tab is manipulatedˇ» + "} + .replace("·", "") + .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace + ); + + // Test on just a few lines, the others should remain unchanged + // Only lines (3, 5, 10, 11) should change + cx.set_state( + indoc! {" + · + abc // No indentation + \tabcˇ // 1 tab + \t\tabc // 2 tabs + \t abcˇ // Tab followed by space + \tabc // Space followed by tab (3 spaces should be the result) + \t \t \t \tabc // Mixed indentation (tab conversion depends on the column) + abc // Already space indented + «\t + \tabc\tdef // Only the leading tab is manipulatedˇ» + "} + .replace("·", "") + .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace + ); + cx.update_editor(|e, window, cx| { + e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx); + }); + cx.assert_editor_state( + indoc! {" + · + abc // No indentation + « abc // 1 tabˇ» + \t\tabc // 2 tabs + « abc // Tab followed by spaceˇ» + \tabc // Space followed by tab (3 spaces should be the result) + \t \t \t \tabc // Mixed indentation (tab conversion depends on the column) + abc // Already space indented + « · + abc\tdef // Only the leading tab is manipulatedˇ» + "} + .replace("·", "") + .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace + ); + + // SINGLE SELECTION + // Ln.1 "«" tests empty lines + // Ln.9 tests just leading whitespace + cx.set_state(indoc! {" + « + abc // No indentation + \tabc // 1 tab + \t\tabc // 2 tabs + \t abc // Tab followed by space + \tabc // Space followed by tab (3 spaces should be the result) + \t \t \t \tabc // Mixed indentation (tab conversion depends on the column) + abc // Already space indented + \t + \tabc\tdef // Only the leading tab is manipulatedˇ» + "}); + cx.update_editor(|e, window, cx| { + e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx); + }); + cx.assert_editor_state( + indoc! {" + « + abc // No indentation + abc // 1 tab + abc // 2 tabs + abc // Tab followed by space + abc // Space followed by tab (3 spaces should be the result) + abc // Mixed indentation (tab conversion depends on the column) + abc // Already space indented + · + abc\tdef // Only the leading tab is manipulatedˇ» + "} + .replace("·", "") + .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace + ); +} + +#[gpui::test] +async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) { + init_test(cx, |settings| { + settings.defaults.tab_size = NonZeroU32::new(3) + }); + + let mut cx = EditorTestContext::new(cx).await; + + // MULTI SELECTION + // Ln.1 "«" tests empty lines + // Ln.11 tests just leading whitespace + cx.set_state(indoc! {" + « + abˇ»ˇc // No indentation + abc ˇ ˇ // 1 space (< 3 so dont convert) + abc « // 2 spaces (< 3 so dont convert) + abc // 3 spaces (convert) + abc ˇ» // 5 spaces (1 tab + 2 spaces) + «\tˇ»\t«\tˇ»abc // Already tab indented + «\t abc // Tab followed by space + \tabc // Space followed by tab (should be consumed due to tab) + \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted) + \tˇ» «\t + abcˇ» \t ˇˇˇ // Only the leading spaces should be converted + "}); + cx.update_editor(|e, window, cx| { + e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx); + }); + cx.assert_editor_state(indoc! {" + « + abc // No indentation + abc // 1 space (< 3 so dont convert) + abc // 2 spaces (< 3 so dont convert) + \tabc // 3 spaces (convert) + \t abc // 5 spaces (1 tab + 2 spaces) + \t\t\tabc // Already tab indented + \t abc // Tab followed by space + \tabc // Space followed by tab (should be consumed due to tab) + \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted) + \t\t\t + \tabc \t // Only the leading spaces should be convertedˇ» + "}); + + // Test on just a few lines, the other should remain unchanged + // Only lines (4, 8, 11, 12) should change + cx.set_state( + indoc! {" + · + abc // No indentation + abc // 1 space (< 3 so dont convert) + abc // 2 spaces (< 3 so dont convert) + « abc // 3 spaces (convert)ˇ» + abc // 5 spaces (1 tab + 2 spaces) + \t\t\tabc // Already tab indented + \t abc // Tab followed by space + \tabc ˇ // Space followed by tab (should be consumed due to tab) + \t\t \tabc // Mixed indentation + \t \t \t \tabc // Mixed indentation + \t \tˇ + « abc \t // Only the leading spaces should be convertedˇ» + "} + .replace("·", "") + .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace + ); + cx.update_editor(|e, window, cx| { + e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx); + }); + cx.assert_editor_state( + indoc! {" + · + abc // No indentation + abc // 1 space (< 3 so dont convert) + abc // 2 spaces (< 3 so dont convert) + «\tabc // 3 spaces (convert)ˇ» + abc // 5 spaces (1 tab + 2 spaces) + \t\t\tabc // Already tab indented + \t abc // Tab followed by space + «\tabc // Space followed by tab (should be consumed due to tab)ˇ» + \t\t \tabc // Mixed indentation + \t \t \t \tabc // Mixed indentation + «\t\t\t + \tabc \t // Only the leading spaces should be convertedˇ» + "} + .replace("·", "") + .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace + ); + + // SINGLE SELECTION + // Ln.1 "«" tests empty lines + // Ln.11 tests just leading whitespace + cx.set_state(indoc! {" + « + abc // No indentation + abc // 1 space (< 3 so dont convert) + abc // 2 spaces (< 3 so dont convert) + abc // 3 spaces (convert) + abc // 5 spaces (1 tab + 2 spaces) + \t\t\tabc // Already tab indented + \t abc // Tab followed by space + \tabc // Space followed by tab (should be consumed due to tab) + \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted) + \t \t + abc \t // Only the leading spaces should be convertedˇ» + "}); + cx.update_editor(|e, window, cx| { + e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx); + }); + cx.assert_editor_state(indoc! {" + « + abc // No indentation + abc // 1 space (< 3 so dont convert) + abc // 2 spaces (< 3 so dont convert) + \tabc // 3 spaces (convert) + \t abc // 5 spaces (1 tab + 2 spaces) + \t\t\tabc // Already tab indented + \t abc // Tab followed by space + \tabc // Space followed by tab (should be consumed due to tab) + \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted) + \t\t\t + \tabc \t // Only the leading spaces should be convertedˇ» + "}); +} + #[gpui::test] async fn test_toggle_case(cx: &mut TestAppContext) { init_test(cx, |_| {}); @@ -4344,6 +4771,23 @@ async fn test_toggle_case(cx: &mut TestAppContext) { "}); } +#[gpui::test] +async fn test_convert_to_sentence_case(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + cx.set_state(indoc! {" + «implement-windows-supportˇ» + "}); + cx.update_editor(|e, window, cx| { + e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx) + }); + cx.assert_editor_state(indoc! {" + «Implement windows supportˇ» + "}); +} + #[gpui::test] async fn test_manipulate_text(cx: &mut TestAppContext) { init_test(cx, |_| {}); @@ -4473,7 +4917,7 @@ fn test_duplicate_line(cx: &mut TestAppContext) { build_editor(buffer, window, cx) }); _ = editor.update(cx, |editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1), DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2), @@ -4499,7 +4943,7 @@ fn test_duplicate_line(cx: &mut TestAppContext) { build_editor(buffer, window, cx) }); _ = editor.update(cx, |editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1), DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1), @@ -4523,7 +4967,7 @@ fn test_duplicate_line(cx: &mut TestAppContext) { build_editor(buffer, window, cx) }); _ = editor.update(cx, |editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1), DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2), @@ -4549,7 +4993,7 @@ fn test_duplicate_line(cx: &mut TestAppContext) { build_editor(buffer, window, cx) }); _ = editor.update(cx, |editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1), DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1), @@ -4571,7 +5015,7 @@ fn test_duplicate_line(cx: &mut TestAppContext) { build_editor(buffer, window, cx) }); _ = editor.update(cx, |editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1), DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1), @@ -4608,7 +5052,7 @@ fn test_move_line_up_down(cx: &mut TestAppContext) { window, cx, ); - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1), DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1), @@ -4689,6 +5133,33 @@ fn test_move_line_up_down(cx: &mut TestAppContext) { }); } +#[gpui::test] +fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let editor = cx.add_window(|window, cx| { + let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx); + build_editor(buffer, window, cx) + }); + _ = editor.update(cx, |editor, window, cx| { + editor.fold_creases( + vec![Crease::simple( + Point::new(6, 4)..Point::new(7, 4), + FoldPlaceholder::test(), + )], + true, + window, + cx, + ); + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges([Point::new(7, 4)..Point::new(7, 4)]) + }); + assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc"); + editor.move_line_up(&MoveLineUp, window, cx); + let buffer_text = editor.buffer.read(cx).snapshot(cx).text(); + assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc"); + }); +} + #[gpui::test] fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) { init_test(cx, |_| {}); @@ -4706,12 +5177,11 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) { height: Some(1), render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }], Some(Autoscroll::fit()), cx, ); - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([Point::new(2, 0)..Point::new(2, 0)]) }); editor.move_line_down(&MoveLineDown, window, cx); @@ -4749,7 +5219,6 @@ async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) { style: BlockStyle::Sticky, render: Arc::new(|_| gpui::div().into_any_element()), priority: 0, - render_in_minimap: true, }], None, cx, @@ -4796,7 +5265,9 @@ fn test_transpose(cx: &mut TestAppContext) { _ = cx.add_window(|window, cx| { let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx); editor.set_style(EditorStyle::default(), window, cx); - editor.change_selections(None, window, cx, |s| s.select_ranges([1..1])); + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges([1..1]) + }); editor.transpose(&Default::default(), window, cx); assert_eq!(editor.text(cx), "bac"); assert_eq!(editor.selections.ranges(cx), [2..2]); @@ -4815,12 +5286,16 @@ fn test_transpose(cx: &mut TestAppContext) { _ = cx.add_window(|window, cx| { let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx); editor.set_style(EditorStyle::default(), window, cx); - editor.change_selections(None, window, cx, |s| s.select_ranges([3..3])); + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges([3..3]) + }); editor.transpose(&Default::default(), window, cx); assert_eq!(editor.text(cx), "acb\nde"); assert_eq!(editor.selections.ranges(cx), [3..3]); - editor.change_selections(None, window, cx, |s| s.select_ranges([4..4])); + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges([4..4]) + }); editor.transpose(&Default::default(), window, cx); assert_eq!(editor.text(cx), "acbd\ne"); assert_eq!(editor.selections.ranges(cx), [5..5]); @@ -4839,7 +5314,9 @@ fn test_transpose(cx: &mut TestAppContext) { _ = cx.add_window(|window, cx| { let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx); editor.set_style(EditorStyle::default(), window, cx); - editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4])); + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges([1..1, 2..2, 4..4]) + }); editor.transpose(&Default::default(), window, cx); assert_eq!(editor.text(cx), "bacd\ne"); assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]); @@ -4866,7 +5343,9 @@ fn test_transpose(cx: &mut TestAppContext) { _ = cx.add_window(|window, cx| { let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx); editor.set_style(EditorStyle::default(), window, cx); - editor.change_selections(None, window, cx, |s| s.select_ranges([4..4])); + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges([4..4]) + }); editor.transpose(&Default::default(), window, cx); assert_eq!(editor.text(cx), "🏀🍐✋"); assert_eq!(editor.selections.ranges(cx), [8..8]); @@ -4886,11 +5365,12 @@ fn test_transpose(cx: &mut TestAppContext) { #[gpui::test] async fn test_rewrap(cx: &mut TestAppContext) { init_test(cx, |settings| { - settings.languages.extend([ + settings.languages.0.extend([ ( "Markdown".into(), LanguageSettingsContent { allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere), + preferred_line_length: Some(40), ..Default::default() }, ), @@ -4898,6 +5378,31 @@ async fn test_rewrap(cx: &mut TestAppContext) { "Plain Text".into(), LanguageSettingsContent { allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere), + preferred_line_length: Some(40), + ..Default::default() + }, + ), + ( + "C++".into(), + LanguageSettingsContent { + allow_rewrap: Some(language_settings::RewrapBehavior::InComments), + preferred_line_length: Some(40), + ..Default::default() + }, + ), + ( + "Python".into(), + LanguageSettingsContent { + allow_rewrap: Some(language_settings::RewrapBehavior::InComments), + preferred_line_length: Some(40), + ..Default::default() + }, + ), + ( + "Rust".into(), + LanguageSettingsContent { + allow_rewrap: Some(language_settings::RewrapBehavior::InComments), + preferred_line_length: Some(40), ..Default::default() }, ), @@ -4906,15 +5411,17 @@ async fn test_rewrap(cx: &mut TestAppContext) { let mut cx = EditorTestContext::new(cx).await; - let language_with_c_comments = Arc::new(Language::new( + let cpp_language = Arc::new(Language::new( LanguageConfig { + name: "C++".into(), line_comments: vec!["// ".into()], ..LanguageConfig::default() }, None, )); - let language_with_pound_comments = Arc::new(Language::new( + let python_language = Arc::new(Language::new( LanguageConfig { + name: "Python".into(), line_comments: vec!["# ".into()], ..LanguageConfig::default() }, @@ -4923,12 +5430,17 @@ async fn test_rewrap(cx: &mut TestAppContext) { let markdown_language = Arc::new(Language::new( LanguageConfig { name: "Markdown".into(), + rewrap_prefixes: vec![ + regex::Regex::new("\\d+\\.\\s+").unwrap(), + regex::Regex::new("[-*+]\\s+").unwrap(), + ], ..LanguageConfig::default() }, None, )); - let language_with_doc_comments = Arc::new(Language::new( + let rust_language = Arc::new(Language::new( LanguageConfig { + name: "Rust".into(), line_comments: vec!["// ".into(), "/// ".into()], ..LanguageConfig::default() }, @@ -4943,233 +5455,295 @@ async fn test_rewrap(cx: &mut TestAppContext) { None, )); + // Test basic rewrapping of a long line with a cursor assert_rewrap( indoc! {" - // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros. + // ˇThis is a long comment that needs to be wrapped. "}, indoc! {" - // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit - // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus - // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam - // tincidunt hendrerit. Praesent semper egestas tellus id dignissim. - // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed - // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, - // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum - // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu - // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis - // porttitor id. Aliquam id accumsan eros. + // ˇThis is a long comment that needs to + // be wrapped. "}, - language_with_c_comments.clone(), + cpp_language.clone(), &mut cx, ); - // Test that rewrapping works inside of a selection + // Test rewrapping a full selection assert_rewrap( indoc! {" - «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.ˇ» - "}, + «// This selected long comment needs to be wrapped.ˇ»" + }, indoc! {" - «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit - // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus - // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam - // tincidunt hendrerit. Praesent semper egestas tellus id dignissim. - // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed - // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, - // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum - // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu - // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis - // porttitor id. Aliquam id accumsan eros.ˇ» - "}, - language_with_c_comments.clone(), + «// This selected long comment needs to + // be wrapped.ˇ»" + }, + cpp_language.clone(), &mut cx, ); - // Test that cursors that expand to the same region are collapsed. + // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping assert_rewrap( indoc! {" - // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. - // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. - // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, - // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros. - "}, + // ˇThis is the first line. + // Thisˇ is the second line. + // This is the thirdˇ line, all part of one paragraph. + "}, indoc! {" - // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit - // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus - // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam - // tincidunt hendrerit. Praesent semper egestas tellus id dignissim. - // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed - // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, - // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum - // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu - // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis - // porttitor id. Aliquam id accumsan eros. - "}, - language_with_c_comments.clone(), + // ˇThis is the first line. Thisˇ is the + // second line. This is the thirdˇ line, + // all part of one paragraph. + "}, + cpp_language.clone(), &mut cx, ); - // Test that non-contiguous selections are treated separately. + // Test multiple cursors in different paragraphs trigger separate rewraps assert_rewrap( indoc! {" - // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. - // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. - // - // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, - // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros. + // ˇThis is the first paragraph, first line. + // ˇThis is the first paragraph, second line. + + // ˇThis is the second paragraph, first line. + // ˇThis is the second paragraph, second line. "}, indoc! {" - // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit - // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus - // auctor, eu lacinia sapien scelerisque. - // - // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas - // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, - // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec - // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque - // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas - // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id - // vulputate turpis porttitor id. Aliquam id accumsan eros. + // ˇThis is the first paragraph, first + // line. ˇThis is the first paragraph, + // second line. + + // ˇThis is the second paragraph, first + // line. ˇThis is the second paragraph, + // second line. "}, - language_with_c_comments.clone(), + cpp_language.clone(), &mut cx, ); - // Test that different comment prefixes are supported. + // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps assert_rewrap( indoc! {" - # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros. - "}, + «// A regular long long comment to be wrapped. + /// A documentation long comment to be wrapped.ˇ» + "}, indoc! {" - # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit - # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, - # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt - # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio - # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit - # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet - # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur - # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. - # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id - # accumsan eros. - "}, - language_with_pound_comments.clone(), + «// A regular long long comment to be + // wrapped. + /// A documentation long comment to be + /// wrapped.ˇ» + "}, + rust_language.clone(), &mut cx, ); - // Test that rewrapping is ignored outside of comments in most languages. + // Test that change in indentation level trigger seperate rewraps assert_rewrap( indoc! {" - /// Adds two numbers. - /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ - fn add(a: u32, b: u32) -> u32 { - a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + bˇ + fn foo() { + «// This is a long comment at the base indent. + // This is a long comment at the next indent.ˇ» } "}, indoc! {" - /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit. - /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ - fn add(a: u32, b: u32) -> u32 { - a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + bˇ + fn foo() { + «// This is a long comment at the + // base indent. + // This is a long comment at the + // next indent.ˇ» } "}, - language_with_doc_comments.clone(), + rust_language.clone(), &mut cx, ); - // Test that rewrapping works in Markdown and Plain Text languages. + // Test that different comment prefix characters (e.g., '#') are handled correctly assert_rewrap( indoc! {" - # Hello - - Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. + # ˇThis is a long comment using a pound sign. "}, indoc! {" - # Hello + # ˇThis is a long comment using a pound + # sign. + "}, + python_language, + &mut cx, + ); - Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit - purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, - eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt - hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio - lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet - nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. - Integer sit amet scelerisque nisi. + // Test rewrapping only affects comments, not code even when selected + assert_rewrap( + indoc! {" + «/// This doc comment is long and should be wrapped. + fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ» + "}, + indoc! {" + «/// This doc comment is long and should + /// be wrapped. + fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ» + "}, + rust_language.clone(), + &mut cx, + ); + + // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere` + assert_rewrap( + indoc! {" + # Header + + A long long long line of markdown text to wrap.ˇ + "}, + indoc! {" + # Header + + A long long long line of markdown text + to wrap.ˇ + "}, + markdown_language.clone(), + &mut cx, + ); + + // Test that rewrapping boundary works and preserves relative indent for Markdown documents + assert_rewrap( + indoc! {" + «1. This is a numbered list item that is very long and needs to be wrapped properly. + 2. This is a numbered list item that is very long and needs to be wrapped properly. + - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ» + "}, + indoc! {" + «1. This is a numbered list item that is + very long and needs to be wrapped + properly. + 2. This is a numbered list item that is + very long and needs to be wrapped + properly. + - This is an unordered list item that is + also very long and should not merge + with the numbered item.ˇ» + "}, + markdown_language.clone(), + &mut cx, + ); + + // Test that rewrapping add indents for rewrapping boundary if not exists already. + assert_rewrap( + indoc! {" + «1. This is a numbered list item that is + very long and needs to be wrapped + properly. + 2. This is a numbered list item that is + very long and needs to be wrapped + properly. + - This is an unordered list item that is + also very long and should not merge with + the numbered item.ˇ» + "}, + indoc! {" + «1. This is a numbered list item that is + very long and needs to be wrapped + properly. + 2. This is a numbered list item that is + very long and needs to be wrapped + properly. + - This is an unordered list item that is + also very long and should not merge + with the numbered item.ˇ» + "}, + markdown_language.clone(), + &mut cx, + ); + + // Test that rewrapping maintain indents even when they already exists. + assert_rewrap( + indoc! {" + «1. This is a numbered list + item that is very long and needs to be wrapped properly. + 2. This is a numbered list + item that is very long and needs to be wrapped properly. + - This is an unordered list item that is also very long and + should not merge with the numbered item.ˇ» + "}, + indoc! {" + «1. This is a numbered list item that is + very long and needs to be wrapped + properly. + 2. This is a numbered list item that is + very long and needs to be wrapped + properly. + - This is an unordered list item that is + also very long and should not merge + with the numbered item.ˇ» "}, markdown_language, &mut cx, ); + // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere` assert_rewrap( indoc! {" - Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. + ˇThis is a very long line of plain text that will be wrapped. "}, indoc! {" - Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit - purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, - eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt - hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio - lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet - nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. - Integer sit amet scelerisque nisi. + ˇThis is a very long line of plain text + that will be wrapped. "}, + plaintext_language.clone(), + &mut cx, + ); + + // Test that non-commented code acts as a paragraph boundary within a selection + assert_rewrap( + indoc! {" + «// This is the first long comment block to be wrapped. + fn my_func(a: u32); + // This is the second long comment block to be wrapped.ˇ» + "}, + indoc! {" + «// This is the first long comment block + // to be wrapped. + fn my_func(a: u32); + // This is the second long comment block + // to be wrapped.ˇ» + "}, + rust_language, + &mut cx, + ); + + // Test rewrapping multiple selections, including ones with blank lines or tabs + assert_rewrap( + indoc! {" + «ˇThis is a very long line that will be wrapped. + + This is another paragraph in the same selection.» + + «\tThis is a very long indented line that will be wrapped.ˇ» + "}, + indoc! {" + «ˇThis is a very long line that will be + wrapped. + + This is another paragraph in the same + selection.» + + «\tThis is a very long indented line + \tthat will be wrapped.ˇ» + "}, plaintext_language, &mut cx, ); - // Test rewrapping unaligned comments in a selection. + // Test that an empty comment line acts as a paragraph boundary assert_rewrap( indoc! {" - fn foo() { - if true { - « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. - // Praesent semper egestas tellus id dignissim.ˇ» - do_something(); - } else { - // - } - } - "}, + // ˇThis is a long comment that will be wrapped. + // + // And this is another long comment that will also be wrapped.ˇ + "}, indoc! {" - fn foo() { - if true { - « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus - // mollis elit purus, a ornare lacus gravida vitae. Praesent semper - // egestas tellus id dignissim.ˇ» - do_something(); - } else { - // - } - } - "}, - language_with_doc_comments.clone(), - &mut cx, - ); - - assert_rewrap( - indoc! {" - fn foo() { - if true { - «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. - // Praesent semper egestas tellus id dignissim.» - do_something(); - } else { - // - } - - } - "}, - indoc! {" - fn foo() { - if true { - «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus - // mollis elit purus, a ornare lacus gravida vitae. Praesent semper - // egestas tellus id dignissim.» - do_something(); - } else { - // - } - - } - "}, - language_with_doc_comments.clone(), + // ˇThis is a long comment that will be + // wrapped. + // + // And this is another long comment that + // will also be wrapped.ˇ + "}, + cpp_language, &mut cx, ); @@ -5782,7 +6356,7 @@ fn test_select_line(cx: &mut TestAppContext) { build_editor(buffer, window, cx) }); _ = editor.update(cx, |editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1), DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2), @@ -5829,7 +6403,7 @@ async fn test_split_selection_into_lines(cx: &mut TestAppContext) { fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) { cx.set_state(initial_state); cx.update_editor(|e, window, cx| { - e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx) + e.split_selection_into_lines(&Default::default(), window, cx) }); cx.assert_editor_state(expected_state); } @@ -5909,7 +6483,7 @@ async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestA }); _ = editor.update(cx, |editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1), DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2), @@ -5917,7 +6491,7 @@ async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestA DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4), ]) }); - editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx); + editor.split_selection_into_lines(&Default::default(), window, cx); assert_eq!( editor.display_text(cx), "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i" @@ -5928,12 +6502,12 @@ async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestA .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ"); _ = editor.update(cx, |editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1) ]) }); - editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx); + editor.split_selection_into_lines(&Default::default(), window, cx); assert_eq!( editor.display_text(cx), "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii" @@ -6236,6 +6810,296 @@ async fn test_add_selection_above_below(cx: &mut TestAppContext) { )); } +#[gpui::test] +async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + + cx.set_state(indoc!( + r#"line onˇe + liˇne two + line three + line four"# + )); + + cx.update_editor(|editor, window, cx| { + editor.add_selection_below(&Default::default(), window, cx); + }); + + // test multiple cursors expand in the same direction + cx.assert_editor_state(indoc!( + r#"line onˇe + liˇne twˇo + liˇne three + line four"# + )); + + cx.update_editor(|editor, window, cx| { + editor.add_selection_below(&Default::default(), window, cx); + }); + + cx.update_editor(|editor, window, cx| { + editor.add_selection_below(&Default::default(), window, cx); + }); + + // test multiple cursors expand below overflow + cx.assert_editor_state(indoc!( + r#"line onˇe + liˇne twˇo + liˇne thˇree + liˇne foˇur"# + )); + + cx.update_editor(|editor, window, cx| { + editor.add_selection_above(&Default::default(), window, cx); + }); + + // test multiple cursors retrieves back correctly + cx.assert_editor_state(indoc!( + r#"line onˇe + liˇne twˇo + liˇne thˇree + line four"# + )); + + cx.update_editor(|editor, window, cx| { + editor.add_selection_above(&Default::default(), window, cx); + }); + + cx.update_editor(|editor, window, cx| { + editor.add_selection_above(&Default::default(), window, cx); + }); + + // test multiple cursor groups maintain independent direction - first expands up, second shrinks above + cx.assert_editor_state(indoc!( + r#"liˇne onˇe + liˇne two + line three + line four"# + )); + + cx.update_editor(|editor, window, cx| { + editor.undo_selection(&Default::default(), window, cx); + }); + + // test undo + cx.assert_editor_state(indoc!( + r#"line onˇe + liˇne twˇo + line three + line four"# + )); + + cx.update_editor(|editor, window, cx| { + editor.redo_selection(&Default::default(), window, cx); + }); + + // test redo + cx.assert_editor_state(indoc!( + r#"liˇne onˇe + liˇne two + line three + line four"# + )); + + cx.set_state(indoc!( + r#"abcd + ef«ghˇ» + ijkl + «mˇ»nop"# + )); + + cx.update_editor(|editor, window, cx| { + editor.add_selection_above(&Default::default(), window, cx); + }); + + // test multiple selections expand in the same direction + cx.assert_editor_state(indoc!( + r#"ab«cdˇ» + ef«ghˇ» + «iˇ»jkl + «mˇ»nop"# + )); + + cx.update_editor(|editor, window, cx| { + editor.add_selection_above(&Default::default(), window, cx); + }); + + // test multiple selection upward overflow + cx.assert_editor_state(indoc!( + r#"ab«cdˇ» + «eˇ»f«ghˇ» + «iˇ»jkl + «mˇ»nop"# + )); + + cx.update_editor(|editor, window, cx| { + editor.add_selection_below(&Default::default(), window, cx); + }); + + // test multiple selection retrieves back correctly + cx.assert_editor_state(indoc!( + r#"abcd + ef«ghˇ» + «iˇ»jkl + «mˇ»nop"# + )); + + cx.update_editor(|editor, window, cx| { + editor.add_selection_below(&Default::default(), window, cx); + }); + + // test multiple cursor groups maintain independent direction - first shrinks down, second expands below + cx.assert_editor_state(indoc!( + r#"abcd + ef«ghˇ» + ij«klˇ» + «mˇ»nop"# + )); + + cx.update_editor(|editor, window, cx| { + editor.undo_selection(&Default::default(), window, cx); + }); + + // test undo + cx.assert_editor_state(indoc!( + r#"abcd + ef«ghˇ» + «iˇ»jkl + «mˇ»nop"# + )); + + cx.update_editor(|editor, window, cx| { + editor.redo_selection(&Default::default(), window, cx); + }); + + // test redo + cx.assert_editor_state(indoc!( + r#"abcd + ef«ghˇ» + ij«klˇ» + «mˇ»nop"# + )); +} + +#[gpui::test] +async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + + cx.set_state(indoc!( + r#"line onˇe + liˇne two + line three + line four"# + )); + + cx.update_editor(|editor, window, cx| { + editor.add_selection_below(&Default::default(), window, cx); + editor.add_selection_below(&Default::default(), window, cx); + editor.add_selection_below(&Default::default(), window, cx); + }); + + // initial state with two multi cursor groups + cx.assert_editor_state(indoc!( + r#"line onˇe + liˇne twˇo + liˇne thˇree + liˇne foˇur"# + )); + + // add single cursor in middle - simulate opt click + cx.update_editor(|editor, window, cx| { + let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4); + editor.begin_selection(new_cursor_point, true, 1, window, cx); + editor.end_selection(window, cx); + }); + + cx.assert_editor_state(indoc!( + r#"line onˇe + liˇne twˇo + liˇneˇ thˇree + liˇne foˇur"# + )); + + cx.update_editor(|editor, window, cx| { + editor.add_selection_above(&Default::default(), window, cx); + }); + + // test new added selection expands above and existing selection shrinks + cx.assert_editor_state(indoc!( + r#"line onˇe + liˇneˇ twˇo + liˇneˇ thˇree + line four"# + )); + + cx.update_editor(|editor, window, cx| { + editor.add_selection_above(&Default::default(), window, cx); + }); + + // test new added selection expands above and existing selection shrinks + cx.assert_editor_state(indoc!( + r#"lineˇ onˇe + liˇneˇ twˇo + lineˇ three + line four"# + )); + + // intial state with two selection groups + cx.set_state(indoc!( + r#"abcd + ef«ghˇ» + ijkl + «mˇ»nop"# + )); + + cx.update_editor(|editor, window, cx| { + editor.add_selection_above(&Default::default(), window, cx); + editor.add_selection_above(&Default::default(), window, cx); + }); + + cx.assert_editor_state(indoc!( + r#"ab«cdˇ» + «eˇ»f«ghˇ» + «iˇ»jkl + «mˇ»nop"# + )); + + // add single selection in middle - simulate opt drag + cx.update_editor(|editor, window, cx| { + let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3); + editor.begin_selection(new_cursor_point, true, 1, window, cx); + editor.update_selection( + DisplayPoint::new(DisplayRow(2), 4), + 0, + gpui::Point::::default(), + window, + cx, + ); + editor.end_selection(window, cx); + }); + + cx.assert_editor_state(indoc!( + r#"ab«cdˇ» + «eˇ»f«ghˇ» + «iˇ»jk«lˇ» + «mˇ»nop"# + )); + + cx.update_editor(|editor, window, cx| { + editor.add_selection_below(&Default::default(), window, cx); + }); + + // test new added selection expands below, others shrinks from above + cx.assert_editor_state(indoc!( + r#"abcd + ef«ghˇ» + «iˇ»jk«lˇ» + «mˇ»no«pˇ»"# + )); +} + #[gpui::test] async fn test_select_next(cx: &mut TestAppContext) { init_test(cx, |_| {}); @@ -6314,6 +7178,15 @@ async fn test_select_all_matches(cx: &mut TestAppContext) { cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx)) .unwrap(); cx.assert_editor_state("abc\n« ˇ»abc\nabc"); + + // Test with a single word and clip_at_line_ends=true (#29823) + cx.set_state("aˇbc"); + cx.update_editor(|e, window, cx| { + e.set_clip_at_line_ends(true, cx); + e.select_all_matches(&SelectAllMatches, window, cx).unwrap(); + e.set_clip_at_line_ends(false, cx); + }); + cx.assert_editor_state("«abcˇ»"); } #[gpui::test] @@ -6375,7 +7248,7 @@ async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) { // Move cursor to a different position cx.update_editor(|editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]); }); }); @@ -6425,12 +7298,12 @@ async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) { } #[gpui::test] -async fn test_undo_inline_completion_scrolls_to_edit_pos(cx: &mut TestAppContext) { +async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) { init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx).await; - let provider = cx.new(|_| FakeInlineCompletionProvider::default()); + let provider = cx.new(|_| FakeEditPredictionProvider::default()); cx.update_editor(|editor, window, cx| { editor.set_edit_prediction_provider(Some(provider.clone()), window, cx); }); @@ -6453,7 +7326,7 @@ async fn test_undo_inline_completion_scrolls_to_edit_pos(cx: &mut TestAppContext cx.update(|_, cx| { provider.update(cx, |provider, _| { - provider.set_inline_completion(Some(inline_completion::InlineCompletion { + provider.set_edit_prediction(Some(edit_prediction::EditPrediction { id: None, edits: vec![(edit_position..edit_position, "X".into())], edit_preview: None, @@ -6461,7 +7334,7 @@ async fn test_undo_inline_completion_scrolls_to_edit_pos(cx: &mut TestAppContext }) }); - cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx)); + cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx)); cx.update_editor(|editor, window, cx| { editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx) }); @@ -6480,7 +7353,7 @@ async fn test_undo_inline_completion_scrolls_to_edit_pos(cx: &mut TestAppContext "}); cx.update_editor(|editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]); }); }); @@ -6740,7 +7613,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) { .await; editor.update_in(cx, |editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25), DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12), @@ -6922,7 +7795,7 @@ async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContex // Test case 1: Cursor at end of word editor.update_in(cx, |editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5) ]); @@ -6946,7 +7819,7 @@ async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContex // Test case 2: Cursor at end of statement editor.update_in(cx, |editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11) ]); @@ -6991,7 +7864,7 @@ async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppConte // Test 1: Cursor on a letter of a string word editor.update_in(cx, |editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17) ]); @@ -7025,7 +7898,7 @@ async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppConte // Test 2: Partial selection within a word editor.update_in(cx, |editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19) ]); @@ -7059,7 +7932,7 @@ async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppConte // Test 3: Complete word already selected editor.update_in(cx, |editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21) ]); @@ -7093,7 +7966,7 @@ async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppConte // Test 4: Selection spanning across words editor.update_in(cx, |editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24) ]); @@ -7143,6 +8016,29 @@ async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppConte }); } +#[gpui::test] +async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + let language = Arc::new(Language::new( + LanguageConfig::default(), + Some(tree_sitter_rust::LANGUAGE.into()), + )); + + cx.update_buffer(|buffer, cx| { + buffer.set_language(Some(language), cx); + }); + + cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# }); + cx.update_editor(|editor, window, cx| { + editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx); + }); + + cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# }); +} + #[gpui::test] async fn test_fold_function_bodies(cx: &mut TestAppContext) { init_test(cx, |_| {}); @@ -7295,7 +8191,9 @@ async fn test_autoindent(cx: &mut TestAppContext) { .await; editor.update_in(cx, |editor, window, cx| { - editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9])); + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges([5..5, 8..8, 9..9]) + }); editor.newline(&Newline, window, cx); assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n"); assert_eq!( @@ -7309,6 +8207,216 @@ async fn test_autoindent(cx: &mut TestAppContext) { }); } +#[gpui::test] +async fn test_autoindent_disabled(cx: &mut TestAppContext) { + init_test(cx, |settings| settings.defaults.auto_indent = Some(false)); + + let language = Arc::new( + Language::new( + LanguageConfig { + brackets: BracketPairConfig { + pairs: vec![ + BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: false, + surround: false, + newline: true, + }, + BracketPair { + start: "(".to_string(), + end: ")".to_string(), + close: false, + surround: false, + newline: true, + }, + ], + ..Default::default() + }, + ..Default::default() + }, + Some(tree_sitter_rust::LANGUAGE.into()), + ) + .with_indents_query( + r#" + (_ "(" ")" @end) @indent + (_ "{" "}" @end) @indent + "#, + ) + .unwrap(), + ); + + let text = "fn a() {}"; + + let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx)); + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx)); + editor + .condition::(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx)) + .await; + + editor.update_in(cx, |editor, window, cx| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges([5..5, 8..8, 9..9]) + }); + editor.newline(&Newline, window, cx); + assert_eq!( + editor.text(cx), + indoc!( + " + fn a( + + ) { + + } + " + ) + ); + assert_eq!( + editor.selections.ranges(cx), + &[ + Point::new(1, 0)..Point::new(1, 0), + Point::new(3, 0)..Point::new(3, 0), + Point::new(5, 0)..Point::new(5, 0) + ] + ); + }); +} + +#[gpui::test] +async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) { + init_test(cx, |settings| { + settings.defaults.auto_indent = Some(true); + settings.languages.0.insert( + "python".into(), + LanguageSettingsContent { + auto_indent: Some(false), + ..Default::default() + }, + ); + }); + + let mut cx = EditorTestContext::new(cx).await; + + let injected_language = Arc::new( + Language::new( + LanguageConfig { + brackets: BracketPairConfig { + pairs: vec![ + BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: false, + surround: false, + newline: true, + }, + BracketPair { + start: "(".to_string(), + end: ")".to_string(), + close: true, + surround: false, + newline: true, + }, + ], + ..Default::default() + }, + name: "python".into(), + ..Default::default() + }, + Some(tree_sitter_python::LANGUAGE.into()), + ) + .with_indents_query( + r#" + (_ "(" ")" @end) @indent + (_ "{" "}" @end) @indent + "#, + ) + .unwrap(), + ); + + let language = Arc::new( + Language::new( + LanguageConfig { + brackets: BracketPairConfig { + pairs: vec![ + BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: false, + surround: false, + newline: true, + }, + BracketPair { + start: "(".to_string(), + end: ")".to_string(), + close: true, + surround: false, + newline: true, + }, + ], + ..Default::default() + }, + name: LanguageName::new("rust"), + ..Default::default() + }, + Some(tree_sitter_rust::LANGUAGE.into()), + ) + .with_indents_query( + r#" + (_ "(" ")" @end) @indent + (_ "{" "}" @end) @indent + "#, + ) + .unwrap() + .with_injection_query( + r#" + (macro_invocation + macro: (identifier) @_macro_name + (token_tree) @injection.content + (#set! injection.language "python")) + "#, + ) + .unwrap(), + ); + + cx.language_registry().add(injected_language); + cx.language_registry().add(language.clone()); + + cx.update_buffer(|buffer, cx| { + buffer.set_language(Some(language), cx); + }); + + cx.set_state(r#"struct A {ˇ}"#); + + cx.update_editor(|editor, window, cx| { + editor.newline(&Default::default(), window, cx); + }); + + cx.assert_editor_state(indoc!( + "struct A { + ˇ + }" + )); + + cx.set_state(r#"select_biased!(ˇ)"#); + + cx.update_editor(|editor, window, cx| { + editor.newline(&Default::default(), window, cx); + editor.handle_input("def ", window, cx); + editor.handle_input("(", window, cx); + editor.newline(&Default::default(), window, cx); + editor.handle_input("a", window, cx); + }); + + cx.assert_editor_state(indoc!( + "select_biased!( + def ( + aˇ + ) + )" + )); +} + #[gpui::test] async fn test_autoindent_selections(cx: &mut TestAppContext) { init_test(cx, |_| {}); @@ -7783,7 +8891,8 @@ async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) { )); cx.language_registry().add(html_language.clone()); - cx.language_registry().add(javascript_language.clone()); + cx.language_registry().add(javascript_language); + cx.executor().run_until_parked(); cx.update_buffer(|buffer, cx| { buffer.set_language(Some(html_language), cx); @@ -8077,7 +9186,7 @@ async fn test_surround_with_pair(cx: &mut TestAppContext) { .await; editor.update_in(cx, |editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1), DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1), @@ -8227,7 +9336,7 @@ async fn test_delete_autoclose_pair(cx: &mut TestAppContext) { .await; editor.update_in(cx, |editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([ Point::new(0, 1)..Point::new(0, 1), Point::new(1, 1)..Point::new(1, 1), @@ -8512,108 +9621,123 @@ async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) { async fn test_snippets(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let (text, insertion_ranges) = marked_text_ranges( - indoc! {" - a.ˇ b - a.ˇ b - a.ˇ b - "}, - false, - ); + let mut cx = EditorTestContext::new(cx).await; - let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx)); - let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx)); + cx.set_state(indoc! {" + a.ˇ b + a.ˇ b + a.ˇ b + "}); - editor.update_in(cx, |editor, window, cx| { + cx.update_editor(|editor, window, cx| { let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap(); - + let insertion_ranges = editor + .selections + .all(cx) + .iter() + .map(|s| s.range()) + .collect::>(); editor .insert_snippet(&insertion_ranges, snippet, window, cx) .unwrap(); - - fn assert(editor: &mut Editor, cx: &mut Context, marked_text: &str) { - let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false); - assert_eq!(editor.text(cx), expected_text); - assert_eq!(editor.selections.ranges::(cx), selection_ranges); - } - - assert( - editor, - cx, - indoc! {" - a.f(«one», two, «three») b - a.f(«one», two, «three») b - a.f(«one», two, «three») b - "}, - ); - - // Can't move earlier than the first tab stop - assert!(!editor.move_to_prev_snippet_tabstop(window, cx)); - assert( - editor, - cx, - indoc! {" - a.f(«one», two, «three») b - a.f(«one», two, «three») b - a.f(«one», two, «three») b - "}, - ); - - assert!(editor.move_to_next_snippet_tabstop(window, cx)); - assert( - editor, - cx, - indoc! {" - a.f(one, «two», three) b - a.f(one, «two», three) b - a.f(one, «two», three) b - "}, - ); - - editor.move_to_prev_snippet_tabstop(window, cx); - assert( - editor, - cx, - indoc! {" - a.f(«one», two, «three») b - a.f(«one», two, «three») b - a.f(«one», two, «three») b - "}, - ); - - assert!(editor.move_to_next_snippet_tabstop(window, cx)); - assert( - editor, - cx, - indoc! {" - a.f(one, «two», three) b - a.f(one, «two», three) b - a.f(one, «two», three) b - "}, - ); - assert!(editor.move_to_next_snippet_tabstop(window, cx)); - assert( - editor, - cx, - indoc! {" - a.f(one, two, three)ˇ b - a.f(one, two, three)ˇ b - a.f(one, two, three)ˇ b - "}, - ); - - // As soon as the last tab stop is reached, snippet state is gone - editor.move_to_prev_snippet_tabstop(window, cx); - assert( - editor, - cx, - indoc! {" - a.f(one, two, three)ˇ b - a.f(one, two, three)ˇ b - a.f(one, two, three)ˇ b - "}, - ); }); + + cx.assert_editor_state(indoc! {" + a.f(«oneˇ», two, «threeˇ») b + a.f(«oneˇ», two, «threeˇ») b + a.f(«oneˇ», two, «threeˇ») b + "}); + + // Can't move earlier than the first tab stop + cx.update_editor(|editor, window, cx| { + assert!(!editor.move_to_prev_snippet_tabstop(window, cx)) + }); + cx.assert_editor_state(indoc! {" + a.f(«oneˇ», two, «threeˇ») b + a.f(«oneˇ», two, «threeˇ») b + a.f(«oneˇ», two, «threeˇ») b + "}); + + cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx))); + cx.assert_editor_state(indoc! {" + a.f(one, «twoˇ», three) b + a.f(one, «twoˇ», three) b + a.f(one, «twoˇ», three) b + "}); + + cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx))); + cx.assert_editor_state(indoc! {" + a.f(«oneˇ», two, «threeˇ») b + a.f(«oneˇ», two, «threeˇ») b + a.f(«oneˇ», two, «threeˇ») b + "}); + + cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx))); + cx.assert_editor_state(indoc! {" + a.f(one, «twoˇ», three) b + a.f(one, «twoˇ», three) b + a.f(one, «twoˇ», three) b + "}); + cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx))); + cx.assert_editor_state(indoc! {" + a.f(one, two, three)ˇ b + a.f(one, two, three)ˇ b + a.f(one, two, three)ˇ b + "}); + + // As soon as the last tab stop is reached, snippet state is gone + cx.update_editor(|editor, window, cx| { + assert!(!editor.move_to_prev_snippet_tabstop(window, cx)) + }); + cx.assert_editor_state(indoc! {" + a.f(one, two, three)ˇ b + a.f(one, two, three)ˇ b + a.f(one, two, three)ˇ b + "}); +} + +#[gpui::test] +async fn test_snippet_indentation(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + cx.update_editor(|editor, window, cx| { + let snippet = Snippet::parse(indoc! {" + /* + * Multiline comment with leading indentation + * + * $1 + */ + $0"}) + .unwrap(); + let insertion_ranges = editor + .selections + .all(cx) + .iter() + .map(|s| s.range()) + .collect::>(); + editor + .insert_snippet(&insertion_ranges, snippet, window, cx) + .unwrap(); + }); + + cx.assert_editor_state(indoc! {" + /* + * Multiline comment with leading indentation + * + * ˇ + */ + "}); + + cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx))); + cx.assert_editor_state(indoc! {" + /* + * Multiline comment with leading indentation + * + *• + */ + ˇ"}); } #[gpui::test] @@ -8673,7 +9797,15 @@ async fn test_document_format_during_save(cx: &mut TestAppContext) { ); let save = editor .update_in(cx, |editor, window, cx| { - editor.save(true, project.clone(), window, cx) + editor.save( + SaveOptions { + format: true, + autosave: false, + }, + project.clone(), + window, + cx, + ) }) .unwrap(); cx.executor().start_waiting(); @@ -8705,7 +9837,15 @@ async fn test_document_format_during_save(cx: &mut TestAppContext) { ); let save = editor .update_in(cx, |editor, window, cx| { - editor.save(true, project.clone(), window, cx) + editor.save( + SaveOptions { + format: true, + autosave: false, + }, + project.clone(), + window, + cx, + ) }) .unwrap(); cx.executor().advance_clock(super::FORMAT_TIMEOUT); @@ -8717,25 +9857,9 @@ async fn test_document_format_during_save(cx: &mut TestAppContext) { ); } - // For non-dirty buffer, no formatting request should be sent - { - assert!(!cx.read(|cx| editor.is_dirty(cx))); - - fake_server.set_request_handler::(move |_, _| async move { - panic!("Should not be invoked on non-dirty buffer"); - }); - let save = editor - .update_in(cx, |editor, window, cx| { - editor.save(true, project.clone(), window, cx) - }) - .unwrap(); - cx.executor().start_waiting(); - save.await; - } - // Set rust language override and assert overridden tabsize is sent to language server update_test_language_settings(cx, |settings| { - settings.languages.insert( + settings.languages.0.insert( "Rust".into(), LanguageSettingsContent { tab_size: NonZeroU32::new(8), @@ -8760,7 +9884,15 @@ async fn test_document_format_during_save(cx: &mut TestAppContext) { }); let save = editor .update_in(cx, |editor, window, cx| { - editor.save(true, project.clone(), window, cx) + editor.save( + SaveOptions { + format: true, + autosave: false, + }, + project.clone(), + window, + cx, + ) }) .unwrap(); cx.executor().start_waiting(); @@ -8768,6 +9900,74 @@ async fn test_document_format_during_save(cx: &mut TestAppContext) { } } +#[gpui::test] +async fn test_redo_after_noop_format(cx: &mut TestAppContext) { + init_test(cx, |settings| { + settings.defaults.ensure_final_newline_on_save = Some(false); + }); + + let fs = FakeFs::new(cx.executor()); + fs.insert_file(path!("/file.txt"), "foo".into()).await; + + let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await; + + let buffer = project + .update(cx, |project, cx| { + project.open_local_buffer(path!("/file.txt"), cx) + }) + .await + .unwrap(); + + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + let (editor, cx) = cx.add_window_view(|window, cx| { + build_editor_with_project(project.clone(), buffer, window, cx) + }); + editor.update_in(cx, |editor, window, cx| { + editor.change_selections(SelectionEffects::default(), window, cx, |s| { + s.select_ranges([0..0]) + }); + }); + assert!(!cx.read(|cx| editor.is_dirty(cx))); + + editor.update_in(cx, |editor, window, cx| { + editor.handle_input("\n", window, cx) + }); + cx.run_until_parked(); + save(&editor, &project, cx).await; + assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx))); + + editor.update_in(cx, |editor, window, cx| { + editor.undo(&Default::default(), window, cx); + }); + save(&editor, &project, cx).await; + assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx))); + + editor.update_in(cx, |editor, window, cx| { + editor.redo(&Default::default(), window, cx); + }); + cx.run_until_parked(); + assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx))); + + async fn save(editor: &Entity, project: &Entity, cx: &mut VisualTestContext) { + let save = editor + .update_in(cx, |editor, window, cx| { + editor.save( + SaveOptions { + format: true, + autosave: false, + }, + project.clone(), + window, + cx, + ) + }) + .unwrap(); + cx.executor().start_waiting(); + save.await; + assert!(!cx.read(|cx| editor.is_dirty(cx))); + } +} + #[gpui::test] async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) { init_test(cx, |_| {}); @@ -8886,16 +10086,22 @@ async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) { }); multi_buffer_editor.update_in(cx, |editor, window, cx| { - editor.change_selections(Some(Autoscroll::Next), window, cx, |s| { - s.select_ranges(Some(1..2)) - }); + editor.change_selections( + SelectionEffects::scroll(Autoscroll::Next), + window, + cx, + |s| s.select_ranges(Some(1..2)), + ); editor.insert("|one|two|three|", window, cx); }); assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx))); multi_buffer_editor.update_in(cx, |editor, window, cx| { - editor.change_selections(Some(Autoscroll::Next), window, cx, |s| { - s.select_ranges(Some(60..70)) - }); + editor.change_selections( + SelectionEffects::scroll(Autoscroll::Next), + window, + cx, + |s| s.select_ranges(Some(60..70)), + ); editor.insert("|four|five|six|", window, cx); }); assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx))); @@ -8928,7 +10134,15 @@ async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) { cx.executor().start_waiting(); let save = multi_buffer_editor .update_in(cx, |editor, window, cx| { - editor.save(true, project.clone(), window, cx) + editor.save( + SaveOptions { + format: true, + autosave: false, + }, + project.clone(), + window, + cx, + ) }) .unwrap(); @@ -8973,7 +10187,180 @@ async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) { } #[gpui::test] -async fn test_range_format_during_save(cx: &mut TestAppContext) { +async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + path!("/dir"), + json!({ + "file1.rs": "fn main() { println!(\"hello\"); }", + "file2.rs": "fn test() { println!(\"test\"); }", + "file3.rs": "fn other() { println!(\"other\"); }\n", + }), + ) + .await; + + let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; + let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx); + + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + language_registry.add(rust_lang()); + + let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap()); + let worktree_id = worktree.update(cx, |worktree, _| worktree.id()); + + // Open three buffers + let buffer_1 = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "file1.rs"), cx) + }) + .await + .unwrap(); + let buffer_2 = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "file2.rs"), cx) + }) + .await + .unwrap(); + let buffer_3 = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "file3.rs"), cx) + }) + .await + .unwrap(); + + // Create a multi-buffer with all three buffers + let multi_buffer = cx.new(|cx| { + let mut multi_buffer = MultiBuffer::new(ReadWrite); + multi_buffer.push_excerpts( + buffer_1.clone(), + [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))], + cx, + ); + multi_buffer.push_excerpts( + buffer_2.clone(), + [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))], + cx, + ); + multi_buffer.push_excerpts( + buffer_3.clone(), + [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))], + cx, + ); + multi_buffer + }); + + let editor = cx.new_window_entity(|window, cx| { + Editor::new( + EditorMode::full(), + multi_buffer, + Some(project.clone()), + window, + cx, + ) + }); + + // Edit only the first buffer + editor.update_in(cx, |editor, window, cx| { + editor.change_selections( + SelectionEffects::scroll(Autoscroll::Next), + window, + cx, + |s| s.select_ranges(Some(10..10)), + ); + editor.insert("// edited", window, cx); + }); + + // Verify that only buffer 1 is dirty + buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty())); + buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty())); + buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty())); + + // Get write counts after file creation (files were created with initial content) + // We expect each file to have been written once during creation + let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs")); + let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs")); + let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs")); + + // Perform autosave + let save_task = editor.update_in(cx, |editor, window, cx| { + editor.save( + SaveOptions { + format: true, + autosave: true, + }, + project.clone(), + window, + cx, + ) + }); + save_task.await.unwrap(); + + // Only the dirty buffer should have been saved + assert_eq!( + fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1, + 1, + "Buffer 1 was dirty, so it should have been written once during autosave" + ); + assert_eq!( + fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2, + 0, + "Buffer 2 was clean, so it should not have been written during autosave" + ); + assert_eq!( + fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3, + 0, + "Buffer 3 was clean, so it should not have been written during autosave" + ); + + // Verify buffer states after autosave + buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty())); + buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty())); + buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty())); + + // Now perform a manual save (format = true) + let save_task = editor.update_in(cx, |editor, window, cx| { + editor.save( + SaveOptions { + format: true, + autosave: false, + }, + project.clone(), + window, + cx, + ) + }); + save_task.await.unwrap(); + + // During manual save, clean buffers don't get written to disk + // They just get did_save called for language server notifications + assert_eq!( + fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1, + 1, + "Buffer 1 should only have been written once total (during autosave, not manual save)" + ); + assert_eq!( + fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2, + 0, + "Buffer 2 should not have been written at all" + ); + assert_eq!( + fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3, + 0, + "Buffer 3 should not have been written at all" + ); +} + +async fn setup_range_format_test( + cx: &mut TestAppContext, +) -> ( + Entity, + Entity, + &mut gpui::VisualTestContext, + lsp::FakeLanguageServer, +) { init_test(cx, |_| {}); let fs = FakeFs::new(cx.executor()); @@ -8988,9 +10375,9 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) { FakeLspAdapter { capabilities: lsp::ServerCapabilities { document_range_formatting_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() + ..lsp::ServerCapabilities::default() }, - ..Default::default() + ..FakeLspAdapter::default() }, ); @@ -9005,17 +10392,33 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) { let (editor, cx) = cx.add_window_view(|window, cx| { build_editor_with_project(project.clone(), buffer, window, cx) }); + + cx.executor().start_waiting(); + let fake_server = fake_servers.next().await.unwrap(); + + (project, editor, cx, fake_server) +} + +#[gpui::test] +async fn test_range_format_on_save_success(cx: &mut TestAppContext) { + let (project, editor, cx, fake_server) = setup_range_format_test(cx).await; + editor.update_in(cx, |editor, window, cx| { editor.set_text("one\ntwo\nthree\n", window, cx) }); assert!(cx.read(|cx| editor.is_dirty(cx))); - cx.executor().start_waiting(); - let fake_server = fake_servers.next().await.unwrap(); - let save = editor .update_in(cx, |editor, window, cx| { - editor.save(true, project.clone(), window, cx) + editor.save( + SaveOptions { + format: true, + autosave: false, + }, + project.clone(), + window, + cx, + ) }) .unwrap(); fake_server @@ -9039,13 +10442,18 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) { "one, two\nthree\n" ); assert!(!cx.read(|cx| editor.is_dirty(cx))); +} + +#[gpui::test] +async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) { + let (project, editor, cx, fake_server) = setup_range_format_test(cx).await; editor.update_in(cx, |editor, window, cx| { editor.set_text("one\ntwo\nthree\n", window, cx) }); assert!(cx.read(|cx| editor.is_dirty(cx))); - // Ensure we can still save even if formatting hangs. + // Test that save still works when formatting hangs fake_server.set_request_handler::( move |params, _| async move { assert_eq!( @@ -9058,7 +10466,15 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) { ); let save = editor .update_in(cx, |editor, window, cx| { - editor.save(true, project.clone(), window, cx) + editor.save( + SaveOptions { + format: true, + autosave: false, + }, + project.clone(), + window, + cx, + ) }) .unwrap(); cx.executor().advance_clock(super::FORMAT_TIMEOUT); @@ -9069,24 +10485,43 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) { "one\ntwo\nthree\n" ); assert!(!cx.read(|cx| editor.is_dirty(cx))); +} - // For non-dirty buffer, no formatting request should be sent +#[gpui::test] +async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) { + let (project, editor, cx, fake_server) = setup_range_format_test(cx).await; + + // Buffer starts clean, no formatting should be requested let save = editor .update_in(cx, |editor, window, cx| { - editor.save(true, project.clone(), window, cx) + editor.save( + SaveOptions { + format: false, + autosave: false, + }, + project.clone(), + window, + cx, + ) }) .unwrap(); let _pending_format_request = fake_server .set_request_handler::(move |_, _| async move { - panic!("Should not be invoked on non-dirty buffer"); + panic!("Should not be invoked"); }) .next(); cx.executor().start_waiting(); save.await; + cx.run_until_parked(); +} + +#[gpui::test] +async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) { + let (project, editor, cx, fake_server) = setup_range_format_test(cx).await; // Set Rust language override and assert overridden tabsize is sent to language server update_test_language_settings(cx, |settings| { - settings.languages.insert( + settings.languages.0.insert( "Rust".into(), LanguageSettingsContent { tab_size: NonZeroU32::new(8), @@ -9096,12 +10531,20 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) { }); editor.update_in(cx, |editor, window, cx| { - editor.set_text("somehting_new\n", window, cx) + editor.set_text("something_new\n", window, cx) }); assert!(cx.read(|cx| editor.is_dirty(cx))); let save = editor .update_in(cx, |editor, window, cx| { - editor.save(true, project.clone(), window, cx) + editor.save( + SaveOptions { + format: true, + autosave: false, + }, + project.clone(), + window, + cx, + ) }) .unwrap(); fake_server @@ -9121,9 +10564,9 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) { #[gpui::test] async fn test_document_format_manual_trigger(cx: &mut TestAppContext) { init_test(cx, |settings| { - settings.defaults.formatter = Some(language_settings::SelectedFormatter::List( - FormatterList(vec![Formatter::LanguageServer { name: None }].into()), - )) + settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single( + Formatter::LanguageServer { name: None }, + ))) }); let fs = FakeFs::new(cx.executor()); @@ -9185,7 +10628,7 @@ async fn test_document_format_manual_trigger(cx: &mut TestAppContext) { editor.perform_format( project.clone(), FormatTrigger::Manual, - FormatTarget::Buffers, + FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()), window, cx, ) @@ -9231,7 +10674,7 @@ async fn test_document_format_manual_trigger(cx: &mut TestAppContext) { editor.perform_format( project, FormatTrigger::Manual, - FormatTarget::Buffers, + FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()), window, cx, ) @@ -9250,21 +10693,17 @@ async fn test_document_format_manual_trigger(cx: &mut TestAppContext) { async fn test_multiple_formatters(cx: &mut TestAppContext) { init_test(cx, |settings| { settings.defaults.remove_trailing_whitespace_on_save = Some(true); - settings.defaults.formatter = - Some(language_settings::SelectedFormatter::List(FormatterList( - vec![ - Formatter::LanguageServer { name: None }, - Formatter::CodeActions( - [ - ("code-action-1".into(), true), - ("code-action-2".into(), true), - ] - .into_iter() - .collect(), - ), + settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![ + Formatter::LanguageServer { name: None }, + Formatter::CodeActions( + [ + ("code-action-1".into(), true), + ("code-action-2".into(), true), ] - .into(), - ))) + .into_iter() + .collect(), + ), + ]))) }); let fs = FakeFs::new(cx.executor()); @@ -9345,7 +10784,7 @@ async fn test_multiple_formatters(cx: &mut TestAppContext) { kind: Some("code-action-2".into()), edit: Some(lsp::WorkspaceEdit::new( [( - uri.clone(), + uri, vec![lsp::TextEdit::new( lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)), "applied-code-action-2-edit\n".to_string(), @@ -9409,7 +10848,7 @@ async fn test_multiple_formatters(cx: &mut TestAppContext) { editor.perform_format( project.clone(), FormatTrigger::Manual, - FormatTarget::Buffers, + FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()), window, cx, ) @@ -9445,7 +10884,7 @@ async fn test_multiple_formatters(cx: &mut TestAppContext) { editor.perform_format( project.clone(), FormatTrigger::Manual, - FormatTarget::Buffers, + FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()), window, cx, ) @@ -9516,9 +10955,9 @@ async fn test_multiple_formatters(cx: &mut TestAppContext) { #[gpui::test] async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) { init_test(cx, |settings| { - settings.defaults.formatter = Some(language_settings::SelectedFormatter::List( - FormatterList(vec![Formatter::LanguageServer { name: None }].into()), - )) + settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![ + Formatter::LanguageServer { name: None }, + ]))) }); let fs = FakeFs::new(cx.executor()); @@ -9724,7 +11163,7 @@ async fn test_concurrent_format_requests(cx: &mut TestAppContext) { #[gpui::test] async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) { init_test(cx, |settings| { - settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto) + settings.defaults.formatter = Some(SelectedFormatter::Auto) }); let mut cx = EditorLspTestContext::new_rust( @@ -9979,9 +11418,10 @@ async fn test_handle_input_for_show_signature_help_auto_signature_help_true( cx.editor(|editor, _, _| { let signature_help_state = editor.signature_help_state.popover().cloned(); + let signature = signature_help_state.unwrap(); assert_eq!( - signature_help_state.unwrap().label, - "param1: u8, param2: u8" + signature.signatures[signature.current_signature].label, + "fn sample(param1: u8, param2: u8)" ); }); } @@ -10150,9 +11590,10 @@ async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestA cx.update_editor(|editor, _, _| { let signature_help_state = editor.signature_help_state.popover().cloned(); assert!(signature_help_state.is_some()); + let signature = signature_help_state.unwrap(); assert_eq!( - signature_help_state.unwrap().label, - "param1: u8, param2: u8" + signature.signatures[signature.current_signature].label, + "fn sample(param1: u8, param2: u8)" ); editor.signature_help_state = SignatureHelpState::default(); }); @@ -10191,9 +11632,10 @@ async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestA cx.editor(|editor, _, _| { let signature_help_state = editor.signature_help_state.popover().cloned(); assert!(signature_help_state.is_some()); + let signature = signature_help_state.unwrap(); assert_eq!( - signature_help_state.unwrap().label, - "param1: u8, param2: u8" + signature.signatures[signature.current_signature].label, + "fn sample(param1: u8, param2: u8)" ); }); } @@ -10252,9 +11694,10 @@ async fn test_signature_help(cx: &mut TestAppContext) { cx.editor(|editor, _, _| { let signature_help_state = editor.signature_help_state.popover().cloned(); assert!(signature_help_state.is_some()); + let signature = signature_help_state.unwrap(); assert_eq!( - signature_help_state.unwrap().label, - "param1: u8, param2: u8" + signature.signatures[signature.current_signature].label, + "fn sample(param1: u8, param2: u8)" ); }); @@ -10268,7 +11711,9 @@ async fn test_signature_help(cx: &mut TestAppContext) { "}); cx.update_editor(|editor, window, cx| { - editor.change_selections(None, window, cx, |s| s.select_ranges([0..0])); + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges([0..0]) + }); }); let mocked_response = lsp::SignatureHelp { @@ -10355,7 +11800,7 @@ async fn test_signature_help(cx: &mut TestAppContext) { // When selecting a range, the popover is gone. // Avoid using `cx.set_state` to not actually edit the document, just change its selections. cx.update_editor(|editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19))); }) }); @@ -10372,7 +11817,7 @@ async fn test_signature_help(cx: &mut TestAppContext) { // When unselecting again, the popover is back if within the brackets. cx.update_editor(|editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19))); }) }); @@ -10392,7 +11837,7 @@ async fn test_signature_help(cx: &mut TestAppContext) { // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape. cx.update_editor(|editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0))); s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19))); }) @@ -10433,7 +11878,7 @@ async fn test_signature_help(cx: &mut TestAppContext) { cx.condition(|editor, _| !editor.signature_help_state.is_shown()) .await; cx.update_editor(|editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19))); }) }); @@ -10445,7 +11890,7 @@ async fn test_signature_help(cx: &mut TestAppContext) { fn sample(param1: u8, param2: u8) {} "}); cx.update_editor(|editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19))); }) }); @@ -10460,6 +11905,132 @@ async fn test_signature_help(cx: &mut TestAppContext) { .await; } +#[gpui::test] +async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + signature_help_provider: Some(lsp::SignatureHelpOptions { + ..Default::default() + }), + ..Default::default() + }, + cx, + ) + .await; + + cx.set_state(indoc! {" + fn main() { + overloadedˇ + } + "}); + + cx.update_editor(|editor, window, cx| { + editor.handle_input("(", window, cx); + editor.show_signature_help(&ShowSignatureHelp, window, cx); + }); + + // Mock response with 3 signatures + let mocked_response = lsp::SignatureHelp { + signatures: vec![ + lsp::SignatureInformation { + label: "fn overloaded(x: i32)".to_string(), + documentation: None, + parameters: Some(vec![lsp::ParameterInformation { + label: lsp::ParameterLabel::Simple("x: i32".to_string()), + documentation: None, + }]), + active_parameter: None, + }, + lsp::SignatureInformation { + label: "fn overloaded(x: i32, y: i32)".to_string(), + documentation: None, + parameters: Some(vec![ + lsp::ParameterInformation { + label: lsp::ParameterLabel::Simple("x: i32".to_string()), + documentation: None, + }, + lsp::ParameterInformation { + label: lsp::ParameterLabel::Simple("y: i32".to_string()), + documentation: None, + }, + ]), + active_parameter: None, + }, + lsp::SignatureInformation { + label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(), + documentation: None, + parameters: Some(vec![ + lsp::ParameterInformation { + label: lsp::ParameterLabel::Simple("x: i32".to_string()), + documentation: None, + }, + lsp::ParameterInformation { + label: lsp::ParameterLabel::Simple("y: i32".to_string()), + documentation: None, + }, + lsp::ParameterInformation { + label: lsp::ParameterLabel::Simple("z: i32".to_string()), + documentation: None, + }, + ]), + active_parameter: None, + }, + ], + active_signature: Some(1), + active_parameter: Some(0), + }; + handle_signature_help_request(&mut cx, mocked_response).await; + + cx.condition(|editor, _| editor.signature_help_state.is_shown()) + .await; + + // Verify we have multiple signatures and the right one is selected + cx.editor(|editor, _, _| { + let popover = editor.signature_help_state.popover().cloned().unwrap(); + assert_eq!(popover.signatures.len(), 3); + // active_signature was 1, so that should be the current + assert_eq!(popover.current_signature, 1); + assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)"); + assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)"); + assert_eq!( + popover.signatures[2].label, + "fn overloaded(x: i32, y: i32, z: i32)" + ); + }); + + // Test navigation functionality + cx.update_editor(|editor, window, cx| { + editor.signature_help_next(&crate::SignatureHelpNext, window, cx); + }); + + cx.editor(|editor, _, _| { + let popover = editor.signature_help_state.popover().cloned().unwrap(); + assert_eq!(popover.current_signature, 2); + }); + + // Test wrap around + cx.update_editor(|editor, window, cx| { + editor.signature_help_next(&crate::SignatureHelpNext, window, cx); + }); + + cx.editor(|editor, _, _| { + let popover = editor.signature_help_state.popover().cloned().unwrap(); + assert_eq!(popover.current_signature, 0); + }); + + // Test previous navigation + cx.update_editor(|editor, window, cx| { + editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx); + }); + + cx.editor(|editor, _, _| { + let popover = editor.signature_help_state.popover().cloned().unwrap(); + assert_eq!(popover.current_signature, 2); + }); +} + #[gpui::test] async fn test_completion_mode(cx: &mut TestAppContext) { init_test(cx, |_| {}); @@ -10479,6 +12050,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) { run_description: &'static str, initial_state: String, buffer_marked_text: String, + completion_label: &'static str, completion_text: &'static str, expected_with_insert_mode: String, expected_with_replace_mode: String, @@ -10491,6 +12063,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) { run_description: "Start of word matches completion text", initial_state: "before ediˇ after".into(), buffer_marked_text: "before after".into(), + completion_label: "editor", completion_text: "editor", expected_with_insert_mode: "before editorˇ after".into(), expected_with_replace_mode: "before editorˇ after".into(), @@ -10501,6 +12074,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) { run_description: "Accept same text at the middle of the word", initial_state: "before ediˇtor after".into(), buffer_marked_text: "before after".into(), + completion_label: "editor", completion_text: "editor", expected_with_insert_mode: "before editorˇtor after".into(), expected_with_replace_mode: "before editorˇ after".into(), @@ -10511,6 +12085,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) { run_description: "End of word matches completion text -- cursor at end", initial_state: "before torˇ after".into(), buffer_marked_text: "before after".into(), + completion_label: "editor", completion_text: "editor", expected_with_insert_mode: "before editorˇ after".into(), expected_with_replace_mode: "before editorˇ after".into(), @@ -10521,6 +12096,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) { run_description: "End of word matches completion text -- cursor at start", initial_state: "before ˇtor after".into(), buffer_marked_text: "before <|tor> after".into(), + completion_label: "editor", completion_text: "editor", expected_with_insert_mode: "before editorˇtor after".into(), expected_with_replace_mode: "before editorˇ after".into(), @@ -10531,6 +12107,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) { run_description: "Prepend text containing whitespace", initial_state: "pˇfield: bool".into(), buffer_marked_text: ": bool".into(), + completion_label: "pub ", completion_text: "pub ", expected_with_insert_mode: "pub ˇfield: bool".into(), expected_with_replace_mode: "pub ˇ: bool".into(), @@ -10541,6 +12118,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) { run_description: "Add element to start of list", initial_state: "[element_ˇelement_2]".into(), buffer_marked_text: "[]".into(), + completion_label: "element_1", completion_text: "element_1", expected_with_insert_mode: "[element_1ˇelement_2]".into(), expected_with_replace_mode: "[element_1ˇ]".into(), @@ -10551,6 +12129,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) { run_description: "Add element to start of list -- first and second elements are equal", initial_state: "[elˇelement]".into(), buffer_marked_text: "[]".into(), + completion_label: "element", completion_text: "element", expected_with_insert_mode: "[elementˇelement]".into(), expected_with_replace_mode: "[elementˇ]".into(), @@ -10561,6 +12140,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) { run_description: "Ends with matching suffix", initial_state: "SubˇError".into(), buffer_marked_text: "".into(), + completion_label: "SubscriptionError", completion_text: "SubscriptionError", expected_with_insert_mode: "SubscriptionErrorˇError".into(), expected_with_replace_mode: "SubscriptionErrorˇ".into(), @@ -10571,6 +12151,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) { run_description: "Suffix is a subsequence -- contiguous", initial_state: "SubˇErr".into(), buffer_marked_text: "".into(), + completion_label: "SubscriptionError", completion_text: "SubscriptionError", expected_with_insert_mode: "SubscriptionErrorˇErr".into(), expected_with_replace_mode: "SubscriptionErrorˇ".into(), @@ -10581,6 +12162,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) { run_description: "Suffix is a subsequence -- non-contiguous -- replace intended", initial_state: "Suˇscrirr".into(), buffer_marked_text: "".into(), + completion_label: "SubscriptionError", completion_text: "SubscriptionError", expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(), expected_with_replace_mode: "SubscriptionErrorˇ".into(), @@ -10591,12 +12173,46 @@ async fn test_completion_mode(cx: &mut TestAppContext) { run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended", initial_state: "foo(indˇix)".into(), buffer_marked_text: "foo()".into(), + completion_label: "node_index", completion_text: "node_index", expected_with_insert_mode: "foo(node_indexˇix)".into(), expected_with_replace_mode: "foo(node_indexˇ)".into(), expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(), expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(), }, + Run { + run_description: "Replace range ends before cursor - should extend to cursor", + initial_state: "before editˇo after".into(), + buffer_marked_text: "before <{ed}>it|o after".into(), + completion_label: "editor", + completion_text: "editor", + expected_with_insert_mode: "before editorˇo after".into(), + expected_with_replace_mode: "before editorˇo after".into(), + expected_with_replace_subsequence_mode: "before editorˇo after".into(), + expected_with_replace_suffix_mode: "before editorˇo after".into(), + }, + Run { + run_description: "Uses label for suffix matching", + initial_state: "before ediˇtor after".into(), + buffer_marked_text: "before after".into(), + completion_label: "editor", + completion_text: "editor()", + expected_with_insert_mode: "before editor()ˇtor after".into(), + expected_with_replace_mode: "before editor()ˇ after".into(), + expected_with_replace_subsequence_mode: "before editor()ˇ after".into(), + expected_with_replace_suffix_mode: "before editor()ˇ after".into(), + }, + Run { + run_description: "Case insensitive subsequence and suffix matching", + initial_state: "before EDiˇtoR after".into(), + buffer_marked_text: "before after".into(), + completion_label: "editor", + completion_text: "editor", + expected_with_insert_mode: "before editorˇtoR after".into(), + expected_with_replace_mode: "before editorˇ after".into(), + expected_with_replace_subsequence_mode: "before editorˇ after".into(), + expected_with_replace_suffix_mode: "before editorˇ after".into(), + }, ]; for run in runs { @@ -10623,6 +12239,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) { settings.defaults.completions = Some(CompletionSettings { lsp_insert_mode, words: WordsCompletionMode::Disabled, + words_min_length: 0, lsp: true, lsp_fetch_timeout_ms: 0, }); @@ -10637,7 +12254,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) { handle_completion_request_with_insert_and_replace( &mut cx, &run.buffer_marked_text, - vec![run.completion_text], + vec![(run.completion_label, run.completion_text)], counter.clone(), ) .await; @@ -10681,6 +12298,7 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) update_test_language_settings(&mut cx, |settings| { settings.defaults.completions = Some(CompletionSettings { words: WordsCompletionMode::Disabled, + words_min_length: 0, // set the opposite here to ensure that the action is overriding the default behavior lsp_insert_mode: LspInsertMode::Insert, lsp: true, @@ -10696,8 +12314,8 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) let counter = Arc::new(AtomicUsize::new(0)); handle_completion_request_with_insert_and_replace( &mut cx, - &buffer_marked_text, - vec![completion_text], + buffer_marked_text, + vec![(completion_text, completion_text)], counter.clone(), ) .await; @@ -10710,13 +12328,14 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) .confirm_completion_replace(&ConfirmCompletionReplace, window, cx) .unwrap() }); - cx.assert_editor_state(&expected_with_replace_mode); + cx.assert_editor_state(expected_with_replace_mode); handle_resolve_completion_request(&mut cx, None).await; apply_additional_edits.await.unwrap(); update_test_language_settings(&mut cx, |settings| { settings.defaults.completions = Some(CompletionSettings { words: WordsCompletionMode::Disabled, + words_min_length: 0, // set the opposite here to ensure that the action is overriding the default behavior lsp_insert_mode: LspInsertMode::Replace, lsp: true, @@ -10730,8 +12349,8 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) }); handle_completion_request_with_insert_and_replace( &mut cx, - &buffer_marked_text, - vec![completion_text], + buffer_marked_text, + vec![(completion_text, completion_text)], counter.clone(), ) .await; @@ -10744,7 +12363,7 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) .confirm_completion_insert(&ConfirmCompletionInsert, window, cx) .unwrap() }); - cx.assert_editor_state(&expected_with_insert_mode); + cx.assert_editor_state(expected_with_insert_mode); handle_resolve_completion_request(&mut cx, None).await; apply_additional_edits.await.unwrap(); } @@ -10818,7 +12437,7 @@ async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut T handle_completion_request_with_insert_and_replace( &mut cx, completion_marked_buffer, - vec![completion_text], + vec![(completion_text, completion_text)], Arc::new(AtomicUsize::new(0)), ) .await; @@ -10872,7 +12491,7 @@ async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut T handle_completion_request_with_insert_and_replace( &mut cx, completion_marked_buffer, - vec![completion_text], + vec![(completion_text, completion_text)], Arc::new(AtomicUsize::new(0)), ) .await; @@ -10921,7 +12540,7 @@ async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut T handle_completion_request_with_insert_and_replace( &mut cx, completion_marked_buffer, - vec![completion_text], + vec![(completion_text, completion_text)], Arc::new(AtomicUsize::new(0)), ) .await; @@ -11056,7 +12675,7 @@ async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppConte let fake_server = fake_servers.next().await.unwrap(); editor.update_in(cx, |editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([ Point::new(1, 11)..Point::new(1, 11), Point::new(7, 11)..Point::new(7, 11), @@ -11139,14 +12758,15 @@ async fn test_completion(cx: &mut TestAppContext) { "}); cx.simulate_keystroke("."); handle_completion_request( - &mut cx, indoc! {" one.|<> two three "}, vec!["first_completion", "second_completion"], + true, counter.clone(), + &mut cx, ) .await; cx.condition(|editor, _| editor.context_menu_visible()) @@ -11246,7 +12866,6 @@ async fn test_completion(cx: &mut TestAppContext) { additional edit "}); handle_completion_request( - &mut cx, indoc! {" one.second_completion two s @@ -11254,7 +12873,9 @@ async fn test_completion(cx: &mut TestAppContext) { additional edit "}, vec!["fourth_completion", "fifth_completion", "sixth_completion"], + true, counter.clone(), + &mut cx, ) .await; cx.condition(|editor, _| editor.context_menu_visible()) @@ -11264,7 +12885,6 @@ async fn test_completion(cx: &mut TestAppContext) { cx.simulate_keystroke("i"); handle_completion_request( - &mut cx, indoc! {" one.second_completion two si @@ -11272,7 +12892,9 @@ async fn test_completion(cx: &mut TestAppContext) { additional edit "}, vec!["fourth_completion", "fifth_completion", "sixth_completion"], + true, counter.clone(), + &mut cx, ) .await; cx.condition(|editor, _| editor.context_menu_visible()) @@ -11306,10 +12928,11 @@ async fn test_completion(cx: &mut TestAppContext) { editor.show_completions(&ShowCompletions { trigger: None }, window, cx); }); handle_completion_request( - &mut cx, "editor.", vec!["close", "clobber"], + true, counter.clone(), + &mut cx, ) .await; cx.condition(|editor, _| editor.context_menu_visible()) @@ -11321,17 +12944,140 @@ async fn test_completion(cx: &mut TestAppContext) { .confirm_completion(&ConfirmCompletion::default(), window, cx) .unwrap() }); - cx.assert_editor_state("editor.closeˇ"); + cx.assert_editor_state("editor.clobberˇ"); handle_resolve_completion_request(&mut cx, None).await; apply_additional_edits.await.unwrap(); } +#[gpui::test] +async fn test_completion_reuse(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string()]), + ..Default::default() + }), + ..Default::default() + }, + cx, + ) + .await; + + let counter = Arc::new(AtomicUsize::new(0)); + cx.set_state("objˇ"); + cx.simulate_keystroke("."); + + // Initial completion request returns complete results + let is_incomplete = false; + handle_completion_request( + "obj.|<>", + vec!["a", "ab", "abc"], + is_incomplete, + counter.clone(), + &mut cx, + ) + .await; + cx.run_until_parked(); + assert_eq!(counter.load(atomic::Ordering::Acquire), 1); + cx.assert_editor_state("obj.ˇ"); + check_displayed_completions(vec!["a", "ab", "abc"], &mut cx); + + // Type "a" - filters existing completions + cx.simulate_keystroke("a"); + cx.run_until_parked(); + assert_eq!(counter.load(atomic::Ordering::Acquire), 1); + cx.assert_editor_state("obj.aˇ"); + check_displayed_completions(vec!["a", "ab", "abc"], &mut cx); + + // Type "b" - filters existing completions + cx.simulate_keystroke("b"); + cx.run_until_parked(); + assert_eq!(counter.load(atomic::Ordering::Acquire), 1); + cx.assert_editor_state("obj.abˇ"); + check_displayed_completions(vec!["ab", "abc"], &mut cx); + + // Type "c" - filters existing completions + cx.simulate_keystroke("c"); + cx.run_until_parked(); + assert_eq!(counter.load(atomic::Ordering::Acquire), 1); + cx.assert_editor_state("obj.abcˇ"); + check_displayed_completions(vec!["abc"], &mut cx); + + // Backspace to delete "c" - filters existing completions + cx.update_editor(|editor, window, cx| { + editor.backspace(&Backspace, window, cx); + }); + cx.run_until_parked(); + assert_eq!(counter.load(atomic::Ordering::Acquire), 1); + cx.assert_editor_state("obj.abˇ"); + check_displayed_completions(vec!["ab", "abc"], &mut cx); + + // Moving cursor to the left dismisses menu. + cx.update_editor(|editor, window, cx| { + editor.move_left(&MoveLeft, window, cx); + }); + cx.run_until_parked(); + assert_eq!(counter.load(atomic::Ordering::Acquire), 1); + cx.assert_editor_state("obj.aˇb"); + cx.update_editor(|editor, _, _| { + assert_eq!(editor.context_menu_visible(), false); + }); + + // Type "b" - new request + cx.simulate_keystroke("b"); + let is_incomplete = false; + handle_completion_request( + "obj.a", + vec!["ab", "abc"], + is_incomplete, + counter.clone(), + &mut cx, + ) + .await; + cx.run_until_parked(); + assert_eq!(counter.load(atomic::Ordering::Acquire), 2); + cx.assert_editor_state("obj.abˇb"); + check_displayed_completions(vec!["ab", "abc"], &mut cx); + + // Backspace to delete "b" - since query was "ab" and is now "a", new request is made. + cx.update_editor(|editor, window, cx| { + editor.backspace(&Backspace, window, cx); + }); + let is_incomplete = false; + handle_completion_request( + "obj.b", + vec!["a", "ab", "abc"], + is_incomplete, + counter.clone(), + &mut cx, + ) + .await; + cx.run_until_parked(); + assert_eq!(counter.load(atomic::Ordering::Acquire), 3); + cx.assert_editor_state("obj.aˇb"); + check_displayed_completions(vec!["a", "ab", "abc"], &mut cx); + + // Backspace to delete "a" - dismisses menu. + cx.update_editor(|editor, window, cx| { + editor.backspace(&Backspace, window, cx); + }); + cx.run_until_parked(); + assert_eq!(counter.load(atomic::Ordering::Acquire), 3); + cx.assert_editor_state("obj.ˇb"); + cx.update_editor(|editor, _, _| { + assert_eq!(editor.context_menu_visible(), false); + }); +} + #[gpui::test] async fn test_word_completion(cx: &mut TestAppContext) { let lsp_fetch_timeout_ms = 10; init_test(cx, |language_settings| { language_settings.defaults.completions = Some(CompletionSettings { words: WordsCompletionMode::Fallback, + words_min_length: 0, lsp: true, lsp_fetch_timeout_ms: 10, lsp_insert_mode: LspInsertMode::Insert, @@ -11392,7 +13138,7 @@ async fn test_word_completion(cx: &mut TestAppContext) { if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() { assert_eq!( - completion_menu_entries(&menu), + completion_menu_entries(menu), &["first", "last"], "When LSP server is fast to reply, no fallback word completions are used" ); @@ -11415,7 +13161,7 @@ async fn test_word_completion(cx: &mut TestAppContext) { cx.update_editor(|editor, _, _| { if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() { - assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"], + assert_eq!(completion_menu_entries(menu), &["one", "three", "two"], "When LSP server is slow, document words can be shown instead, if configured accordingly"); } else { panic!("expected completion menu to be open"); @@ -11428,6 +13174,7 @@ async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext init_test(cx, |language_settings| { language_settings.defaults.completions = Some(CompletionSettings { words: WordsCompletionMode::Enabled, + words_min_length: 0, lsp: true, lsp_fetch_timeout_ms: 0, lsp_insert_mode: LspInsertMode::Insert, @@ -11476,7 +13223,7 @@ async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() { assert_eq!( - completion_menu_entries(&menu), + completion_menu_entries(menu), &["first", "last", "second"], "Word completions that has the same edit as the any of the LSP ones, should not be proposed" ); @@ -11491,6 +13238,7 @@ async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) { init_test(cx, |language_settings| { language_settings.defaults.completions = Some(CompletionSettings { words: WordsCompletionMode::Disabled, + words_min_length: 0, lsp: true, lsp_fetch_timeout_ms: 0, lsp_insert_mode: LspInsertMode::Insert, @@ -11532,7 +13280,7 @@ async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) { if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() { assert_eq!( - completion_menu_entries(&menu), + completion_menu_entries(menu), &["first", "last", "second"], "`ShowWordCompletions` action should show word completions" ); @@ -11549,7 +13297,7 @@ async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) { if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() { assert_eq!( - completion_menu_entries(&menu), + completion_menu_entries(menu), &["last"], "After showing word completions, further editing should filter them and not query the LSP" ); @@ -11564,6 +13312,7 @@ async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) { init_test(cx, |language_settings| { language_settings.defaults.completions = Some(CompletionSettings { words: WordsCompletionMode::Fallback, + words_min_length: 0, lsp: false, lsp_fetch_timeout_ms: 0, lsp_insert_mode: LspInsertMode::Insert, @@ -11588,7 +13337,7 @@ async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) { if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() { assert_eq!( - completion_menu_entries(&menu), + completion_menu_entries(menu), &["let"], "With no digits in the completion query, no digits should be in the word completions" ); @@ -11613,7 +13362,7 @@ async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) { cx.update_editor(|editor, _, _| { if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() { - assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \ + assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \ return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)"); } else { panic!("expected completion menu to be open"); @@ -11621,6 +13370,56 @@ async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) { }); } +#[gpui::test] +async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) { + init_test(cx, |language_settings| { + language_settings.defaults.completions = Some(CompletionSettings { + words: WordsCompletionMode::Enabled, + words_min_length: 3, + lsp: true, + lsp_fetch_timeout_ms: 0, + lsp_insert_mode: LspInsertMode::Insert, + }); + }); + + let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await; + cx.set_state(indoc! {"ˇ + wow + wowen + wowser + "}); + cx.simulate_keystroke("w"); + cx.executor().run_until_parked(); + cx.update_editor(|editor, _, _| { + if editor.context_menu.borrow_mut().is_some() { + panic!( + "expected completion menu to be hidden, as words completion threshold is not met" + ); + } + }); + + cx.simulate_keystroke("o"); + cx.executor().run_until_parked(); + cx.update_editor(|editor, _, _| { + if editor.context_menu.borrow_mut().is_some() { + panic!( + "expected completion menu to be hidden, as words completion threshold is not met still" + ); + } + }); + + cx.simulate_keystroke("w"); + cx.executor().run_until_parked(); + cx.update_editor(|editor, _, _| { + if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() + { + assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word"); + } else { + panic!("expected completion menu to be open after the word completions threshold is met"); + } + }); +} + fn gen_text_edit(params: &CompletionParams, text: &str) -> Option { let position = || lsp::Position { line: params.text_document_position.position.line, @@ -11850,7 +13649,7 @@ async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) { cx.update_editor(|editor, _, _| { if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() { - assert_eq!(completion_menu_entries(&menu), &["first", "last"]); + assert_eq!(completion_menu_entries(menu), &["first", "last"]); } else { panic!("expected completion menu to be open"); } @@ -11939,6 +13738,178 @@ async fn test_as_is_completions(cx: &mut TestAppContext) { cx.assert_editor_state("fn a() {}\n unsafeˇ"); } +#[gpui::test] +async fn test_panic_during_c_completions(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let language = + Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap(); + let mut cx = EditorLspTestContext::new( + language, + lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + ..lsp::CompletionOptions::default() + }), + ..lsp::ServerCapabilities::default() + }, + cx, + ) + .await; + + cx.set_state( + "#ifndef BAR_H +#define BAR_H + +#include + +int fn_branch(bool do_branch1, bool do_branch2); + +#endif // BAR_H +ˇ", + ); + cx.executor().run_until_parked(); + cx.update_editor(|editor, window, cx| { + editor.handle_input("#", window, cx); + }); + cx.executor().run_until_parked(); + cx.update_editor(|editor, window, cx| { + editor.handle_input("i", window, cx); + }); + cx.executor().run_until_parked(); + cx.update_editor(|editor, window, cx| { + editor.handle_input("n", window, cx); + }); + cx.executor().run_until_parked(); + cx.assert_editor_state( + "#ifndef BAR_H +#define BAR_H + +#include + +int fn_branch(bool do_branch1, bool do_branch2); + +#endif // BAR_H +#inˇ", + ); + + cx.lsp + .set_request_handler::(move |_, _| async move { + Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList { + is_incomplete: false, + item_defaults: None, + items: vec![lsp::CompletionItem { + kind: Some(lsp::CompletionItemKind::SNIPPET), + label_details: Some(lsp::CompletionItemLabelDetails { + detail: Some("header".to_string()), + description: None, + }), + label: " include".to_string(), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: lsp::Range { + start: lsp::Position { + line: 8, + character: 1, + }, + end: lsp::Position { + line: 8, + character: 1, + }, + }, + new_text: "include \"$0\"".to_string(), + })), + sort_text: Some("40b67681include".to_string()), + insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), + filter_text: Some("include".to_string()), + insert_text: Some("include \"$0\"".to_string()), + ..lsp::CompletionItem::default() + }], + }))) + }); + cx.update_editor(|editor, window, cx| { + editor.show_completions(&ShowCompletions { trigger: None }, window, cx); + }); + cx.executor().run_until_parked(); + cx.update_editor(|editor, window, cx| { + editor.confirm_completion(&ConfirmCompletion::default(), window, cx) + }); + cx.executor().run_until_parked(); + cx.assert_editor_state( + "#ifndef BAR_H +#define BAR_H + +#include + +int fn_branch(bool do_branch1, bool do_branch2); + +#endif // BAR_H +#include \"ˇ\"", + ); + + cx.lsp + .set_request_handler::(move |_, _| async move { + Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList { + is_incomplete: true, + item_defaults: None, + items: vec![lsp::CompletionItem { + kind: Some(lsp::CompletionItemKind::FILE), + label: "AGL/".to_string(), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: lsp::Range { + start: lsp::Position { + line: 8, + character: 10, + }, + end: lsp::Position { + line: 8, + character: 11, + }, + }, + new_text: "AGL/".to_string(), + })), + sort_text: Some("40b67681AGL/".to_string()), + insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT), + filter_text: Some("AGL/".to_string()), + insert_text: Some("AGL/".to_string()), + ..lsp::CompletionItem::default() + }], + }))) + }); + cx.update_editor(|editor, window, cx| { + editor.show_completions(&ShowCompletions { trigger: None }, window, cx); + }); + cx.executor().run_until_parked(); + cx.update_editor(|editor, window, cx| { + editor.confirm_completion(&ConfirmCompletion::default(), window, cx) + }); + cx.executor().run_until_parked(); + cx.assert_editor_state( + r##"#ifndef BAR_H +#define BAR_H + +#include + +int fn_branch(bool do_branch1, bool do_branch2); + +#endif // BAR_H +#include "AGL/ˇ"##, + ); + + cx.update_editor(|editor, window, cx| { + editor.handle_input("\"", window, cx); + }); + cx.executor().run_until_parked(); + cx.assert_editor_state( + r##"#ifndef BAR_H +#define BAR_H + +#include + +int fn_branch(bool do_branch1, bool do_branch2); + +#endif // BAR_H +#include "AGL/"ˇ"##, + ); +} + #[gpui::test] async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) { init_test(cx, |_| {}); @@ -12006,9 +13977,11 @@ async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) { let task_completion_item = closure_completion_item.clone(); counter_clone.fetch_add(1, atomic::Ordering::Release); async move { - Ok(Some(lsp::CompletionResponse::Array(vec![ - task_completion_item, - ]))) + Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList { + is_incomplete: true, + item_defaults: None, + items: vec![task_completion_item], + }))) } }); @@ -12422,7 +14395,12 @@ async fn test_toggle_block_comment(cx: &mut TestAppContext) { Language::new( LanguageConfig { name: "HTML".into(), - block_comment: Some(("".into())), + block_comment: Some(BlockCommentConfig { + start: "".into(), + tab_size: 0, + }), ..Default::default() }, Some(tree_sitter_html::LANGUAGE.into()), @@ -12447,7 +14425,7 @@ async fn test_toggle_block_comment(cx: &mut TestAppContext) { )); cx.language_registry().add(html_language.clone()); - cx.language_registry().add(javascript_language.clone()); + cx.language_registry().add(javascript_language); cx.update_buffer(|buffer, cx| { buffer.set_language(Some(html_language), cx); }); @@ -12569,7 +14547,7 @@ fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) { let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx)); editor.update_in(cx, |editor, window, cx| { assert_eq!(editor.text(cx), "aaaa\nbbbb"); - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([ Point::new(0, 0)..Point::new(0, 0), Point::new(1, 0)..Point::new(1, 0), @@ -12587,7 +14565,7 @@ fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) { ); // Ensure the cursor's head is respected when deleting across an excerpt boundary. - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([Point::new(0, 2)..Point::new(1, 2)]) }); editor.backspace(&Default::default(), window, cx); @@ -12597,7 +14575,7 @@ fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) { [Point::new(1, 0)..Point::new(1, 0)] ); - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([Point::new(1, 1)..Point::new(0, 1)]) }); editor.backspace(&Default::default(), window, cx); @@ -12624,7 +14602,7 @@ fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) { ); let excerpt_ranges = markers.into_iter().map(|marker| { let context = excerpt_ranges.remove(&marker).unwrap()[0].clone(); - ExcerptRange::new(context.clone()) + ExcerptRange::new(context) }); let buffer = cx.new(|cx| Buffer::local(initial_text, cx)); let multibuffer = cx.new(|cx| { @@ -12645,7 +14623,9 @@ fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) { true, ); assert_eq!(editor.text(cx), expected_text); - editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges)); + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges(selection_ranges) + }); editor.handle_input("X", window, cx); @@ -12706,7 +14686,7 @@ fn test_refresh_selections(cx: &mut TestAppContext) { let editor = cx.add_window(|window, cx| { let mut editor = build_editor(multibuffer.clone(), window, cx); let snapshot = editor.snapshot(window, cx); - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([Point::new(1, 3)..Point::new(1, 3)]) }); editor.begin_selection( @@ -12728,7 +14708,7 @@ fn test_refresh_selections(cx: &mut TestAppContext) { // Refreshing selections is a no-op when excerpts haven't changed. _ = editor.update(cx, |editor, window, cx| { - editor.change_selections(None, window, cx, |s| s.refresh()); + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh()); assert_eq!( editor.selections.ranges(cx), [ @@ -12753,7 +14733,7 @@ fn test_refresh_selections(cx: &mut TestAppContext) { // Refreshing selections will relocate the first selection to the original buffer // location. - editor.change_selections(None, window, cx, |s| s.refresh()); + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh()); assert_eq!( editor.selections.ranges(cx), [ @@ -12815,7 +14795,7 @@ fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) { ); // Ensure we don't panic when selections are refreshed and that the pending selection is finalized. - editor.change_selections(None, window, cx, |s| s.refresh()); + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh()); assert_eq!( editor.selections.ranges(cx), [Point::new(0, 3)..Point::new(0, 3)] @@ -12874,7 +14854,7 @@ async fn test_extra_newline_insertion(cx: &mut TestAppContext) { .await; editor.update_in(cx, |editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_display_ranges([ DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3), DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5), @@ -12907,7 +14887,7 @@ fn test_highlighted_ranges(cx: &mut TestAppContext) { let editor = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx); - build_editor(buffer.clone(), window, cx) + build_editor(buffer, window, cx) }); _ = editor.update(cx, |editor, window, cx| { @@ -12944,7 +14924,7 @@ fn test_highlighted_ranges(cx: &mut TestAppContext) { let mut highlighted_ranges = editor.background_highlights_in_range( anchor_range(Point::new(3, 4)..Point::new(7, 4)), &snapshot, - cx.theme().colors(), + cx.theme(), ); // Enforce a consistent ordering based on color without relying on the ordering of the // highlight's `TypeId` which is non-executor. @@ -12974,7 +14954,7 @@ fn test_highlighted_ranges(cx: &mut TestAppContext) { editor.background_highlights_in_range( anchor_range(Point::new(5, 6)..Point::new(6, 4)), &snapshot, - cx.theme().colors(), + cx.theme(), ), &[( DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5), @@ -13053,7 +15033,9 @@ async fn test_following(cx: &mut TestAppContext) { // Update the selections only _ = leader.update(cx, |leader, window, cx| { - leader.change_selections(None, window, cx, |s| s.select_ranges([1..1])); + leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges([1..1]) + }); }); follower .update(cx, |follower, window, cx| { @@ -13101,7 +15083,9 @@ async fn test_following(cx: &mut TestAppContext) { // Update the selections and scroll position. The follower's scroll position is updated // via autoscroll, not via the leader's exact scroll position. _ = leader.update(cx, |leader, window, cx| { - leader.change_selections(None, window, cx, |s| s.select_ranges([0..0])); + leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges([0..0]) + }); leader.request_autoscroll(Autoscroll::newest(), cx); leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx); }); @@ -13125,7 +15109,9 @@ async fn test_following(cx: &mut TestAppContext) { // Creating a pending selection that precedes another selection _ = leader.update(cx, |leader, window, cx| { - leader.change_selections(None, window, cx, |s| s.select_ranges([1..1])); + leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges([1..1]) + }); leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx); }); follower @@ -13356,7 +15342,7 @@ async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mu let mut cx = EditorTestContext::new(cx).await; let lsp_store = - cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store()); + cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store()); cx.set_state(indoc! {" ˇfn func(abc def: i32) -> u32 { @@ -13398,6 +15384,8 @@ async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mu }, ], }, + None, + DiagnosticSourceKind::Pushed, &[], cx, ) @@ -13408,7 +15396,7 @@ async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mu executor.run_until_parked(); cx.update_editor(|editor, window, cx| { - editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx); + editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx); }); cx.assert_editor_state(indoc! {" @@ -13417,7 +15405,7 @@ async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mu "}); cx.update_editor(|editor, window, cx| { - editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx); + editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx); }); cx.assert_editor_state(indoc! {" @@ -13426,7 +15414,7 @@ async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mu "}); cx.update_editor(|editor, window, cx| { - editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx); + editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx); }); cx.assert_editor_state(indoc! {" @@ -13435,7 +15423,7 @@ async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mu "}); cx.update_editor(|editor, window, cx| { - editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx); + editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx); }); cx.assert_editor_state(indoc! {" @@ -13779,7 +15767,7 @@ async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) { editor_handle.update_in(cx, |editor, window, cx| { window.focus(&editor.focus_handle(cx)); - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([Point::new(0, 21)..Point::new(0, 20)]) }); editor.handle_input("{", window, cx); @@ -13796,6 +15784,57 @@ async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) { }); } +#[gpui::test(iterations = 20, seeds(31))] +async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions { + first_trigger_character: ".".to_string(), + more_trigger_character: None, + }), + ..Default::default() + }, + cx, + ) + .await; + + cx.update_buffer(|buffer, _| { + // This causes autoindent to be async. + buffer.set_sync_parse_timeout(Duration::ZERO) + }); + + cx.set_state("fn c() {\n d()ˇ\n}\n"); + cx.simulate_keystroke("\n"); + cx.run_until_parked(); + + let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap()); + let mut request = + cx.set_request_handler::(move |_, _, mut cx| { + let buffer_cloned = buffer_cloned.clone(); + async move { + buffer_cloned.update(&mut cx, |buffer, _| { + assert_eq!( + buffer.text(), + "fn c() {\n d()\n .\n}\n", + "OnTypeFormatting should triggered after autoindent applied" + ) + })?; + + Ok(Some(vec![])) + } + }); + + cx.simulate_keystroke("."); + cx.run_until_parked(); + + cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n"); + assert!(request.next().await.is_some()); + request.close(); + assert!(request.next().await.is_none()); +} + #[gpui::test] async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) { init_test(cx, |_| {}); @@ -13856,7 +15895,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppCon .unwrap(); let _fake_server = fake_servers.next().await.unwrap(); update_test_language_settings(cx, |language_settings| { - language_settings.languages.insert( + language_settings.languages.0.insert( language_name.clone(), LanguageSettingsContent { tab_size: NonZeroU32::new(8), @@ -14633,7 +16672,7 @@ async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) // Completions that have already been resolved are skipped. assert_eq!( *resolved_items.lock(), - items[items.len() - 16..items.len() - 4] + items[items.len() - 17..items.len() - 4] .iter() .cloned() .map(|mut item| { @@ -14712,8 +16751,8 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestA if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() { assert_eq!( - completion_menu_entries(&menu), - &["bg-red", "bg-blue", "bg-yellow"] + completion_menu_entries(menu), + &["bg-blue", "bg-red", "bg-yellow"] ); } else { panic!("expected completion menu to be open"); @@ -14725,7 +16764,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestA cx.update_editor(|editor, _, _| { if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() { - assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]); + assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]); } else { panic!("expected completion menu to be open"); } @@ -14739,7 +16778,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestA cx.update_editor(|editor, _, _| { if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() { - assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]); + assert_eq!(completion_menu_entries(menu), &["bg-yellow"]); } else { panic!("expected completion menu to be open"); } @@ -14754,9 +16793,9 @@ fn completion_menu_entries(menu: &CompletionsMenu) -> Vec { #[gpui::test] async fn test_document_format_with_prettier(cx: &mut TestAppContext) { init_test(cx, |settings| { - settings.defaults.formatter = Some(language_settings::SelectedFormatter::List( - FormatterList(vec![Formatter::Prettier].into()), - )) + settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single( + Formatter::Prettier, + ))) }); let fs = FakeFs::new(cx.executor()); @@ -14812,7 +16851,7 @@ async fn test_document_format_with_prettier(cx: &mut TestAppContext) { editor.perform_format( project.clone(), FormatTrigger::Manual, - FormatTarget::Buffers, + FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()), window, cx, ) @@ -14826,13 +16865,13 @@ async fn test_document_format_with_prettier(cx: &mut TestAppContext) { ); update_test_language_settings(cx, |settings| { - settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto) + settings.defaults.formatter = Some(SelectedFormatter::Auto) }); let format = editor.update_in(cx, |editor, window, cx| { editor.perform_format( project.clone(), FormatTrigger::Manual, - FormatTarget::Buffers, + FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()), window, cx, ) @@ -15308,7 +17347,7 @@ async fn test_multibuffer_reverts(cx: &mut TestAppContext) { (buffer_2.clone(), base_text_2), (buffer_3.clone(), base_text_3), ] { - let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx)); + let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx)); editor .buffer .update(cx, |buffer, cx| buffer.add_diff(diff, cx)); @@ -15342,7 +17381,7 @@ async fn test_multibuffer_reverts(cx: &mut TestAppContext) { }); editor.update_in(cx, |editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0))); }); editor.git_restore(&Default::default(), window, cx); @@ -15381,7 +17420,7 @@ async fn test_multibuffer_reverts(cx: &mut TestAppContext) { } #[gpui::test] -async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) { +async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) { init_test(cx, |_| {}); let cols = 4; @@ -15486,9 +17525,12 @@ async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) { cx.executor().run_until_parked(); multi_buffer_editor.update_in(cx, |editor, window, cx| { - editor.change_selections(Some(Autoscroll::Next), window, cx, |s| { - s.select_ranges(Some(1..2)) - }); + editor.change_selections( + SelectionEffects::scroll(Autoscroll::Next), + window, + cx, + |s| s.select_ranges(Some(1..2)), + ); editor.open_excerpts(&OpenExcerpts, window, cx); }); cx.executor().run_until_parked(); @@ -15538,9 +17580,12 @@ async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) { .unwrap(); multi_buffer_editor.update_in(cx, |editor, window, cx| { - editor.change_selections(Some(Autoscroll::Next), window, cx, |s| { - s.select_ranges(Some(39..40)) - }); + editor.change_selections( + SelectionEffects::scroll(Autoscroll::Next), + window, + cx, + |s| s.select_ranges(Some(39..40)), + ); editor.open_excerpts(&OpenExcerpts, window, cx); }); cx.executor().run_until_parked(); @@ -15594,9 +17639,12 @@ async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) { .unwrap(); multi_buffer_editor.update_in(cx, |editor, window, cx| { - editor.change_selections(Some(Autoscroll::Next), window, cx, |s| { - s.select_ranges(Some(70..70)) - }); + editor.change_selections( + SelectionEffects::scroll(Autoscroll::Next), + window, + cx, + |s| s.select_ranges(Some(70..70)), + ); editor.open_excerpts(&OpenExcerpts, window, cx); }); cx.executor().run_until_parked(); @@ -15920,7 +17968,7 @@ async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) { (buffer_2.clone(), file_2_old), (buffer_3.clone(), file_3_old), ] { - let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx)); + let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx)); editor .buffer .update(cx, |buffer, cx| buffer.add_diff(diff, cx)); @@ -17082,6 +19130,64 @@ async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) { ); } +#[gpui::test] +async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) { + let (buffer_id, mut cx) = setup_indent_guides_editor( + &" + function component() { + \treturn ( + \t\t\t + \t\t

+ \t\t\t + \t\t
+ \t) + }" + .unindent(), + cx, + ) + .await; + + assert_indent_guides( + 0..8, + vec![ + indent_guide(buffer_id, 1, 6, 0), + indent_guide(buffer_id, 2, 5, 1), + indent_guide(buffer_id, 4, 4, 2), + ], + None, + &mut cx, + ); +} + +#[gpui::test] +async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) { + let (buffer_id, mut cx) = setup_indent_guides_editor( + &" + function component() { + \treturn ( + \t + \t\t
+ \t\t\t + \t\t
+ \t) + }" + .unindent(), + cx, + ) + .await; + + assert_indent_guides( + 0..8, + vec![ + indent_guide(buffer_id, 1, 6, 0), + indent_guide(buffer_id, 2, 5, 1), + indent_guide(buffer_id, 4, 4, 2), + ], + None, + &mut cx, + ); +} + #[gpui::test] async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) { let (buffer_id, mut cx) = setup_indent_guides_editor( @@ -17140,7 +19246,7 @@ async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) { .await; cx.update_editor(|editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([Point::new(1, 0)..Point::new(1, 0)]) }); }); @@ -17168,7 +19274,7 @@ async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext .await; cx.update_editor(|editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([Point::new(1, 0)..Point::new(1, 0)]) }); }); @@ -17184,7 +19290,7 @@ async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext ); cx.update_editor(|editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([Point::new(2, 0)..Point::new(2, 0)]) }); }); @@ -17200,7 +19306,7 @@ async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext ); cx.update_editor(|editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([Point::new(3, 0)..Point::new(3, 0)]) }); }); @@ -17231,7 +19337,7 @@ async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) { .await; cx.update_editor(|editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([Point::new(2, 0)..Point::new(2, 0)]) }); }); @@ -17257,7 +19363,7 @@ async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) { .await; cx.update_editor(|editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([Point::new(1, 0)..Point::new(1, 0)]) }); }); @@ -17407,7 +19513,7 @@ async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestApp let buffer_id = hunks[0].buffer_id; hunks .into_iter() - .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone())) + .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range)) .collect::>() }); assert_eq!(hunk_ranges.len(), 2); @@ -17498,7 +19604,7 @@ async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestApp let buffer_id = hunks[0].buffer_id; hunks .into_iter() - .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone())) + .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range)) .collect::>() }); assert_eq!(hunk_ranges.len(), 2); @@ -17564,7 +19670,7 @@ async fn test_toggle_deletion_hunk_at_start_of_file( let buffer_id = hunks[0].buffer_id; hunks .into_iter() - .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone())) + .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range)) .collect::>() }); assert_eq!(hunk_ranges.len(), 1); @@ -17587,7 +19693,7 @@ async fn test_toggle_deletion_hunk_at_start_of_file( }); executor.run_until_parked(); - cx.assert_state_with_diff(hunk_expanded.clone()); + cx.assert_state_with_diff(hunk_expanded); } #[gpui::test] @@ -17613,6 +19719,7 @@ async fn test_display_diff_hunks(cx: &mut TestAppContext) { ("file-2".into(), "two\n".into()), ("file-3".into(), "three\n".into()), ], + "deadbeef", ); let project = Project::test(fs, [path!("/test").as_ref()], cx).await; @@ -17786,13 +19893,8 @@ fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) { editor.insert_creases(Some(crease), cx); let snapshot = editor.snapshot(window, cx); - let _div = snapshot.render_crease_toggle( - MultiBufferRow(1), - false, - cx.entity().clone(), - window, - cx, - ); + let _div = + snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx); snapshot }) .unwrap(); @@ -18194,14 +20296,14 @@ async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) { ); // Test finding task when cursor is inside function body - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([Point::new(4, 5)..Point::new(4, 5)]) }); let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap(); assert_eq!(row, 3, "Should find task for cursor inside runnable_1"); // Test finding task when cursor is on function name - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([Point::new(8, 4)..Point::new(8, 4)]) }); let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap(); @@ -18355,7 +20457,7 @@ async fn test_folding_buffers(cx: &mut TestAppContext) { .collect::(), "bbbb" ); - editor.change_selections(None, window, cx, |selections| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| { selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]); }); editor.handle_input("B", window, cx); @@ -18582,7 +20684,9 @@ async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut Test HighlightStyle::color(Hsla::green()), cx, ); - editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range))); + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges(Some(highlight_range)) + }); }); let full_text = format!("\n\n{sample_text}"); @@ -18779,7 +20883,7 @@ async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContex } #[gpui::test] -async fn test_inline_completion_text(cx: &mut TestAppContext) { +async fn test_edit_prediction_text(cx: &mut TestAppContext) { init_test(cx, |_| {}); // Simple insertion @@ -18878,7 +20982,7 @@ async fn test_inline_completion_text(cx: &mut TestAppContext) { } #[gpui::test] -async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) { +async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) { init_test(cx, |_| {}); // Deletion @@ -18968,8 +21072,8 @@ async fn assert_highlighted_edits( .await; cx.update(|_window, cx| { - let highlighted_edits = inline_completion_edit_text( - &snapshot.as_singleton().unwrap().2, + let highlighted_edits = edit_prediction_edit_text( + snapshot.as_singleton().unwrap().2, &edits, &edit_preview, include_deletions, @@ -18985,13 +21089,13 @@ fn assert_breakpoint( path: &Arc, expected: Vec<(u32, Breakpoint)>, ) { - if expected.len() == 0usize { + if expected.is_empty() { assert!(!breakpoints.contains_key(path), "{}", path.display()); } else { let mut breakpoint = breakpoints .get(path) .unwrap() - .into_iter() + .iter() .map(|breakpoint| { ( breakpoint.row, @@ -19020,13 +21124,7 @@ fn add_log_breakpoint_at_cursor( let (anchor, bp) = editor .breakpoints_at_cursors(window, cx) .first() - .and_then(|(anchor, bp)| { - if let Some(bp) = bp { - Some((*anchor, bp.clone())) - } else { - None - } - }) + .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone()))) .unwrap_or_else(|| { let cursor_position: Point = editor.selections.newest(cx).head(); @@ -19036,7 +21134,7 @@ fn add_log_breakpoint_at_cursor( .buffer_snapshot .anchor_before(Point::new(cursor_position.row, 0)); - (breakpoint_position, Breakpoint::new_log(&log_message)) + (breakpoint_position, Breakpoint::new_log(log_message)) }); editor.edit_breakpoint_at_anchor( @@ -19104,7 +21202,7 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) { let abs_path = project.read_with(cx, |project, cx| { project .absolute_path(&project_path, cx) - .map(|path_buf| Arc::from(path_buf.to_owned())) + .map(Arc::from) .unwrap() }); @@ -19122,7 +21220,6 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_eq!(1, breakpoints.len()); @@ -19147,7 +21244,6 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_eq!(1, breakpoints.len()); @@ -19169,7 +21265,6 @@ async fn test_breakpoint_toggling(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_eq!(0, breakpoints.len()); @@ -19221,7 +21316,7 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) { let abs_path = project.read_with(cx, |project, cx| { project .absolute_path(&project_path, cx) - .map(|path_buf| Arc::from(path_buf.to_owned())) + .map(Arc::from) .unwrap() }); @@ -19236,7 +21331,6 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_breakpoint( @@ -19257,7 +21351,6 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_breakpoint(&breakpoints, &abs_path, vec![]); @@ -19277,7 +21370,6 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_breakpoint( @@ -19300,7 +21392,6 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_breakpoint( @@ -19323,7 +21414,6 @@ async fn test_log_breakpoint_editing(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_breakpoint( @@ -19396,7 +21486,7 @@ async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) { let abs_path = project.read_with(cx, |project, cx| { project .absolute_path(&project_path, cx) - .map(|path_buf| Arc::from(path_buf.to_owned())) + .map(Arc::from) .unwrap() }); @@ -19416,7 +21506,6 @@ async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_eq!(1, breakpoints.len()); @@ -19448,7 +21537,6 @@ async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); let disable_breakpoint = { @@ -19484,7 +21572,6 @@ async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) { .unwrap() .read(cx) .all_source_breakpoints(cx) - .clone() }); assert_eq!(1, breakpoints.len()); @@ -19520,7 +21607,7 @@ async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) { let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx)); editor.highlight_background::( &[highlight_range], - |c| c.editor_document_highlight_read_background, + |theme| theme.colors().editor_document_highlight_read_background, cx, ); }); @@ -19598,7 +21685,7 @@ async fn test_rename_without_prepare(cx: &mut TestAppContext) { let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx)); editor.highlight_background::( &[highlight_range], - |c| c.editor_document_highlight_read_background, + |theme| theme.colors().editor_document_highlight_read_background, cx, ); }); @@ -19740,16 +21827,32 @@ async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContex }, ); - let (buffer, _handle) = project - .update(cx, |p, cx| { - p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx) + let editor = workspace + .update(cx, |workspace, window, cx| { + workspace.open_abs_path( + PathBuf::from(path!("/dir/a.ts")), + OpenOptions::default(), + window, + cx, + ) }) + .unwrap() .await + .unwrap() + .downcast::() .unwrap(); cx.executor().run_until_parked(); let fake_server = fake_language_servers.next().await.unwrap(); + let buffer = editor.update(cx, |editor, cx| { + editor + .buffer() + .read(cx) + .as_singleton() + .expect("have opened a single file by path") + }); + let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot()); let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left); drop(buffer_snapshot); @@ -19807,7 +21910,7 @@ async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContex assert_eq!( actions.len(), 1, - "Should have only one valid action for the 0..0 range" + "Should have only one valid action for the 0..0 range, got: {actions:#?}" ); let action = actions[0].clone(); let apply = project.update(cx, |project, cx| { @@ -19853,7 +21956,7 @@ async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContex .into_iter() .collect(), ), - ..Default::default() + ..lsp::WorkspaceEdit::default() }, }, ) @@ -19876,6 +21979,38 @@ async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContex buffer.undo(cx); assert_eq!(buffer.text(), "a"); }); + + let actions_after_edits = cx + .update_window(*workspace, |_, window, cx| { + project.code_actions(&buffer, anchor..anchor, window, cx) + }) + .unwrap() + .await + .unwrap(); + assert_eq!( + actions, actions_after_edits, + "For the same selection, same code lens actions should be returned" + ); + + let _responses = + fake_server.set_request_handler::(|_, _| async move { + panic!("No more code lens requests are expected"); + }); + editor.update_in(cx, |editor, window, cx| { + editor.select_all(&SelectAll, window, cx); + }); + cx.executor().run_until_parked(); + let new_actions = cx + .update_window(*workspace, |_, window, cx| { + project.code_actions(&buffer, anchor..anchor, window, cx) + }) + .unwrap() + .await + .unwrap(); + assert_eq!( + actions, new_actions, + "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now" + ); } #[gpui::test] @@ -19952,7 +22087,7 @@ println!("5"); }) }); editor_1.update_in(cx, |editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges(expected_ranges.clone()); }); }); @@ -20015,8 +22150,7 @@ println!("5"); .unwrap(); pane_1 .update_in(cx, |pane, window, cx| { - pane.close_inactive_items(&CloseInactiveItems::default(), window, cx) - .unwrap() + pane.close_other_items(&CloseOtherItems::default(), None, window, cx) }) .await .unwrap(); @@ -20052,8 +22186,7 @@ println!("5"); .unwrap(); pane_2 .update_in(cx, |pane, window, cx| { - pane.close_inactive_items(&CloseInactiveItems::default(), window, cx) - .unwrap() + pane.close_other_items(&CloseOtherItems::default(), None, window, cx) }) .await .unwrap(); @@ -20229,7 +22362,6 @@ println!("5"); }); pane.update_in(cx, |pane, window, cx| { pane.close_all_items(&CloseAllItems::default(), window, cx) - .unwrap() }) .await .unwrap(); @@ -20401,7 +22533,7 @@ async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) { let fake_server = fake_servers.next().await.unwrap(); editor.update_in(cx, |editor, window, cx| { editor.set_text("", window, cx); - editor.change_selections(None, window, cx, |selections| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| { selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]); }); let Some((buffer, _)) = editor @@ -20418,10 +22550,7 @@ async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) { let closing_range = buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8)); let mut linked_ranges = HashMap::default(); - linked_ranges.insert( - buffer_id, - vec![(opening_range.clone(), vec![closing_range.clone()])], - ); + linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]); editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges); }); let mut completion_handle = @@ -20583,11 +22712,10 @@ async fn test_invisible_worktree_servers(cx: &mut TestAppContext) { pane.update_in(cx, |pane, window, cx| { pane.close_active_item(&CloseActiveItem::default(), window, cx) }) - .unwrap() .await .unwrap(); pane.update_in(cx, |pane, window, cx| { - pane.navigate_backward(window, cx); + pane.navigate_backward(&Default::default(), window, cx); }); cx.run_until_parked(); pane.update(cx, |pane, cx| { @@ -20607,7 +22735,7 @@ async fn test_invisible_worktree_servers(cx: &mut TestAppContext) { ); cx.update(|_, cx| { - workspace::reload(&workspace::Reload::default(), cx); + workspace::reload(cx); }); assert_language_servers_count( 1, @@ -20684,9 +22812,9 @@ async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestApp cx.set_state(indoc! {" def main(): ˇ try: - ˇ fetch() + ˇ fetch() ˇ except ValueError: - ˇ handle_error() + ˇ handle_error() ˇ else: ˇ match value: ˇ case _: @@ -20814,74 +22942,101 @@ async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) { finally:ˇ "}); - // TODO: test `except` auto outdents when typed inside `try` block right after for block - // cx.set_state(indoc! {" - // def main(): - // try: - // for i in range(n): - // pass - // ˇ - // "}); - // cx.update_editor(|editor, window, cx| { - // editor.handle_input("except:", window, cx); - // }); - // cx.assert_editor_state(indoc! {" - // def main(): - // try: - // for i in range(n): - // pass - // except:ˇ - // "}); + // test `else` does not outdents when typed inside `except` block right after for block + cx.set_state(indoc! {" + def main(): + try: + i = 2 + except: + for i in range(n): + pass + ˇ + "}); + cx.update_editor(|editor, window, cx| { + editor.handle_input("else:", window, cx); + }); + cx.assert_editor_state(indoc! {" + def main(): + try: + i = 2 + except: + for i in range(n): + pass + else:ˇ + "}); - // TODO: test `else` auto outdents when typed inside `except` block right after for block - // cx.set_state(indoc! {" - // def main(): - // try: - // i = 2 - // except: - // for i in range(n): - // pass - // ˇ - // "}); - // cx.update_editor(|editor, window, cx| { - // editor.handle_input("else:", window, cx); - // }); - // cx.assert_editor_state(indoc! {" - // def main(): - // try: - // i = 2 - // except: - // for i in range(n): - // pass - // else:ˇ - // "}); + // test `finally` auto outdents when typed inside `else` block right after for block + cx.set_state(indoc! {" + def main(): + try: + i = 2 + except: + j = 2 + else: + for i in range(n): + pass + ˇ + "}); + cx.update_editor(|editor, window, cx| { + editor.handle_input("finally:", window, cx); + }); + cx.assert_editor_state(indoc! {" + def main(): + try: + i = 2 + except: + j = 2 + else: + for i in range(n): + pass + finally:ˇ + "}); - // TODO: test `finally` auto outdents when typed inside `else` block right after for block - // cx.set_state(indoc! {" - // def main(): - // try: - // i = 2 - // except: - // j = 2 - // else: - // for i in range(n): - // pass - // ˇ - // "}); - // cx.update_editor(|editor, window, cx| { - // editor.handle_input("finally:", window, cx); - // }); - // cx.assert_editor_state(indoc! {" - // def main(): - // try: - // i = 2 - // except: - // j = 2 - // else: - // for i in range(n): - // pass - // finally:ˇ - // "}); + // test `except` outdents to inner "try" block + cx.set_state(indoc! {" + def main(): + try: + i = 2 + if i == 2: + try: + i = 3 + ˇ + "}); + cx.update_editor(|editor, window, cx| { + editor.handle_input("except:", window, cx); + }); + cx.assert_editor_state(indoc! {" + def main(): + try: + i = 2 + if i == 2: + try: + i = 3 + except:ˇ + "}); + + // test `except` outdents to outer "try" block + cx.set_state(indoc! {" + def main(): + try: + i = 2 + if i == 2: + try: + i = 3 + ˇ + "}); + cx.update_editor(|editor, window, cx| { + editor.handle_input("except:", window, cx); + }); + cx.assert_editor_state(indoc! {" + def main(): + try: + i = 2 + if i == 2: + try: + i = 3 + except:ˇ + "}); // test `else` stays at correct indent when typed after `for` block cx.set_state(indoc! {" @@ -20914,6 +23069,19 @@ async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) { def f() -> list[str]: aˇ "}); + + // test does not outdent on typing : after case keyword + cx.set_state(indoc! {" + match 1: + caseˇ + "}); + cx.update_editor(|editor, window, cx| { + editor.handle_input(":", window, cx); + }); + cx.assert_editor_state(indoc! {" + match 1: + case:ˇ + "}); } #[gpui::test] @@ -20979,11 +23147,441 @@ async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) { "}); } +#[gpui::test] +async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into()); + cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + + // test cursor move to start of each line on tab + // for `if`, `elif`, `else`, `while`, `for`, `case` and `function` + cx.set_state(indoc! {" + function main() { + ˇ for item in $items; do + ˇ while [ -n \"$item\" ]; do + ˇ if [ \"$value\" -gt 10 ]; then + ˇ continue + ˇ elif [ \"$value\" -lt 0 ]; then + ˇ break + ˇ else + ˇ echo \"$item\" + ˇ fi + ˇ done + ˇ done + ˇ} + "}); + cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx)); + cx.assert_editor_state(indoc! {" + function main() { + ˇfor item in $items; do + ˇwhile [ -n \"$item\" ]; do + ˇif [ \"$value\" -gt 10 ]; then + ˇcontinue + ˇelif [ \"$value\" -lt 0 ]; then + ˇbreak + ˇelse + ˇecho \"$item\" + ˇfi + ˇdone + ˇdone + ˇ} + "}); + // test relative indent is preserved when tab + cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx)); + cx.assert_editor_state(indoc! {" + function main() { + ˇfor item in $items; do + ˇwhile [ -n \"$item\" ]; do + ˇif [ \"$value\" -gt 10 ]; then + ˇcontinue + ˇelif [ \"$value\" -lt 0 ]; then + ˇbreak + ˇelse + ˇecho \"$item\" + ˇfi + ˇdone + ˇdone + ˇ} + "}); + + // test cursor move to start of each line on tab + // for `case` statement with patterns + cx.set_state(indoc! {" + function handle() { + ˇ case \"$1\" in + ˇ start) + ˇ echo \"a\" + ˇ ;; + ˇ stop) + ˇ echo \"b\" + ˇ ;; + ˇ *) + ˇ echo \"c\" + ˇ ;; + ˇ esac + ˇ} + "}); + cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx)); + cx.assert_editor_state(indoc! {" + function handle() { + ˇcase \"$1\" in + ˇstart) + ˇecho \"a\" + ˇ;; + ˇstop) + ˇecho \"b\" + ˇ;; + ˇ*) + ˇecho \"c\" + ˇ;; + ˇesac + ˇ} + "}); +} + +#[gpui::test] +async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into()); + cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + + // test indents on comment insert + cx.set_state(indoc! {" + function main() { + ˇ for item in $items; do + ˇ while [ -n \"$item\" ]; do + ˇ if [ \"$value\" -gt 10 ]; then + ˇ continue + ˇ elif [ \"$value\" -lt 0 ]; then + ˇ break + ˇ else + ˇ echo \"$item\" + ˇ fi + ˇ done + ˇ done + ˇ} + "}); + cx.update_editor(|e, window, cx| e.handle_input("#", window, cx)); + cx.assert_editor_state(indoc! {" + function main() { + #ˇ for item in $items; do + #ˇ while [ -n \"$item\" ]; do + #ˇ if [ \"$value\" -gt 10 ]; then + #ˇ continue + #ˇ elif [ \"$value\" -lt 0 ]; then + #ˇ break + #ˇ else + #ˇ echo \"$item\" + #ˇ fi + #ˇ done + #ˇ done + #ˇ} + "}); +} + +#[gpui::test] +async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into()); + cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + + // test `else` auto outdents when typed inside `if` block + cx.set_state(indoc! {" + if [ \"$1\" = \"test\" ]; then + echo \"foo bar\" + ˇ + "}); + cx.update_editor(|editor, window, cx| { + editor.handle_input("else", window, cx); + }); + cx.assert_editor_state(indoc! {" + if [ \"$1\" = \"test\" ]; then + echo \"foo bar\" + elseˇ + "}); + + // test `elif` auto outdents when typed inside `if` block + cx.set_state(indoc! {" + if [ \"$1\" = \"test\" ]; then + echo \"foo bar\" + ˇ + "}); + cx.update_editor(|editor, window, cx| { + editor.handle_input("elif", window, cx); + }); + cx.assert_editor_state(indoc! {" + if [ \"$1\" = \"test\" ]; then + echo \"foo bar\" + elifˇ + "}); + + // test `fi` auto outdents when typed inside `else` block + cx.set_state(indoc! {" + if [ \"$1\" = \"test\" ]; then + echo \"foo bar\" + else + echo \"bar baz\" + ˇ + "}); + cx.update_editor(|editor, window, cx| { + editor.handle_input("fi", window, cx); + }); + cx.assert_editor_state(indoc! {" + if [ \"$1\" = \"test\" ]; then + echo \"foo bar\" + else + echo \"bar baz\" + fiˇ + "}); + + // test `done` auto outdents when typed inside `while` block + cx.set_state(indoc! {" + while read line; do + echo \"$line\" + ˇ + "}); + cx.update_editor(|editor, window, cx| { + editor.handle_input("done", window, cx); + }); + cx.assert_editor_state(indoc! {" + while read line; do + echo \"$line\" + doneˇ + "}); + + // test `done` auto outdents when typed inside `for` block + cx.set_state(indoc! {" + for file in *.txt; do + cat \"$file\" + ˇ + "}); + cx.update_editor(|editor, window, cx| { + editor.handle_input("done", window, cx); + }); + cx.assert_editor_state(indoc! {" + for file in *.txt; do + cat \"$file\" + doneˇ + "}); + + // test `esac` auto outdents when typed inside `case` block + cx.set_state(indoc! {" + case \"$1\" in + start) + echo \"foo bar\" + ;; + stop) + echo \"bar baz\" + ;; + ˇ + "}); + cx.update_editor(|editor, window, cx| { + editor.handle_input("esac", window, cx); + }); + cx.assert_editor_state(indoc! {" + case \"$1\" in + start) + echo \"foo bar\" + ;; + stop) + echo \"bar baz\" + ;; + esacˇ + "}); + + // test `*)` auto outdents when typed inside `case` block + cx.set_state(indoc! {" + case \"$1\" in + start) + echo \"foo bar\" + ;; + ˇ + "}); + cx.update_editor(|editor, window, cx| { + editor.handle_input("*)", window, cx); + }); + cx.assert_editor_state(indoc! {" + case \"$1\" in + start) + echo \"foo bar\" + ;; + *)ˇ + "}); + + // test `fi` outdents to correct level with nested if blocks + cx.set_state(indoc! {" + if [ \"$1\" = \"test\" ]; then + echo \"outer if\" + if [ \"$2\" = \"debug\" ]; then + echo \"inner if\" + ˇ + "}); + cx.update_editor(|editor, window, cx| { + editor.handle_input("fi", window, cx); + }); + cx.assert_editor_state(indoc! {" + if [ \"$1\" = \"test\" ]; then + echo \"outer if\" + if [ \"$2\" = \"debug\" ]; then + echo \"inner if\" + fiˇ + "}); +} + +#[gpui::test] +async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + update_test_language_settings(cx, |settings| { + settings.defaults.extend_comment_on_newline = Some(false); + }); + let mut cx = EditorTestContext::new(cx).await; + let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into()); + cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + + // test correct indent after newline on comment + cx.set_state(indoc! {" + # COMMENT:ˇ + "}); + cx.update_editor(|editor, window, cx| { + editor.newline(&Newline, window, cx); + }); + cx.assert_editor_state(indoc! {" + # COMMENT: + ˇ + "}); + + // test correct indent after newline after `then` + cx.set_state(indoc! {" + + if [ \"$1\" = \"test\" ]; thenˇ + "}); + cx.update_editor(|editor, window, cx| { + editor.newline(&Newline, window, cx); + }); + cx.run_until_parked(); + cx.assert_editor_state(indoc! {" + + if [ \"$1\" = \"test\" ]; then + ˇ + "}); + + // test correct indent after newline after `else` + cx.set_state(indoc! {" + if [ \"$1\" = \"test\" ]; then + elseˇ + "}); + cx.update_editor(|editor, window, cx| { + editor.newline(&Newline, window, cx); + }); + cx.run_until_parked(); + cx.assert_editor_state(indoc! {" + if [ \"$1\" = \"test\" ]; then + else + ˇ + "}); + + // test correct indent after newline after `elif` + cx.set_state(indoc! {" + if [ \"$1\" = \"test\" ]; then + elifˇ + "}); + cx.update_editor(|editor, window, cx| { + editor.newline(&Newline, window, cx); + }); + cx.run_until_parked(); + cx.assert_editor_state(indoc! {" + if [ \"$1\" = \"test\" ]; then + elif + ˇ + "}); + + // test correct indent after newline after `do` + cx.set_state(indoc! {" + for file in *.txt; doˇ + "}); + cx.update_editor(|editor, window, cx| { + editor.newline(&Newline, window, cx); + }); + cx.run_until_parked(); + cx.assert_editor_state(indoc! {" + for file in *.txt; do + ˇ + "}); + + // test correct indent after newline after case pattern + cx.set_state(indoc! {" + case \"$1\" in + start)ˇ + "}); + cx.update_editor(|editor, window, cx| { + editor.newline(&Newline, window, cx); + }); + cx.run_until_parked(); + cx.assert_editor_state(indoc! {" + case \"$1\" in + start) + ˇ + "}); + + // test correct indent after newline after case pattern + cx.set_state(indoc! {" + case \"$1\" in + start) + ;; + *)ˇ + "}); + cx.update_editor(|editor, window, cx| { + editor.newline(&Newline, window, cx); + }); + cx.run_until_parked(); + cx.assert_editor_state(indoc! {" + case \"$1\" in + start) + ;; + *) + ˇ + "}); + + // test correct indent after newline after function opening brace + cx.set_state(indoc! {" + function test() {ˇ} + "}); + cx.update_editor(|editor, window, cx| { + editor.newline(&Newline, window, cx); + }); + cx.run_until_parked(); + cx.assert_editor_state(indoc! {" + function test() { + ˇ + } + "}); + + // test no extra indent after semicolon on same line + cx.set_state(indoc! {" + echo \"test\";ˇ + "}); + cx.update_editor(|editor, window, cx| { + editor.newline(&Newline, window, cx); + }); + cx.run_until_parked(); + cx.assert_editor_state(indoc! {" + echo \"test\"; + ˇ + "}); +} + fn empty_range(row: usize, column: usize) -> Range { let point = DisplayPoint::new(DisplayRow(row as u32), column as u32); point..point } +#[track_caller] fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context) { let (text, ranges) = marked_text_ranges(marked_text, true); assert_eq!(editor.text(cx), text); @@ -21010,6 +23608,22 @@ pub fn handle_signature_help_request( } } +#[track_caller] +pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) { + cx.update_editor(|editor, _, _| { + if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() { + let entries = menu.entries.borrow(); + let entries = entries + .iter() + .map(|entry| entry.string.as_str()) + .collect::>(); + assert_eq!(entries, expected); + } else { + panic!("Expected completions menu"); + } + }); +} + /// Handle completion request passing a marked string specifying where the completion /// should be triggered from using '|' character, what range should be replaced, and what completions /// should be returned using '<' and '>' to delimit the range. @@ -21017,10 +23631,11 @@ pub fn handle_signature_help_request( /// Also see `handle_completion_request_with_insert_and_replace`. #[track_caller] pub fn handle_completion_request( - cx: &mut EditorLspTestContext, marked_string: &str, completions: Vec<&'static str>, + is_incomplete: bool, counter: Arc, + cx: &mut EditorLspTestContext, ) -> impl Future { let complete_from_marker: TextRangeMarker = '|'.into(); let replace_range_marker: TextRangeMarker = ('<', '>').into(); @@ -21044,8 +23659,10 @@ pub fn handle_completion_request( params.text_document_position.position, complete_from_position ); - Ok(Some(lsp::CompletionResponse::Array( - completions + Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList { + is_incomplete, + item_defaults: None, + items: completions .iter() .map(|completion_text| lsp::CompletionItem { label: completion_text.to_string(), @@ -21056,7 +23673,7 @@ pub fn handle_completion_request( ..Default::default() }) .collect(), - ))) + }))) } }); @@ -21068,19 +23685,27 @@ pub fn handle_completion_request( /// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be /// given instead, which also contains an `insert` range. /// -/// This function uses the cursor position to mimic what Rust-Analyzer provides as the `insert` range, -/// that is, `replace_range.start..cursor_pos`. +/// This function uses markers to define ranges: +/// - `|` marks the cursor position +/// - `<>` marks the replace range +/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides) pub fn handle_completion_request_with_insert_and_replace( cx: &mut EditorLspTestContext, marked_string: &str, - completions: Vec<&'static str>, + completions: Vec<(&'static str, &'static str)>, // (label, new_text) counter: Arc, ) -> impl Future { let complete_from_marker: TextRangeMarker = '|'.into(); let replace_range_marker: TextRangeMarker = ('<', '>').into(); + let insert_range_marker: TextRangeMarker = ('{', '}').into(); + let (_, mut marked_ranges) = marked_text_ranges_by( marked_string, - vec![complete_from_marker.clone(), replace_range_marker.clone()], + vec![ + complete_from_marker.clone(), + replace_range_marker.clone(), + insert_range_marker.clone(), + ], ); let complete_from_position = @@ -21088,6 +23713,14 @@ pub fn handle_completion_request_with_insert_and_replace( let replace_range = cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone()); + let insert_range = match marked_ranges.remove(&insert_range_marker) { + Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()), + _ => lsp::Range { + start: replace_range.start, + end: complete_from_position, + }, + }; + let mut request = cx.set_request_handler::(move |url, params, _| { let completions = completions.clone(); @@ -21101,16 +23734,13 @@ pub fn handle_completion_request_with_insert_and_replace( Ok(Some(lsp::CompletionResponse::Array( completions .iter() - .map(|completion_text| lsp::CompletionItem { - label: completion_text.to_string(), + .map(|(label, new_text)| lsp::CompletionItem { + label: label.to_string(), text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace( lsp::InsertReplaceEdit { - insert: lsp::Range { - start: replace_range.start, - end: complete_from_position, - }, + insert: insert_range, replace: replace_range, - new_text: completion_text.to_string(), + new_text: new_text.to_string(), }, )), ..Default::default() @@ -21191,7 +23821,7 @@ pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsC workspace::init_settings(cx); crate::init(cx); }); - + zlog::init_test(); update_test_language_settings(cx, f); } @@ -21222,3 +23852,545 @@ fn assert_hunk_revert( cx.assert_editor_state(expected_reverted_text_with_selections); assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before); } + +#[gpui::test(iterations = 10)] +async fn test_pulling_diagnostics(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let diagnostic_requests = Arc::new(AtomicUsize::new(0)); + let counter = diagnostic_requests.clone(); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + path!("/a"), + json!({ + "first.rs": "fn main() { let a = 5; }", + "second.rs": "// Test file", + }), + ) + .await; + + let project = Project::test(fs, [path!("/a").as_ref()], cx).await; + let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + language_registry.add(rust_lang()); + let mut fake_servers = language_registry.register_fake_lsp( + "Rust", + FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options( + lsp::DiagnosticOptions { + identifier: None, + inter_file_dependencies: true, + workspace_diagnostics: true, + work_done_progress_options: Default::default(), + }, + )), + ..Default::default() + }, + ..Default::default() + }, + ); + + let editor = workspace + .update(cx, |workspace, window, cx| { + workspace.open_abs_path( + PathBuf::from(path!("/a/first.rs")), + OpenOptions::default(), + window, + cx, + ) + }) + .unwrap() + .await + .unwrap() + .downcast::() + .unwrap(); + let fake_server = fake_servers.next().await.unwrap(); + let server_id = fake_server.server.server_id(); + let mut first_request = fake_server + .set_request_handler::(move |params, _| { + let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1; + let result_id = Some(new_result_id.to_string()); + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path(path!("/a/first.rs")).unwrap() + ); + async move { + Ok(lsp::DocumentDiagnosticReportResult::Report( + lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport { + related_documents: None, + full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport { + items: Vec::new(), + result_id, + }, + }), + )) + } + }); + + let ensure_result_id = |expected: Option, cx: &mut TestAppContext| { + project.update(cx, |project, cx| { + let buffer_id = editor + .read(cx) + .buffer() + .read(cx) + .as_singleton() + .expect("created a singleton buffer") + .read(cx) + .remote_id(); + let buffer_result_id = project + .lsp_store() + .read(cx) + .result_id(server_id, buffer_id, cx); + assert_eq!(expected, buffer_result_id); + }); + }; + + ensure_result_id(None, cx); + cx.executor().advance_clock(Duration::from_millis(60)); + cx.executor().run_until_parked(); + assert_eq!( + diagnostic_requests.load(atomic::Ordering::Acquire), + 1, + "Opening file should trigger diagnostic request" + ); + first_request + .next() + .await + .expect("should have sent the first diagnostics pull request"); + ensure_result_id(Some("1".to_string()), cx); + + // Editing should trigger diagnostics + editor.update_in(cx, |editor, window, cx| { + editor.handle_input("2", window, cx) + }); + cx.executor().advance_clock(Duration::from_millis(60)); + cx.executor().run_until_parked(); + assert_eq!( + diagnostic_requests.load(atomic::Ordering::Acquire), + 2, + "Editing should trigger diagnostic request" + ); + ensure_result_id(Some("2".to_string()), cx); + + // Moving cursor should not trigger diagnostic request + editor.update_in(cx, |editor, window, cx| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges([Point::new(0, 0)..Point::new(0, 0)]) + }); + }); + cx.executor().advance_clock(Duration::from_millis(60)); + cx.executor().run_until_parked(); + assert_eq!( + diagnostic_requests.load(atomic::Ordering::Acquire), + 2, + "Cursor movement should not trigger diagnostic request" + ); + ensure_result_id(Some("2".to_string()), cx); + // Multiple rapid edits should be debounced + for _ in 0..5 { + editor.update_in(cx, |editor, window, cx| { + editor.handle_input("x", window, cx) + }); + } + cx.executor().advance_clock(Duration::from_millis(60)); + cx.executor().run_until_parked(); + + let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire); + assert!( + final_requests <= 4, + "Multiple rapid edits should be debounced (got {final_requests} requests)", + ); + ensure_result_id(Some(final_requests.to_string()), cx); +} + +#[gpui::test] +async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) { + // Regression test for issue #11671 + // Previously, adding a cursor after moving multiple cursors would reset + // the cursor count instead of adding to the existing cursors. + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + + // Create a simple buffer with cursor at start + cx.set_state(indoc! {" + ˇaaaa + bbbb + cccc + dddd + eeee + ffff + gggg + hhhh"}); + + // Add 2 cursors below (so we have 3 total) + cx.update_editor(|editor, window, cx| { + editor.add_selection_below(&Default::default(), window, cx); + editor.add_selection_below(&Default::default(), window, cx); + }); + + // Verify we have 3 cursors + let initial_count = cx.update_editor(|editor, _, _| editor.selections.count()); + assert_eq!( + initial_count, 3, + "Should have 3 cursors after adding 2 below" + ); + + // Move down one line + cx.update_editor(|editor, window, cx| { + editor.move_down(&MoveDown, window, cx); + }); + + // Add another cursor below + cx.update_editor(|editor, window, cx| { + editor.add_selection_below(&Default::default(), window, cx); + }); + + // Should now have 4 cursors (3 original + 1 new) + let final_count = cx.update_editor(|editor, _, _| editor.selections.count()); + assert_eq!( + final_count, 4, + "Should have 4 cursors after moving and adding another" + ); +} + +#[gpui::test(iterations = 10)] +async fn test_document_colors(cx: &mut TestAppContext) { + let expected_color = Rgba { + r: 0.33, + g: 0.33, + b: 0.33, + a: 0.33, + }; + + init_test(cx, |_| {}); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + path!("/a"), + json!({ + "first.rs": "fn main() { let a = 5; }", + }), + ) + .await; + + let project = Project::test(fs, [path!("/a").as_ref()], cx).await; + let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + language_registry.add(rust_lang()); + let mut fake_servers = language_registry.register_fake_lsp( + "Rust", + FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + color_provider: Some(lsp::ColorProviderCapability::Simple(true)), + ..lsp::ServerCapabilities::default() + }, + name: "rust-analyzer", + ..FakeLspAdapter::default() + }, + ); + let mut fake_servers_without_capabilities = language_registry.register_fake_lsp( + "Rust", + FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + color_provider: Some(lsp::ColorProviderCapability::Simple(false)), + ..lsp::ServerCapabilities::default() + }, + name: "not-rust-analyzer", + ..FakeLspAdapter::default() + }, + ); + + let editor = workspace + .update(cx, |workspace, window, cx| { + workspace.open_abs_path( + PathBuf::from(path!("/a/first.rs")), + OpenOptions::default(), + window, + cx, + ) + }) + .unwrap() + .await + .unwrap() + .downcast::() + .unwrap(); + let fake_language_server = fake_servers.next().await.unwrap(); + let fake_language_server_without_capabilities = + fake_servers_without_capabilities.next().await.unwrap(); + let requests_made = Arc::new(AtomicUsize::new(0)); + let closure_requests_made = Arc::clone(&requests_made); + let mut color_request_handle = fake_language_server + .set_request_handler::(move |params, _| { + let requests_made = Arc::clone(&closure_requests_made); + async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path(path!("/a/first.rs")).unwrap() + ); + requests_made.fetch_add(1, atomic::Ordering::Release); + Ok(vec![ + lsp::ColorInformation { + range: lsp::Range { + start: lsp::Position { + line: 0, + character: 0, + }, + end: lsp::Position { + line: 0, + character: 1, + }, + }, + color: lsp::Color { + red: 0.33, + green: 0.33, + blue: 0.33, + alpha: 0.33, + }, + }, + lsp::ColorInformation { + range: lsp::Range { + start: lsp::Position { + line: 0, + character: 0, + }, + end: lsp::Position { + line: 0, + character: 1, + }, + }, + color: lsp::Color { + red: 0.33, + green: 0.33, + blue: 0.33, + alpha: 0.33, + }, + }, + ]) + } + }); + + let _handle = fake_language_server_without_capabilities + .set_request_handler::(move |_, _| async move { + panic!("Should not be called"); + }); + cx.executor().advance_clock(Duration::from_millis(100)); + color_request_handle.next().await.unwrap(); + cx.run_until_parked(); + assert_eq!( + 1, + requests_made.load(atomic::Ordering::Acquire), + "Should query for colors once per editor open" + ); + editor.update_in(cx, |editor, _, cx| { + assert_eq!( + vec![expected_color], + extract_color_inlays(editor, cx), + "Should have an initial inlay" + ); + }); + + // opening another file in a split should not influence the LSP query counter + workspace + .update(cx, |workspace, window, cx| { + assert_eq!( + workspace.panes().len(), + 1, + "Should have one pane with one editor" + ); + workspace.move_item_to_pane_in_direction( + &MoveItemToPaneInDirection { + direction: SplitDirection::Right, + focus: false, + clone: true, + }, + window, + cx, + ); + }) + .unwrap(); + cx.run_until_parked(); + workspace + .update(cx, |workspace, _, cx| { + let panes = workspace.panes(); + assert_eq!(panes.len(), 2, "Should have two panes after splitting"); + for pane in panes { + let editor = pane + .read(cx) + .active_item() + .and_then(|item| item.downcast::()) + .expect("Should have opened an editor in each split"); + let editor_file = editor + .read(cx) + .buffer() + .read(cx) + .as_singleton() + .expect("test deals with singleton buffers") + .read(cx) + .file() + .expect("test buffese should have a file") + .path(); + assert_eq!( + editor_file.as_ref(), + Path::new("first.rs"), + "Both editors should be opened for the same file" + ) + } + }) + .unwrap(); + + cx.executor().advance_clock(Duration::from_millis(500)); + let save = editor.update_in(cx, |editor, window, cx| { + editor.move_to_end(&MoveToEnd, window, cx); + editor.handle_input("dirty", window, cx); + editor.save( + SaveOptions { + format: true, + autosave: true, + }, + project.clone(), + window, + cx, + ) + }); + save.await.unwrap(); + + color_request_handle.next().await.unwrap(); + cx.run_until_parked(); + assert_eq!( + 3, + requests_made.load(atomic::Ordering::Acquire), + "Should query for colors once per save and once per formatting after save" + ); + + drop(editor); + let close = workspace + .update(cx, |workspace, window, cx| { + workspace.active_pane().update(cx, |pane, cx| { + pane.close_active_item(&CloseActiveItem::default(), window, cx) + }) + }) + .unwrap(); + close.await.unwrap(); + let close = workspace + .update(cx, |workspace, window, cx| { + workspace.active_pane().update(cx, |pane, cx| { + pane.close_active_item(&CloseActiveItem::default(), window, cx) + }) + }) + .unwrap(); + close.await.unwrap(); + assert_eq!( + 3, + requests_made.load(atomic::Ordering::Acquire), + "After saving and closing all editors, no extra requests should be made" + ); + workspace + .update(cx, |workspace, _, cx| { + assert!( + workspace.active_item(cx).is_none(), + "Should close all editors" + ) + }) + .unwrap(); + + workspace + .update(cx, |workspace, window, cx| { + workspace.active_pane().update(cx, |pane, cx| { + pane.navigate_backward(&Default::default(), window, cx); + }) + }) + .unwrap(); + cx.executor().advance_clock(Duration::from_millis(100)); + cx.run_until_parked(); + let editor = workspace + .update(cx, |workspace, _, cx| { + workspace + .active_item(cx) + .expect("Should have reopened the editor again after navigating back") + .downcast::() + .expect("Should be an editor") + }) + .unwrap(); + color_request_handle.next().await.unwrap(); + assert_eq!( + 3, + requests_made.load(atomic::Ordering::Acquire), + "Cache should be reused on buffer close and reopen" + ); + editor.update(cx, |editor, cx| { + assert_eq!( + vec![expected_color], + extract_color_inlays(editor, cx), + "Should have an initial inlay" + ); + }); +} + +#[gpui::test] +async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let (editor, cx) = cx.add_window_view(Editor::single_line); + editor.update_in(cx, |editor, window, cx| { + editor.set_text("oops\n\nwow\n", window, cx) + }); + cx.run_until_parked(); + editor.update(cx, |editor, cx| { + assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯"); + }); + editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx)); + cx.run_until_parked(); + editor.update(cx, |editor, cx| { + assert_eq!(editor.display_text(cx), "oop⋯wow⋯"); + }); +} + +#[gpui::test] +async fn test_non_utf_8_opens(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + cx.update(|cx| { + register_project_item::(cx); + }); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree("/root1", json!({})).await; + fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd]) + .await; + + let project = Project::test(fs, ["/root1".as_ref()], cx).await; + let (workspace, cx) = + cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + + let worktree_id = project.update(cx, |project, cx| { + project.worktrees(cx).next().unwrap().read(cx).id() + }); + + let handle = workspace + .update_in(cx, |workspace, window, cx| { + let project_path = (worktree_id, "one.pdf"); + workspace.open_path(project_path, None, true, window, cx) + }) + .await + .unwrap(); + + assert_eq!( + handle.to_any().entity_type(), + TypeId::of::() + ); +} + +#[track_caller] +fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec { + editor + .all_inlays(cx) + .into_iter() + .filter_map(|inlay| inlay.get_color()) + .map(Rgba::from) + .collect() +} diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index b6996b9a91..4f3580da07 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1,24 +1,24 @@ use crate::{ - ActiveDiagnostic, BlockId, COLUMNAR_SELECTION_MODIFIERS, CURSORS_VISIBLE_FOR, - ChunkRendererContext, ChunkReplacement, CodeActionSource, ConflictsOurs, ConflictsOursMarker, - ConflictsOuter, ConflictsTheirs, ConflictsTheirsMarker, ContextMenuPlacement, CursorShape, - CustomBlockId, DisplayDiffHunk, DisplayPoint, DisplayRow, DocumentHighlightRead, - DocumentHighlightWrite, EditDisplayMode, Editor, EditorMode, EditorSettings, EditorSnapshot, + ActiveDiagnostic, BlockId, CURSORS_VISIBLE_FOR, ChunkRendererContext, ChunkReplacement, + CodeActionSource, ColumnarMode, ConflictsOurs, ConflictsOursMarker, ConflictsOuter, + ConflictsTheirs, ConflictsTheirsMarker, ContextMenuPlacement, CursorShape, CustomBlockId, + DisplayDiffHunk, DisplayPoint, DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, + EditDisplayMode, EditPrediction, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, FILE_HEADER_HEIGHT, FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, - HandleInput, HoveredCursor, InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, - LineHighlight, LineUp, MAX_LINE_LEN, MIN_LINE_NUMBER_DIGITS, MINIMAP_FONT_SIZE, - MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts, PageDown, PageUp, PhantomBreakpointIndicator, - Point, RowExt, RowRangeExt, SelectPhase, SelectedTextHighlight, Selection, SoftWrap, - StickyHeaderExcerpt, ToPoint, ToggleFold, + HandleInput, HoveredCursor, InlayHintRefreshReason, JumpData, LineDown, LineHighlight, LineUp, + MAX_LINE_LEN, MINIMAP_FONT_SIZE, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts, PageDown, + PageUp, PhantomBreakpointIndicator, Point, RowExt, RowRangeExt, SelectPhase, + SelectedTextHighlight, Selection, SelectionDragState, SoftWrap, StickyHeaderExcerpt, ToPoint, + ToggleFold, ToggleFoldAll, code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP}, display_map::{ - Block, BlockContext, BlockStyle, DisplaySnapshot, EditorMargins, FoldId, HighlightedChunk, - ToDisplayPoint, + Block, BlockContext, BlockStyle, ChunkRendererId, DisplaySnapshot, EditorMargins, + HighlightKey, HighlightedChunk, ToDisplayPoint, }, editor_settings::{ - CurrentLineHighlight, DoubleClickInMultibuffer, MinimapThumb, MinimapThumbBorder, - MultiCursorModifier, ScrollBeyondLastLine, ScrollbarAxes, ScrollbarDiagnostics, - ShowMinimap, ShowScrollbar, + CurrentLineHighlight, DocumentColorsRenderMode, DoubleClickInMultibuffer, Minimap, + MinimapThumb, MinimapThumbBorder, ScrollBeyondLastLine, ScrollbarAxes, + ScrollbarDiagnostics, ShowMinimap, ShowScrollbar, }, git::blame::{BlameRenderer, GitBlame, GlobalBlameRenderer}, hover_popover::{ @@ -32,7 +32,6 @@ use crate::{ }; use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind}; use collections::{BTreeMap, HashMap}; -use feature_flags::{DebuggerFeatureFlag, FeatureFlagAppExt}; use file_icons::FileIcons; use git::{ Oid, @@ -41,14 +40,15 @@ use git::{ }; use gpui::{ Action, Along, AnyElement, App, AppContext, AvailableSpace, Axis as ScrollbarAxis, BorderStyle, - Bounds, ClickEvent, ContentMask, Context, Corner, Corners, CursorStyle, DispatchPhase, Edges, - Element, ElementInputHandler, Entity, Focusable as _, FontId, GlobalElementId, Hitbox, - HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero, Keystroke, Length, - ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, - ParentElement, Pixels, ScrollDelta, ScrollHandle, ScrollWheelEvent, ShapedLine, SharedString, - Size, StatefulInteractiveElement, Style, Styled, TextRun, TextStyleRefinement, WeakEntity, - Window, anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, point, px, - quad, relative, size, solid_background, transparent_black, + Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle, + DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId, + GlobalElementId, Hitbox, HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero, + Keystroke, Length, ModifiersChangedEvent, MouseButton, MouseClickEvent, MouseDownEvent, + MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollHandle, + ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled, + TextRun, TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill, + linear_color_stop, linear_gradient, outline, point, px, quad, relative, size, solid_background, + transparent_black, }; use itertools::Itertools; use language::language_settings::{ @@ -61,7 +61,7 @@ use multi_buffer::{ }; use project::{ - ProjectPath, + Entry, ProjectPath, debugger::breakpoint_store::{Breakpoint, BreakpointSessionState}, project_settings::{GitGutterSetting, GitHunkStyleSetting, ProjectSettings}, }; @@ -74,19 +74,25 @@ use std::{ fmt::{self, Write}, iter, mem, ops::{Deref, Range}, + path::{self, Path}, rc::Rc, sync::Arc, - time::Duration, + time::{Duration, Instant}, }; use sum_tree::Bias; -use text::BufferId; +use text::{BufferId, SelectionGoal}; use theme::{ActiveTheme, Appearance, BufferLineHeight, PlayerColor}; -use ui::{ButtonLike, KeyBinding, POPOVER_Y_PADDING, Tooltip, h_flex, prelude::*}; +use ui::{ + ButtonLike, ContextMenu, Indicator, KeyBinding, POPOVER_Y_PADDING, Tooltip, h_flex, prelude::*, + right_click_menu, +}; use unicode_segmentation::UnicodeSegmentation; +use util::post_inc; use util::{RangeExt, ResultExt, debug_panic}; -use workspace::{CollaboratorId, Workspace, item::Item, notifications::NotifyTaskExt}; - -const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 7.; +use workspace::{ + CollaboratorId, ItemSettings, OpenInTerminal, OpenTerminal, RevealInProjectPanel, Workspace, + item::Item, notifications::NotifyTaskExt, +}; /// Determines what kinds of highlights should be applied to a lines background. #[derive(Clone, Copy, Default)] @@ -107,6 +113,12 @@ struct SelectionLayout { user_name: Option, } +struct InlineBlameLayout { + element: AnyElement, + bounds: Bounds, + entry: BlameEntry, +} + impl SelectionLayout { fn new( selection: Selection, @@ -187,7 +199,7 @@ impl EditorElement { let editor = &self.editor; editor.update(cx, |editor, cx| { for action in editor.editor_actions.borrow().values() { - (action)(window, cx) + (action)(editor, window, cx) } }); @@ -210,6 +222,7 @@ impl EditorElement { register_action(editor, window, Editor::newline_above); register_action(editor, window, Editor::newline_below); register_action(editor, window, Editor::backspace); + register_action(editor, window, Editor::blame_hover); register_action(editor, window, Editor::delete); register_action(editor, window, Editor::tab); register_action(editor, window, Editor::backtab); @@ -218,11 +231,13 @@ impl EditorElement { register_action(editor, window, Editor::autoindent); register_action(editor, window, Editor::delete_line); register_action(editor, window, Editor::join_lines); + register_action(editor, window, Editor::sort_lines_by_length); register_action(editor, window, Editor::sort_lines_case_sensitive); register_action(editor, window, Editor::sort_lines_case_insensitive); register_action(editor, window, Editor::reverse_lines); register_action(editor, window, Editor::shuffle_lines); - register_action(editor, window, Editor::toggle_case); + register_action(editor, window, Editor::convert_indentation_to_spaces); + register_action(editor, window, Editor::convert_indentation_to_tabs); register_action(editor, window, Editor::convert_to_upper_case); register_action(editor, window, Editor::convert_to_lower_case); register_action(editor, window, Editor::convert_to_title_case); @@ -231,6 +246,8 @@ impl EditorElement { register_action(editor, window, Editor::convert_to_upper_camel_case); register_action(editor, window, Editor::convert_to_lower_camel_case); register_action(editor, window, Editor::convert_to_opposite_case); + register_action(editor, window, Editor::convert_to_sentence_case); + register_action(editor, window, Editor::toggle_case); register_action(editor, window, Editor::convert_to_rot13); register_action(editor, window, Editor::convert_to_rot47); register_action(editor, window, Editor::delete_to_previous_word_start); @@ -252,6 +269,7 @@ impl EditorElement { register_action(editor, window, Editor::kill_ring_yank); register_action(editor, window, Editor::copy); register_action(editor, window, Editor::copy_and_trim); + register_action(editor, window, Editor::diff_clipboard_with_selection); register_action(editor, window, Editor::paste); register_action(editor, window, Editor::undo); register_action(editor, window, Editor::redo); @@ -345,6 +363,7 @@ impl EditorElement { register_action(editor, window, Editor::toggle_comments); register_action(editor, window, Editor::select_larger_syntax_node); register_action(editor, window, Editor::select_smaller_syntax_node); + register_action(editor, window, Editor::unwrap_syntax_node); register_action(editor, window, Editor::select_enclosing_symbol); register_action(editor, window, Editor::move_to_enclosing_bracket); register_action(editor, window, Editor::undo_selection); @@ -407,6 +426,7 @@ impl EditorElement { register_action(editor, window, Editor::fold_recursive); register_action(editor, window, Editor::toggle_fold); register_action(editor, window, Editor::toggle_fold_recursive); + register_action(editor, window, Editor::toggle_fold_all); register_action(editor, window, Editor::unfold_lines); register_action(editor, window, Editor::unfold_recursive); register_action(editor, window, Editor::unfold_all); @@ -537,9 +557,11 @@ impl EditorElement { } }); register_action(editor, window, Editor::show_signature_help); + register_action(editor, window, Editor::signature_help_prev); + register_action(editor, window, Editor::signature_help_next); register_action(editor, window, Editor::next_edit_prediction); register_action(editor, window, Editor::previous_edit_prediction); - register_action(editor, window, Editor::show_inline_completion); + register_action(editor, window, Editor::show_edit_prediction); register_action(editor, window, Editor::context_menu_first); register_action(editor, window, Editor::context_menu_prev); register_action(editor, window, Editor::context_menu_next); @@ -547,7 +569,7 @@ impl EditorElement { register_action(editor, window, Editor::display_cursor_names); register_action(editor, window, Editor::unique_lines_case_insensitive); register_action(editor, window, Editor::unique_lines_case_sensitive); - register_action(editor, window, Editor::accept_partial_inline_completion); + register_action(editor, window, Editor::accept_partial_edit_prediction); register_action(editor, window, Editor::accept_edit_prediction); register_action(editor, window, Editor::restore_file); register_action(editor, window, Editor::git_restore); @@ -559,12 +581,10 @@ impl EditorElement { register_action(editor, window, Editor::insert_uuid_v4); register_action(editor, window, Editor::insert_uuid_v7); register_action(editor, window, Editor::open_selections_in_multibuffer); - if cx.has_flag::() { - register_action(editor, window, Editor::toggle_breakpoint); - register_action(editor, window, Editor::edit_log_breakpoint); - register_action(editor, window, Editor::enable_breakpoint); - register_action(editor, window, Editor::disable_breakpoint); - } + register_action(editor, window, Editor::toggle_breakpoint); + register_action(editor, window, Editor::edit_log_breakpoint); + register_action(editor, window, Editor::enable_breakpoint); + register_action(editor, window, Editor::disable_breakpoint); } fn register_key_listeners(&self, window: &mut Window, _: &mut App, layout: &EditorLayout) { @@ -620,6 +640,7 @@ impl EditorElement { let text_hitbox = &position_map.text_hitbox; let gutter_hitbox = &position_map.gutter_hitbox; + let point_for_position = position_map.point_for_position(event.position); let mut click_count = event.click_count; let mut modifiers = event.modifiers; @@ -633,6 +654,25 @@ impl EditorElement { return; } + if EditorSettings::get_global(cx) + .drag_and_drop_selection + .enabled + && click_count == 1 + { + let newest_anchor = editor.selections.newest_anchor(); + let snapshot = editor.snapshot(window, cx); + let selection = newest_anchor.map(|anchor| anchor.to_display_point(&snapshot)); + if point_for_position.intersects_selection(&selection) { + editor.selection_drag_state = SelectionDragState::ReadyToDrag { + selection: newest_anchor.clone(), + click_position: event.position, + mouse_down_time: Instant::now(), + }; + cx.stop_propagation(); + return; + } + } + let is_singleton = editor.buffer().read(cx).is_singleton(); if click_count == 2 && !is_singleton { @@ -676,13 +716,16 @@ impl EditorElement { } } - let point_for_position = position_map.point_for_position(event.position); let position = point_for_position.previous_valid; - if modifiers == COLUMNAR_SELECTION_MODIFIERS { + if let Some(mode) = Editor::columnar_selection_mode(&modifiers, cx) { editor.select( SelectPhase::BeginColumnar { position, - reset: false, + reset: match mode { + ColumnarMode::FromMouse => true, + ColumnarMode::FromSelection => false, + }, + mode, goal_column: point_for_position.exact_unclipped.column(), }, window, @@ -699,15 +742,10 @@ impl EditorElement { cx, ); } else { - let multi_cursor_setting = EditorSettings::get_global(cx).multi_cursor_modifier; - let multi_cursor_modifier = match multi_cursor_setting { - MultiCursorModifier::Alt => modifiers.alt, - MultiCursorModifier::CmdOrCtrl => modifiers.secondary(), - }; editor.select( SelectPhase::Begin { position, - add: multi_cursor_modifier, + add: Editor::multi_cursor_modifier(true, &modifiers, cx), click_count, }, window, @@ -804,6 +842,7 @@ impl EditorElement { SelectPhase::BeginColumnar { position, reset: true, + mode: ColumnarMode::FromMouse, goal_column: point_for_position.exact_unclipped.column(), }, window, @@ -821,6 +860,54 @@ impl EditorElement { let text_hitbox = &position_map.text_hitbox; let end_selection = editor.has_pending_selection(); let pending_nonempty_selections = editor.has_pending_nonempty_selection(); + let point_for_position = position_map.point_for_position(event.position); + + match editor.selection_drag_state { + SelectionDragState::ReadyToDrag { + selection: _, + ref click_position, + mouse_down_time: _, + } => { + if event.position == *click_position { + editor.select( + SelectPhase::Begin { + position: point_for_position.previous_valid, + add: false, + click_count: 1, // ready to drag state only occurs on click count 1 + }, + window, + cx, + ); + editor.selection_drag_state = SelectionDragState::None; + cx.stop_propagation(); + return; + } else { + debug_panic!("drag state can never be in ready state after drag") + } + } + SelectionDragState::Dragging { ref selection, .. } => { + let snapshot = editor.snapshot(window, cx); + let selection_display = selection.map(|anchor| anchor.to_display_point(&snapshot)); + if !point_for_position.intersects_selection(&selection_display) + && text_hitbox.is_hovered(window) + { + let is_cut = !(cfg!(target_os = "macos") && event.modifiers.alt + || cfg!(not(target_os = "macos")) && event.modifiers.control); + editor.move_selection_on_drop( + &selection.clone(), + point_for_position.previous_valid, + is_cut, + window, + cx, + ); + } + editor.selection_drag_state = SelectionDragState::None; + cx.stop_propagation(); + cx.notify(); + return; + } + _ => {} + } if end_selection { editor.select(SelectPhase::End, window, cx); @@ -831,6 +918,11 @@ impl EditorElement { } else if cfg!(any(target_os = "linux", target_os = "freebsd")) && event.button == MouseButton::Middle { + #[allow( + clippy::collapsible_if, + clippy::needless_return, + reason = "The cfg-block below makes this a false positive" + )] if !text_hitbox.is_hovered(window) || editor.read_only(cx) { return; } @@ -867,15 +959,16 @@ impl EditorElement { let text_hitbox = &position_map.text_hitbox; let pending_nonempty_selections = editor.has_pending_nonempty_selection(); - let multi_cursor_setting = EditorSettings::get_global(cx).multi_cursor_modifier; - let multi_cursor_modifier = match multi_cursor_setting { - MultiCursorModifier::Alt => event.modifiers().secondary(), - MultiCursorModifier::CmdOrCtrl => event.modifiers().alt, - }; + let hovered_link_modifier = Editor::multi_cursor_modifier(false, &event.modifiers(), cx); - if !pending_nonempty_selections && multi_cursor_modifier && text_hitbox.is_hovered(window) { - let point = position_map.point_for_position(event.up.position); + if let Some(mouse_position) = event.mouse_position() + && !pending_nonempty_selections + && hovered_link_modifier + && text_hitbox.is_hovered(window) + { + let point = position_map.point_for_position(mouse_position); editor.handle_click_hovered_link(point, event.modifiers(), window, cx); + editor.selection_drag_state = SelectionDragState::None; cx.stop_propagation(); } @@ -888,52 +981,125 @@ impl EditorElement { window: &mut Window, cx: &mut Context, ) { - if !editor.has_pending_selection() { + if !editor.has_pending_selection() + && matches!(editor.selection_drag_state, SelectionDragState::None) + { return; } - let text_bounds = position_map.text_hitbox.bounds; let point_for_position = position_map.point_for_position(event.position); - let mut scroll_delta = gpui::Point::::default(); - let vertical_margin = position_map.line_height.min(text_bounds.size.height / 3.0); - let top = text_bounds.origin.y + vertical_margin; - let bottom = text_bounds.bottom_left().y - vertical_margin; - if event.position.y < top { - scroll_delta.y = -scale_vertical_mouse_autoscroll_delta(top - event.position.y); + let text_hitbox = &position_map.text_hitbox; + + let scroll_delta = { + let text_bounds = text_hitbox.bounds; + let mut scroll_delta = gpui::Point::::default(); + let vertical_margin = position_map.line_height.min(text_bounds.size.height / 3.0); + let top = text_bounds.origin.y + vertical_margin; + let bottom = text_bounds.bottom_left().y - vertical_margin; + if event.position.y < top { + scroll_delta.y = -scale_vertical_mouse_autoscroll_delta(top - event.position.y); + } + if event.position.y > bottom { + scroll_delta.y = scale_vertical_mouse_autoscroll_delta(event.position.y - bottom); + } + + // We need horizontal width of text + let style = editor.style.clone().unwrap_or_default(); + let font_id = window.text_system().resolve_font(&style.text.font()); + let font_size = style.text.font_size.to_pixels(window.rem_size()); + let em_width = window.text_system().em_width(font_id, font_size).unwrap(); + + let scroll_margin_x = EditorSettings::get_global(cx).horizontal_scroll_margin; + + let scroll_space: Pixels = scroll_margin_x * em_width; + + let left = text_bounds.origin.x + scroll_space; + let right = text_bounds.top_right().x - scroll_space; + + if event.position.x < left { + scroll_delta.x = -scale_horizontal_mouse_autoscroll_delta(left - event.position.x); + } + if event.position.x > right { + scroll_delta.x = scale_horizontal_mouse_autoscroll_delta(event.position.x - right); + } + scroll_delta + }; + + if !editor.has_pending_selection() { + let drop_anchor = position_map + .snapshot + .display_point_to_anchor(point_for_position.previous_valid, Bias::Left); + match editor.selection_drag_state { + SelectionDragState::Dragging { + ref mut drop_cursor, + ref mut hide_drop_cursor, + .. + } => { + drop_cursor.start = drop_anchor; + drop_cursor.end = drop_anchor; + *hide_drop_cursor = !text_hitbox.is_hovered(window); + editor.apply_scroll_delta(scroll_delta, window, cx); + cx.notify(); + } + SelectionDragState::ReadyToDrag { + ref selection, + ref click_position, + ref mouse_down_time, + } => { + let drag_and_drop_delay = Duration::from_millis( + EditorSettings::get_global(cx).drag_and_drop_selection.delay, + ); + if mouse_down_time.elapsed() >= drag_and_drop_delay { + let drop_cursor = Selection { + id: post_inc(&mut editor.selections.next_selection_id), + start: drop_anchor, + end: drop_anchor, + reversed: false, + goal: SelectionGoal::None, + }; + editor.selection_drag_state = SelectionDragState::Dragging { + selection: selection.clone(), + drop_cursor, + hide_drop_cursor: false, + }; + editor.apply_scroll_delta(scroll_delta, window, cx); + cx.notify(); + } else { + let click_point = position_map.point_for_position(*click_position); + editor.selection_drag_state = SelectionDragState::None; + editor.select( + SelectPhase::Begin { + position: click_point.previous_valid, + add: false, + click_count: 1, + }, + window, + cx, + ); + editor.select( + SelectPhase::Update { + position: point_for_position.previous_valid, + goal_column: point_for_position.exact_unclipped.column(), + scroll_delta, + }, + window, + cx, + ); + } + } + _ => {} + } + } else { + editor.select( + SelectPhase::Update { + position: point_for_position.previous_valid, + goal_column: point_for_position.exact_unclipped.column(), + scroll_delta, + }, + window, + cx, + ); } - if event.position.y > bottom { - scroll_delta.y = scale_vertical_mouse_autoscroll_delta(event.position.y - bottom); - } - - // We need horizontal width of text - let style = editor.style.clone().unwrap_or_default(); - let font_id = window.text_system().resolve_font(&style.text.font()); - let font_size = style.text.font_size.to_pixels(window.rem_size()); - let em_width = window.text_system().em_width(font_id, font_size).unwrap(); - - let scroll_margin_x = EditorSettings::get_global(cx).horizontal_scroll_margin; - - let scroll_space: Pixels = scroll_margin_x * em_width; - - let left = text_bounds.origin.x + scroll_space; - let right = text_bounds.top_right().x - scroll_space; - - if event.position.x < left { - scroll_delta.x = -scale_horizontal_mouse_autoscroll_delta(left - event.position.x); - } - if event.position.x > right { - scroll_delta.x = scale_horizontal_mouse_autoscroll_delta(event.position.x - right); - } - - editor.select( - SelectPhase::Update { - position: point_for_position.previous_valid, - goal_column: point_for_position.exact_unclipped.column(), - scroll_delta, - }, - window, - cx, - ); } fn mouse_moved( @@ -946,17 +1112,72 @@ impl EditorElement { let text_hitbox = &position_map.text_hitbox; let gutter_hitbox = &position_map.gutter_hitbox; let modifiers = event.modifiers; + let text_hovered = text_hitbox.is_hovered(window); let gutter_hovered = gutter_hitbox.is_hovered(window); editor.set_gutter_hovered(gutter_hovered, cx); - editor.mouse_cursor_hidden = false; + editor.show_mouse_cursor(cx); - if gutter_hovered { - let new_point = position_map - .point_for_position(event.position) - .previous_valid; + let point_for_position = position_map.point_for_position(event.position); + let valid_point = point_for_position.previous_valid; + + let hovered_diff_control = position_map + .diff_hunk_control_bounds + .iter() + .find(|(_, bounds)| bounds.contains(&event.position)) + .map(|(row, _)| *row); + + let hovered_diff_hunk_row = if let Some(control_row) = hovered_diff_control { + Some(control_row) + } else if text_hovered { + let current_row = valid_point.row(); + position_map.display_hunks.iter().find_map(|(hunk, _)| { + if let DisplayDiffHunk::Unfolded { + display_row_range, .. + } = hunk + { + if display_row_range.contains(¤t_row) { + Some(display_row_range.start) + } else { + None + } + } else { + None + } + }) + } else { + None + }; + + if hovered_diff_hunk_row != editor.hovered_diff_hunk_row { + editor.hovered_diff_hunk_row = hovered_diff_hunk_row; + cx.notify(); + } + + if let Some((bounds, blame_entry)) = &position_map.inline_blame_bounds { + let mouse_over_inline_blame = bounds.contains(&event.position); + let mouse_over_popover = editor + .inline_blame_popover + .as_ref() + .and_then(|state| state.popover_bounds) + .is_some_and(|bounds| bounds.contains(&event.position)); + let keyboard_grace = editor + .inline_blame_popover + .as_ref() + .is_some_and(|state| state.keyboard_grace); + + if mouse_over_inline_blame || mouse_over_popover { + editor.show_blame_popover(blame_entry, event.position, false, cx); + } else if !keyboard_grace { + editor.hide_blame_popover(cx); + } + } else { + editor.hide_blame_popover(cx); + } + + let breakpoint_indicator = if gutter_hovered { let buffer_anchor = position_map .snapshot - .display_point_to_anchor(new_point, Bias::Left); + .display_point_to_anchor(valid_point, Bias::Left); if let Some((buffer_snapshot, file)) = position_map .snapshot @@ -964,16 +1185,15 @@ impl EditorElement { .buffer_for_excerpt(buffer_anchor.excerpt_id) .and_then(|buffer| buffer.file().map(|file| (buffer, file))) { - let was_hovered = editor.gutter_breakpoint_indicator.0.is_some(); let as_point = text::ToPoint::to_point(&buffer_anchor.text_anchor, buffer_snapshot); let is_visible = editor .gutter_breakpoint_indicator .0 - .map_or(false, |indicator| indicator.is_active); + .is_some_and(|indicator| indicator.is_active); let has_existing_breakpoint = - editor.breakpoint_store.as_ref().map_or(false, |store| { + editor.breakpoint_store.as_ref().is_some_and(|store| { let Some(project) = &editor.project else { return false; }; @@ -992,43 +1212,46 @@ impl EditorElement { .is_some() }); - editor.gutter_breakpoint_indicator.0 = Some(PhantomBreakpointIndicator { - display_row: new_point.row(), - is_active: is_visible, - collides_with_existing_breakpoint: has_existing_breakpoint, - }); - - editor.gutter_breakpoint_indicator.1.get_or_insert_with(|| { - cx.spawn(async move |this, cx| { - if !was_hovered { + if !is_visible { + editor.gutter_breakpoint_indicator.1.get_or_insert_with(|| { + cx.spawn(async move |this, cx| { cx.background_executor() .timer(Duration::from_millis(200)) .await; - } - this.update(cx, |this, cx| { - if let Some(indicator) = this.gutter_breakpoint_indicator.0.as_mut() { - indicator.is_active = true; - } - - cx.notify(); + this.update(cx, |this, cx| { + if let Some(indicator) = this.gutter_breakpoint_indicator.0.as_mut() + { + indicator.is_active = true; + cx.notify(); + } + }) + .ok(); }) - .ok(); - }) - }); + }); + } + + Some(PhantomBreakpointIndicator { + display_row: valid_point.row(), + is_active: is_visible, + collides_with_existing_breakpoint: has_existing_breakpoint, + }) } else { - editor.gutter_breakpoint_indicator = (None, None); + editor.gutter_breakpoint_indicator.1 = None; + None } } else { - editor.gutter_breakpoint_indicator = (None, None); + editor.gutter_breakpoint_indicator.1 = None; + None + }; + + if &breakpoint_indicator != &editor.gutter_breakpoint_indicator.0 { + editor.gutter_breakpoint_indicator.0 = breakpoint_indicator; + cx.notify(); } - cx.notify(); - // Don't trigger hover popover if mouse is hovering over context menu - if text_hitbox.is_hovered(window) { - let point_for_position = position_map.point_for_position(event.position); - + if text_hovered { editor.update_hovered_link( point_for_position, &position_map.snapshot, @@ -1162,6 +1385,34 @@ impl EditorElement { let player = editor.current_user_player_color(cx); selections.push((player, layouts)); + + if let SelectionDragState::Dragging { + ref selection, + ref drop_cursor, + ref hide_drop_cursor, + } = editor.selection_drag_state + && !hide_drop_cursor + && (drop_cursor + .start + .cmp(&selection.start, &snapshot.buffer_snapshot) + .eq(&Ordering::Less) + || drop_cursor + .end + .cmp(&selection.end, &snapshot.buffer_snapshot) + .eq(&Ordering::Greater)) + { + let drag_cursor_layout = SelectionLayout::new( + drop_cursor.clone(), + false, + CursorShape::Bar, + &snapshot.display_snapshot, + false, + false, + None, + ); + let absent_color = cx.theme().players().absent(); + selections.push((absent_color, vec![drag_cursor_layout])); + } } if let Some(collaboration_hub) = &editor.collaboration_hub { @@ -1171,19 +1422,15 @@ impl EditorElement { CollaboratorId::PeerId(peer_id) => { if let Some(collaborator) = collaboration_hub.collaborators(cx).get(&peer_id) - { - if let Some(participant_index) = collaboration_hub + && let Some(participant_index) = collaboration_hub .user_participant_indices(cx) .get(&collaborator.user_id) - { - if let Some((local_selection_style, _)) = selections.first_mut() - { - *local_selection_style = cx - .theme() - .players() - .color_for_participant(participant_index.0); - } - } + && let Some((local_selection_style, _)) = selections.first_mut() + { + *local_selection_style = cx + .theme() + .players() + .color_for_participant(participant_index.0); } } CollaboratorId::Agent => { @@ -1342,7 +1589,7 @@ impl EditorElement { snapshot .grapheme_at(cursor_position) .or_else(|| { - if cursor_column == 0 { + if snapshot.is_empty() { snapshot.placeholder_text().and_then(|s| { s.graphemes(true).next().map(|s| s.to_string().into()) }) @@ -1387,6 +1634,7 @@ impl EditorElement { strikethrough: None, underline: None, }], + None, ) }) } else { @@ -1556,6 +1804,19 @@ impl EditorElement { let minimap_settings = EditorSettings::get_global(cx).minimap; + if minimap_settings.on_active_editor() { + let active_editor = self.editor.read(cx).workspace().and_then(|ws| { + ws.read(cx) + .active_pane() + .read(cx) + .active_item() + .and_then(|i| i.act_as::(cx)) + }); + if active_editor.is_some_and(|e| e != self.editor) { + return None; + } + } + if !snapshot.mode.is_full() || minimap_width.is_zero() || matches!( @@ -1639,7 +1900,7 @@ impl EditorElement { let mut minimap = div() .size_full() - .shadow_sm() + .shadow_xs() .px(PADDING_OFFSET) .child(minimap_editor) .into_any_element(); @@ -1676,6 +1937,40 @@ impl EditorElement { text_style.line_height_in_pixels(rem_size) } + fn get_minimap_width( + &self, + minimap_settings: &Minimap, + scrollbars_shown: bool, + text_width: Pixels, + em_width: Pixels, + font_size: Pixels, + rem_size: Pixels, + cx: &App, + ) -> Option { + if minimap_settings.show == ShowMinimap::Auto && !scrollbars_shown { + return None; + } + + let minimap_font_size = self.editor.read_with(cx, |editor, cx| { + editor.minimap().map(|minimap_editor| { + minimap_editor + .read(cx) + .text_style_refinement + .as_ref() + .and_then(|refinement| refinement.font_size) + .unwrap_or(MINIMAP_FONT_SIZE) + }) + })?; + + let minimap_em_width = em_width * (minimap_font_size.to_pixels(rem_size) / font_size); + + let minimap_width = (text_width * MinimapLayout::MINIMAP_WIDTH_PCT) + .min(minimap_em_width * minimap_settings.max_width_columns.get() as f32); + + (minimap_width >= minimap_em_width * MinimapLayout::MINIMAP_MIN_WIDTH_COLUMNS) + .then_some(minimap_width) + } + fn prepaint_crease_toggles( &self, crease_toggles: &mut [Option], @@ -1806,7 +2101,7 @@ impl EditorElement { row_block_types: &HashMap, content_origin: gpui::Point, scroll_pixel_position: gpui::Point, - inline_completion_popover_origin: Option>, + edit_prediction_popover_origin: Option>, start_row: DisplayRow, end_row: DisplayRow, line_height: Pixels, @@ -1815,16 +2110,19 @@ impl EditorElement { window: &mut Window, cx: &mut App, ) -> HashMap { - if self.editor.read(cx).mode().is_minimap() { - return HashMap::default(); - } - - let max_severity = match ProjectSettings::get_global(cx) - .diagnostics - .inline - .max_severity - .unwrap_or_else(|| self.editor.read(cx).diagnostics_max_severity) - .into_lsp() + let max_severity = match self + .editor + .read(cx) + .inline_diagnostics_enabled() + .then(|| { + ProjectSettings::get_global(cx) + .diagnostics + .inline + .max_severity + .unwrap_or_else(|| self.editor.read(cx).diagnostics_max_severity) + .into_lsp() + }) + .flatten() { Some(max_severity) => max_severity, None => return HashMap::default(), @@ -1875,11 +2173,13 @@ impl EditorElement { }; let padding = ProjectSettings::get_global(cx).diagnostics.inline.padding as f32 * em_width; - let min_x = ProjectSettings::get_global(cx) - .diagnostics - .inline - .min_column as f32 - * em_width; + let min_x = self.column_pixels( + ProjectSettings::get_global(cx) + .diagnostics + .inline + .min_column as usize, + window, + ); let mut elements = HashMap::default(); for (row, mut diagnostics) in diagnostics_by_rows { @@ -1920,12 +2220,12 @@ impl EditorElement { cmp::max(padded_line, min_start) }; - let behind_inline_completion_popover = inline_completion_popover_origin + let behind_edit_prediction_popover = edit_prediction_popover_origin .as_ref() - .map_or(false, |inline_completion_popover_origin| { - (pos_y..pos_y + line_height).contains(&inline_completion_popover_origin.y) + .is_some_and(|edit_prediction_popover_origin| { + (pos_y..pos_y + line_height).contains(&edit_prediction_popover_origin.y) }); - let opacity = if behind_inline_completion_popover { + let opacity = if behind_edit_prediction_popover { 0.5 } else { 1.0 @@ -1990,9 +2290,7 @@ impl EditorElement { None } }) - .map_or(false, |source| { - matches!(source, CodeActionSource::Indicator(..)) - }); + .is_some_and(|source| matches!(source, CodeActionSource::Indicator(..))); Some(editor.render_inline_code_actions(icon_size, display_point.row(), active, cx)) })?; @@ -2121,7 +2419,7 @@ impl EditorElement { text_hitbox: &Hitbox, window: &mut Window, cx: &mut App, - ) -> Option { + ) -> Option { if !self .editor .update(cx, |editor, cx| editor.render_git_blame_inline(window, cx)) @@ -2132,31 +2430,33 @@ impl EditorElement { let editor = self.editor.read(cx); let blame = editor.blame.clone()?; let padding = { - const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 6.; const INLINE_ACCEPT_SUGGESTION_EM_WIDTHS: f32 = 14.; - let mut padding = INLINE_BLAME_PADDING_EM_WIDTHS; + let mut padding = ProjectSettings::get_global(cx) + .git + .inline_blame + .unwrap_or_default() + .padding as f32; - if let Some(inline_completion) = editor.active_inline_completion.as_ref() { - match &inline_completion.completion { - InlineCompletion::Edit { - display_mode: EditDisplayMode::TabAccept, - .. - } => padding += INLINE_ACCEPT_SUGGESTION_EM_WIDTHS, - _ => {} - } + if let Some(edit_prediction) = editor.active_edit_prediction.as_ref() + && let EditPrediction::Edit { + display_mode: EditDisplayMode::TabAccept, + .. + } = &edit_prediction.completion + { + padding += INLINE_ACCEPT_SUGGESTION_EM_WIDTHS } padding * em_width }; - let blame_entry = blame + let entry = blame .update(cx, |blame, cx| { blame.blame_for_rows(&[*row_info], cx).next() }) .flatten()?; - let mut element = render_inline_blame_entry(blame_entry.clone(), &self.style, cx)?; + let mut element = render_inline_blame_entry(entry.clone(), &self.style, cx)?; let start_y = content_origin.y + line_height * (display_row.as_f32() - scroll_pixel_position.y / line_height); @@ -2173,8 +2473,8 @@ impl EditorElement { let min_column_in_pixels = ProjectSettings::get_global(cx) .git .inline_blame - .and_then(|settings| settings.min_column) - .map(|col| self.column_pixels(col as usize, window, cx)) + .map(|settings| settings.min_column) + .map(|col| self.column_pixels(col as usize, window)) .unwrap_or(px(0.)); let min_start = content_origin.x - scroll_pixel_position.x + min_column_in_pixels; @@ -2185,24 +2485,19 @@ impl EditorElement { let size = element.layout_as_root(AvailableSpace::min_size(), window, cx); let bounds = Bounds::new(absolute_offset, size); - self.layout_blame_entry_popover( - bounds, - blame_entry, - blame, - line_height, - text_hitbox, - window, - cx, - ); + self.layout_blame_entry_popover(entry.clone(), blame, line_height, text_hitbox, window, cx); element.prepaint_as_root(absolute_offset, AvailableSpace::min_size(), window, cx); - Some(element) + Some(InlineBlameLayout { + element, + bounds, + entry, + }) } fn layout_blame_entry_popover( &self, - parent_bounds: Bounds, blame_entry: BlameEntry, blame: Entity, line_height: Pixels, @@ -2210,91 +2505,59 @@ impl EditorElement { window: &mut Window, cx: &mut App, ) { - let mouse_position = window.mouse_position(); - let mouse_over_inline_blame = parent_bounds.contains(&mouse_position); - let mouse_over_popover = self.editor.read_with(cx, |editor, _| { + let Some((popover_state, target_point)) = self.editor.read_with(cx, |editor, _| { editor .inline_blame_popover .as_ref() - .and_then(|state| state.popover_bounds) - .map_or(false, |bounds| bounds.contains(&mouse_position)) + .map(|state| (state.popover_state.clone(), state.position)) + }) else { + return; + }; + + let workspace = self + .editor + .read_with(cx, |editor, _| editor.workspace().map(|w| w.downgrade())); + + let maybe_element = workspace.and_then(|workspace| { + render_blame_entry_popover( + blame_entry, + popover_state.scroll_handle, + popover_state.commit_message, + popover_state.markdown, + workspace, + &blame, + window, + cx, + ) }); - self.editor.update(cx, |editor, cx| { - if mouse_over_inline_blame || mouse_over_popover { - editor.show_blame_popover(&blame_entry, mouse_position, cx); + if let Some(mut element) = maybe_element { + let size = element.layout_as_root(AvailableSpace::min_size(), window, cx); + let overall_height = size.height + HOVER_POPOVER_GAP; + let popover_origin = if target_point.y > overall_height { + point(target_point.x, target_point.y - size.height) } else { - editor.hide_blame_popover(cx); - } - }); + point( + target_point.x, + target_point.y + line_height + HOVER_POPOVER_GAP, + ) + }; - let should_draw = self.editor.read_with(cx, |editor, _| { - editor - .inline_blame_popover - .as_ref() - .map_or(false, |state| state.show_task.is_none()) - }); + let horizontal_offset = (text_hitbox.top_right().x + - POPOVER_RIGHT_OFFSET + - (popover_origin.x + size.width)) + .min(Pixels::ZERO); - if should_draw { - let maybe_element = self.editor.update(cx, |editor, cx| { - editor - .workspace() - .map(|workspace| workspace.downgrade()) - .zip( - editor - .inline_blame_popover - .as_ref() - .map(|p| p.popover_state.clone()), - ) - .and_then(|(workspace, popover_state)| { - render_blame_entry_popover( - blame_entry, - popover_state.scroll_handle, - popover_state.commit_message, - popover_state.markdown, - workspace, - &blame, - window, - cx, - ) - }) + let origin = point(popover_origin.x + horizontal_offset, popover_origin.y); + let popover_bounds = Bounds::new(origin, size); + + self.editor.update(cx, |editor, _| { + if let Some(state) = &mut editor.inline_blame_popover { + state.popover_bounds = Some(popover_bounds); + } }); - if let Some(mut element) = maybe_element { - let size = element.layout_as_root(AvailableSpace::min_size(), window, cx); - let origin = self.editor.read_with(cx, |editor, _| { - let target_point = editor - .inline_blame_popover - .as_ref() - .map_or(mouse_position, |state| state.position); - - let overall_height = size.height + HOVER_POPOVER_GAP; - let popover_origin = if target_point.y > overall_height { - point(target_point.x, target_point.y - size.height) - } else { - point( - target_point.x, - target_point.y + line_height + HOVER_POPOVER_GAP, - ) - }; - - let horizontal_offset = (text_hitbox.top_right().x - - POPOVER_RIGHT_OFFSET - - (popover_origin.x + size.width)) - .min(Pixels::ZERO); - - point(popover_origin.x + horizontal_offset, popover_origin.y) - }); - - let popover_bounds = Bounds::new(origin, size); - self.editor.update(cx, |editor, _| { - if let Some(state) = &mut editor.inline_blame_popover { - state.popover_bounds = Some(popover_bounds); - } - }); - - window.defer_draw(element, origin, 2); - } + window.defer_draw(element, origin, 2); } } @@ -2377,9 +2640,6 @@ impl EditorElement { window: &mut Window, cx: &mut App, ) -> Option> { - if self.editor.read(cx).mode().is_minimap() { - return None; - } let indent_guides = self.editor.update(cx, |editor, cx| { editor.indent_guides(visible_buffer_range, snapshot, cx) })?; @@ -2396,7 +2656,7 @@ impl EditorElement { .enumerate() .filter_map(|(i, indent_guide)| { let single_indent_width = - self.column_pixels(indent_guide.tab_size as usize, window, cx); + self.column_pixels(indent_guide.tab_size as usize, window); let total_width = single_indent_width * indent_guide.depth as f32; let start_x = content_origin.x + total_width - scroll_pixel_position.x; if start_x >= text_origin.x { @@ -2424,6 +2684,39 @@ impl EditorElement { ) } + fn layout_wrap_guides( + &self, + em_advance: Pixels, + scroll_position: gpui::Point, + content_origin: gpui::Point, + scrollbar_layout: Option<&EditorScrollbars>, + vertical_scrollbar_width: Pixels, + hitbox: &Hitbox, + window: &Window, + cx: &App, + ) -> SmallVec<[(Pixels, bool); 2]> { + let scroll_left = scroll_position.x * em_advance; + let content_origin = content_origin.x; + let horizontal_offset = content_origin - scroll_left; + let vertical_scrollbar_width = scrollbar_layout + .and_then(|layout| layout.visible.then_some(vertical_scrollbar_width)) + .unwrap_or_default(); + + self.editor + .read(cx) + .wrap_guides(cx) + .into_iter() + .flat_map(|(guide, active)| { + let wrap_position = self.column_pixels(guide, window); + let wrap_guide_x = wrap_position + horizontal_offset; + let display_wrap_guide = wrap_guide_x >= content_origin + && wrap_guide_x <= hitbox.bounds.right() - vertical_scrollbar_width; + + display_wrap_guide.then_some((wrap_guide_x, active)) + }) + .collect() + } + fn calculate_indent_guide_bounds( row_range: Range, line_height: Pixels, @@ -2457,7 +2750,10 @@ impl EditorElement { let mut block_offset = 0; let mut found_excerpt_header = false; for (_, block) in snapshot.blocks_in_range(prev_line..row_range.start) { - if matches!(block, Block::ExcerptBoundary { .. }) { + if matches!( + block, + Block::ExcerptBoundary { .. } | Block::BufferHeader { .. } + ) { found_excerpt_header = true; break; } @@ -2474,7 +2770,10 @@ impl EditorElement { let mut block_height = 0; let mut found_excerpt_header = false; for (_, block) in snapshot.blocks_in_range(row_range.end..cons_line) { - if matches!(block, Block::ExcerptBoundary { .. }) { + if matches!( + block, + Block::ExcerptBoundary { .. } | Block::BufferHeader { .. } + ) { found_excerpt_header = true; } block_height += block.height(); @@ -2521,7 +2820,7 @@ impl EditorElement { } let row = - MultiBufferRow(DisplayPoint::new(display_row, 0).to_point(&snapshot).row); + MultiBufferRow(DisplayPoint::new(display_row, 0).to_point(snapshot).row); if snapshot.is_line_folded(row) { return None; } @@ -2562,6 +2861,7 @@ impl EditorElement { ) -> Vec { self.editor.update(cx, |editor, cx| { let active_task_indicator_row = + // TODO: add edit button on the right side of each row in the context menu if let Some(crate::CodeContextMenu::CodeActions(CodeActionsMenu { deployed_from, actions, @@ -2611,7 +2911,7 @@ impl EditorElement { if multibuffer_row .0 .checked_sub(1) - .map_or(false, |previous_row| { + .is_some_and(|previous_row| { snapshot.is_line_folded(MultiBufferRow(previous_row)) }) { @@ -2684,8 +2984,8 @@ impl EditorElement { .ilog10() + 1; - let elements = buffer_rows - .into_iter() + buffer_rows + .iter() .enumerate() .map(|(ix, row_info)| { let ExpandInfo { @@ -2703,7 +3003,8 @@ impl EditorElement { let available_width = gutter_dimensions.left_padding - git_gutter_width; let editor = self.editor.clone(); - let is_wide = max_line_number_length >= MIN_LINE_NUMBER_DIGITS + let is_wide = max_line_number_length + >= EditorSettings::get_global(cx).gutter.min_line_number_digits as u32 && row_info .buffer_row .is_some_and(|row| (row + 1).ilog10() + 1 == max_line_number_length) @@ -2719,7 +3020,7 @@ impl EditorElement { .icon_color(Color::Custom(cx.theme().colors().editor_line_number)) .selected_icon_color(Color::Custom(cx.theme().colors().editor_foreground)) .icon_size(IconSize::Custom(rems(editor_font_size / window.rem_size()))) - .width(width.into()) + .width(width) .on_click(move |_, window, cx| { editor.update(cx, |editor, cx| { editor.expand_excerpt(excerpt_id, direction, window, cx); @@ -2739,9 +3040,7 @@ impl EditorElement { Some((toggle, origin)) }) - .collect(); - - elements + .collect() } fn calculate_relative_line_numbers( @@ -2808,9 +3107,9 @@ impl EditorElement { window: &mut Window, cx: &mut App, ) -> Arc> { - let include_line_numbers = snapshot.show_line_numbers.unwrap_or_else(|| { - EditorSettings::get_global(cx).gutter.line_numbers && snapshot.mode.is_full() - }); + let include_line_numbers = snapshot + .show_line_numbers + .unwrap_or_else(|| EditorSettings::get_global(cx).gutter.line_numbers); if !include_line_numbers { return Arc::default(); } @@ -2841,7 +3140,7 @@ impl EditorElement { let relative_rows = self.calculate_relative_line_numbers(snapshot, &rows, relative_to); let mut line_number = String::new(); let line_numbers = buffer_rows - .into_iter() + .iter() .enumerate() .flat_map(|(ix, row_info)| { let display_row = DisplayRow(rows.start.0 + ix as u32); @@ -2918,7 +3217,7 @@ impl EditorElement { && self.editor.read(cx).is_singleton(cx); if include_fold_statuses { row_infos - .into_iter() + .iter() .enumerate() .map(|(ix, info)| { if info.expand_info.is_some() { @@ -2994,10 +3293,12 @@ impl EditorElement { underline: None, strikethrough: None, }; - let line = - window - .text_system() - .shape_line(line.to_string().into(), font_size, &[run]); + let line = window.text_system().shape_line( + line.to_string().into(), + font_size, + &[run], + None, + ); LineWithInvisibles { width: line.width, len: line.len, @@ -3011,7 +3312,7 @@ impl EditorElement { let chunks = snapshot.highlighted_chunks(rows.clone(), true, style); LineWithInvisibles::from_chunks( chunks, - &style, + style, MAX_LINE_LEN, rows.len(), &snapshot.mode, @@ -3092,7 +3393,7 @@ impl EditorElement { let line_ix = align_to.row().0.checked_sub(rows.start.0); x_position = if let Some(layout) = line_ix.and_then(|ix| line_layouts.get(ix as usize)) { - x_and_width(&layout) + x_and_width(layout) } else { x_and_width(&layout_line( align_to.row(), @@ -3121,22 +3422,18 @@ impl EditorElement { div() .size_full() - .children( - (!snapshot.mode.is_minimap() || custom.render_in_minimap).then(|| { - custom.render(&mut BlockContext { - window, - app: cx, - anchor_x, - margins: editor_margins, - line_height, - em_width, - block_id, - selected, - max_width: text_hitbox.size.width.max(*scroll_width), - editor_style: &self.style, - }) - }), - ) + .child(custom.render(&mut BlockContext { + window, + app: cx, + anchor_x, + margins: editor_margins, + line_height, + em_width, + block_id, + selected, + max_width: text_hitbox.size.width.max(*scroll_width), + editor_style: &self.style, + })) .into_any() } @@ -3162,42 +3459,41 @@ impl EditorElement { .into_any_element() } - Block::ExcerptBoundary { - excerpt, - height, - starts_new_buffer, - .. - } => { + Block::ExcerptBoundary { .. } => { let color = cx.theme().colors().clone(); let mut result = v_flex().id(block_id).w_full(); + result = result.child( + h_flex().relative().child( + div() + .top(line_height / 2.) + .absolute() + .w_full() + .h_px() + .bg(color.border_variant), + ), + ); + + result.into_any() + } + + Block::BufferHeader { excerpt, height } => { + let mut result = v_flex().id(block_id).w_full(); + let jump_data = header_jump_data(snapshot, block_row_start, *height, excerpt); - if *starts_new_buffer { - if sticky_header_excerpt_id != Some(excerpt.id) { - let selected = selected_buffer_ids.contains(&excerpt.buffer_id); + if sticky_header_excerpt_id != Some(excerpt.id) { + let selected = selected_buffer_ids.contains(&excerpt.buffer_id); - result = result.child(div().pr(editor_margins.right).child( - self.render_buffer_header( - excerpt, false, selected, false, jump_data, window, cx, - ), - )); - } else { - result = - result.child(div().h(FILE_HEADER_HEIGHT as f32 * window.line_height())); - } - } else { - result = result.child( - h_flex().relative().child( - div() - .top(line_height / 2.) - .absolute() - .w_full() - .h_px() - .bg(color.border_variant), + result = result.child(div().pr(editor_margins.right).child( + self.render_buffer_header( + excerpt, false, selected, false, jump_data, window, cx, ), - ); - }; + )); + } else { + result = + result.child(div().h(FILE_HEADER_HEIGHT as f32 * window.line_height())); + } result.into_any() } @@ -3221,33 +3517,33 @@ impl EditorElement { let mut x_offset = px(0.); let mut is_block = true; - if let BlockId::Custom(custom_block_id) = block_id { - if block.has_height() { - if block.place_near() { - if let Some((x_target, line_width)) = x_position { - let margin = em_width * 2; - if line_width + final_size.width + margin - < editor_width + editor_margins.gutter.full_width() - && !row_block_types.contains_key(&(row - 1)) - && element_height_in_lines == 1 - { - x_offset = line_width + margin; - row = row - 1; - is_block = false; - element_height_in_lines = 0; - row_block_types.insert(row, is_block); - } else { - let max_offset = editor_width + editor_margins.gutter.full_width() - - final_size.width; - let min_offset = (x_target + em_width - final_size.width) - .max(editor_margins.gutter.full_width()); - x_offset = x_target.min(max_offset).max(min_offset); - } - } - }; - if element_height_in_lines != block.height() { - resized_blocks.insert(custom_block_id, element_height_in_lines); + if let BlockId::Custom(custom_block_id) = block_id + && block.has_height() + { + if block.place_near() + && let Some((x_target, line_width)) = x_position + { + let margin = em_width * 2; + if line_width + final_size.width + margin + < editor_width + editor_margins.gutter.full_width() + && !row_block_types.contains_key(&(row - 1)) + && element_height_in_lines == 1 + { + x_offset = line_width + margin; + row = row - 1; + is_block = false; + element_height_in_lines = 0; + row_block_types.insert(row, is_block); + } else { + let max_offset = + editor_width + editor_margins.gutter.full_width() - final_size.width; + let min_offset = (x_target + em_width - final_size.width) + .max(editor_margins.gutter.full_width()); + x_offset = x_target.min(max_offset).max(min_offset); } + }; + if element_height_in_lines != block.height() { + resized_blocks.insert(custom_block_id, element_height_in_lines); } } for i in 0..element_height_in_lines { @@ -3266,11 +3562,10 @@ impl EditorElement { jump_data: JumpData, window: &mut Window, cx: &mut App, - ) -> Div { + ) -> impl IntoElement { let editor = self.editor.read(cx); - let file_status = editor - .buffer - .read(cx) + let multi_buffer = editor.buffer.read(cx); + let file_status = multi_buffer .all_diff_hunks_expanded() .then(|| { editor @@ -3280,6 +3575,17 @@ impl EditorElement { .status_for_buffer_id(for_excerpt.buffer_id, cx) }) .flatten(); + let indicator = multi_buffer + .buffer(for_excerpt.buffer_id) + .and_then(|buffer| { + let buffer = buffer.read(cx); + let indicator_color = match (buffer.has_conflict(), buffer.is_dirty()) { + (true, _) => Some(Color::Warning), + (_, true) => Some(Color::Accent), + (false, false) => None, + }; + indicator_color.map(|indicator_color| Indicator::dot().color(indicator_color)) + }); let include_root = editor .project @@ -3287,17 +3593,17 @@ impl EditorElement { .map(|project| project.read(cx).visible_worktrees(cx).count() > 1) .unwrap_or_default(); let can_open_excerpts = Editor::can_open_excerpts_in_file(for_excerpt.buffer.file()); - let path = for_excerpt.buffer.resolve_file_path(cx, include_root); - let filename = path + let relative_path = for_excerpt.buffer.resolve_file_path(cx, include_root); + let filename = relative_path .as_ref() .and_then(|path| Some(path.file_name()?.to_string_lossy().to_string())); - let parent_path = path.as_ref().and_then(|path| { + let parent_path = relative_path.as_ref().and_then(|path| { Some(path.parent()?.to_string_lossy().to_string() + std::path::MAIN_SEPARATOR_STR) }); let focus_handle = editor.focus_handle(cx); let colors = cx.theme().colors(); - div() + let header = div() .p_1() .w_full() .h(FILE_HEADER_HEIGHT as f32 * window.line_height()) @@ -3337,29 +3643,42 @@ impl EditorElement { ButtonLike::new("toggle-buffer-fold") .style(ui::ButtonStyle::Transparent) .height(px(28.).into()) - .width(px(28.).into()) + .width(px(28.)) .children(toggle_chevron_icon) .tooltip({ let focus_handle = focus_handle.clone(); move |window, cx| { - Tooltip::for_action_in( + Tooltip::with_meta_in( "Toggle Excerpt Fold", - &ToggleFold, + Some(&ToggleFold), + "Alt+click to toggle all", &focus_handle, window, cx, ) } }) - .on_click(move |_, _, cx| { - if is_folded { + .on_click(move |event, window, cx| { + if event.modifiers().alt { + // Alt+click toggles all buffers editor.update(cx, |editor, cx| { - editor.unfold_buffer(buffer_id, cx); + editor.toggle_fold_all( + &ToggleFoldAll, + window, + cx, + ); }); } else { - editor.update(cx, |editor, cx| { - editor.fold_buffer(buffer_id, cx); - }); + // Regular click toggles single buffer + if is_folded { + editor.update(cx, |editor, cx| { + editor.unfold_buffer(buffer_id, cx); + }); + } else { + editor.update(cx, |editor, cx| { + editor.fold_buffer(buffer_id, cx); + }); + } } }), ), @@ -3374,38 +3693,54 @@ impl EditorElement { }) .take(1), ) + .child( + h_flex() + .size(Pixels(12.0)) + .justify_center() + .children(indicator), + ) .child( h_flex() .cursor_pointer() .id("path header block") .size_full() .justify_between() + .overflow_hidden() .child( h_flex() .gap_2() - .child( - Label::new( - filename - .map(SharedString::from) - .unwrap_or_else(|| "untitled".into()), - ) - .single_line() - .when_some( - file_status, - |el, status| { - el.color(if status.is_conflicted() { - Color::Conflict - } else if status.is_modified() { - Color::Modified - } else if status.is_deleted() { - Color::Disabled - } else { - Color::Created - }) - .when(status.is_deleted(), |el| el.strikethrough()) - }, - ), - ) + .map(|path_header| { + let filename = filename + .map(SharedString::from) + .unwrap_or_else(|| "untitled".into()); + + path_header + .when(ItemSettings::get_global(cx).file_icons, |el| { + let path = path::Path::new(filename.as_str()); + let icon = FileIcons::get_icon(path, cx) + .unwrap_or_default(); + let icon = + Icon::from_path(icon).color(Color::Muted); + el.child(icon) + }) + .child(Label::new(filename).single_line().when_some( + file_status, + |el, status| { + el.color(if status.is_conflicted() { + Color::Conflict + } else if status.is_modified() { + Color::Modified + } else if status.is_deleted() { + Color::Disabled + } else { + Color::Created + }) + .when(status.is_deleted(), |el| { + el.strikethrough() + }) + }, + )) + }) .when_some(parent_path, |then, path| { then.child(div().child(path).text_color( if file_status.is_some_and(FileStatus::is_deleted) { @@ -3416,36 +3751,139 @@ impl EditorElement { )) }), ) - .when(can_open_excerpts && is_selected && path.is_some(), |el| { - el.child( - h_flex() - .id("jump-to-file-button") - .gap_2p5() - .child(Label::new("Jump To File")) - .children( - KeyBinding::for_action_in( - &OpenExcerpts, - &focus_handle, - window, - cx, - ) - .map(|binding| binding.into_any_element()), - ), - ) - }) + .when( + can_open_excerpts && is_selected && relative_path.is_some(), + |el| { + el.child( + h_flex() + .id("jump-to-file-button") + .gap_2p5() + .child(Label::new("Jump To File")) + .children( + KeyBinding::for_action_in( + &OpenExcerpts, + &focus_handle, + window, + cx, + ) + .map(|binding| binding.into_any_element()), + ), + ) + }, + ) .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) .on_click(window.listener_for(&self.editor, { move |editor, e: &ClickEvent, window, cx| { editor.open_excerpts_common( Some(jump_data.clone()), - e.down.modifiers.secondary(), + e.modifiers().secondary(), window, cx, ); } })), ), - ) + ); + + let file = for_excerpt.buffer.file().cloned(); + let editor = self.editor.clone(); + right_click_menu("buffer-header-context-menu") + .trigger(move |_, _, _| header) + .menu(move |window, cx| { + let menu_context = focus_handle.clone(); + let editor = editor.clone(); + let file = file.clone(); + ContextMenu::build(window, cx, move |mut menu, window, cx| { + if let Some(file) = file + && let Some(project) = editor.read(cx).project() + && let Some(worktree) = + project.read(cx).worktree_for_id(file.worktree_id(cx), cx) + { + let worktree = worktree.read(cx); + let relative_path = file.path(); + let entry_for_path = worktree.entry_for_path(relative_path); + let abs_path = entry_for_path.map(|e| { + e.canonical_path.as_deref().map_or_else( + || worktree.abs_path().join(relative_path), + Path::to_path_buf, + ) + }); + let has_relative_path = worktree.root_entry().is_some_and(Entry::is_dir); + + let parent_abs_path = abs_path + .as_ref() + .and_then(|abs_path| Some(abs_path.parent()?.to_path_buf())); + let relative_path = has_relative_path + .then_some(relative_path) + .map(ToOwned::to_owned); + + let visible_in_project_panel = + relative_path.is_some() && worktree.is_visible(); + let reveal_in_project_panel = entry_for_path + .filter(|_| visible_in_project_panel) + .map(|entry| entry.id); + menu = menu + .when_some(abs_path, |menu, abs_path| { + menu.entry( + "Copy Path", + Some(Box::new(zed_actions::workspace::CopyPath)), + window.handler_for(&editor, move |_, _, cx| { + cx.write_to_clipboard(ClipboardItem::new_string( + abs_path.to_string_lossy().to_string(), + )); + }), + ) + }) + .when_some(relative_path, |menu, relative_path| { + menu.entry( + "Copy Relative Path", + Some(Box::new(zed_actions::workspace::CopyRelativePath)), + window.handler_for(&editor, move |_, _, cx| { + cx.write_to_clipboard(ClipboardItem::new_string( + relative_path.to_string_lossy().to_string(), + )); + }), + ) + }) + .when( + reveal_in_project_panel.is_some() || parent_abs_path.is_some(), + |menu| menu.separator(), + ) + .when_some(reveal_in_project_panel, |menu, entry_id| { + menu.entry( + "Reveal In Project Panel", + Some(Box::new(RevealInProjectPanel::default())), + window.handler_for(&editor, move |editor, _, cx| { + if let Some(project) = &mut editor.project { + project.update(cx, |_, cx| { + cx.emit(project::Event::RevealInProjectPanel( + entry_id, + )) + }); + } + }), + ) + }) + .when_some(parent_abs_path, |menu, parent_abs_path| { + menu.entry( + "Open in Terminal", + Some(Box::new(OpenInTerminal)), + window.handler_for(&editor, move |_, window, cx| { + window.dispatch_action( + OpenTerminal { + working_directory: parent_abs_path.clone(), + } + .boxed_clone(), + cx, + ); + }), + ) + }); + } + + menu.context(menu_context) + }) + }) } fn render_blocks( @@ -3483,7 +3921,7 @@ impl EditorElement { for (row, block) in fixed_blocks { let block_id = block.id(); - if focused_block.as_ref().map_or(false, |b| b.id == block_id) { + if focused_block.as_ref().is_some_and(|b| b.id == block_id) { focused_block = None; } @@ -3540,7 +3978,7 @@ impl EditorElement { }; let block_id = block.id(); - if focused_block.as_ref().map_or(false, |b| b.id == block_id) { + if focused_block.as_ref().is_some_and(|b| b.id == block_id) { focused_block = None; } @@ -3581,60 +4019,58 @@ impl EditorElement { } } - if let Some(focused_block) = focused_block { - if let Some(focus_handle) = focused_block.focus_handle.upgrade() { - if focus_handle.is_focused(window) { - if let Some(block) = snapshot.block_for_id(focused_block.id) { - let style = block.style(); - let width = match style { - BlockStyle::Fixed => AvailableSpace::MinContent, - BlockStyle::Flex => AvailableSpace::Definite( - hitbox - .size - .width - .max(fixed_block_max_width) - .max(editor_margins.gutter.width + *scroll_width), - ), - BlockStyle::Sticky => AvailableSpace::Definite(hitbox.size.width), - }; + if let Some(focused_block) = focused_block + && let Some(focus_handle) = focused_block.focus_handle.upgrade() + && focus_handle.is_focused(window) + && let Some(block) = snapshot.block_for_id(focused_block.id) + { + let style = block.style(); + let width = match style { + BlockStyle::Fixed => AvailableSpace::MinContent, + BlockStyle::Flex => AvailableSpace::Definite( + hitbox + .size + .width + .max(fixed_block_max_width) + .max(editor_margins.gutter.width + *scroll_width), + ), + BlockStyle::Sticky => AvailableSpace::Definite(hitbox.size.width), + }; - if let Some((element, element_size, _, x_offset)) = self.render_block( - &block, - width, - focused_block.id, - rows.end, - snapshot, - text_x, - &rows, - line_layouts, - editor_margins, - line_height, - em_width, - text_hitbox, - editor_width, - scroll_width, - &mut resized_blocks, - &mut row_block_types, - selections, - selected_buffer_ids, - is_row_soft_wrapped, - sticky_header_excerpt_id, - window, - cx, - ) { - blocks.push(BlockLayout { - id: block.id(), - x_offset, - row: None, - element, - available_space: size(width, element_size.height.into()), - style, - overlaps_gutter: true, - is_buffer_header: block.is_buffer_header(), - }); - } - } - } + if let Some((element, element_size, _, x_offset)) = self.render_block( + &block, + width, + focused_block.id, + rows.end, + snapshot, + text_x, + &rows, + line_layouts, + editor_margins, + line_height, + em_width, + text_hitbox, + editor_width, + scroll_width, + &mut resized_blocks, + &mut row_block_types, + selections, + selected_buffer_ids, + is_row_soft_wrapped, + sticky_header_excerpt_id, + window, + cx, + ) { + blocks.push(BlockLayout { + id: block.id(), + x_offset, + row: None, + element, + available_space: size(width, element_size.height.into()), + style, + overlaps_gutter: true, + is_buffer_header: block.is_buffer_header(), + }); } } @@ -3715,6 +4151,7 @@ impl EditorElement { let available_width = hitbox.bounds.size.width - right_margin; let mut header = v_flex() + .w_full() .relative() .child( div() @@ -3789,27 +4226,26 @@ impl EditorElement { { let editor = self.editor.read(cx); - if editor - .edit_prediction_visible_in_cursor_popover(editor.has_active_inline_completion()) + if editor.edit_prediction_visible_in_cursor_popover(editor.has_active_edit_prediction()) { height_above_menu += editor.edit_prediction_cursor_popover_height() + POPOVER_Y_PADDING; edit_prediction_popover_visible = true; } - if editor.context_menu_visible() { - if let Some(crate::ContextMenuOrigin::Cursor) = editor.context_menu_origin() { - let (min_height_in_lines, max_height_in_lines) = editor - .context_menu_options - .as_ref() - .map_or((3, 12), |options| { - (options.min_entries_visible, options.max_entries_visible) - }); + if editor.context_menu_visible() + && let Some(crate::ContextMenuOrigin::Cursor) = editor.context_menu_origin() + { + let (min_height_in_lines, max_height_in_lines) = editor + .context_menu_options + .as_ref() + .map_or((3, 12), |options| { + (options.min_entries_visible, options.max_entries_visible) + }); - min_menu_height += line_height * min_height_in_lines as f32 + POPOVER_Y_PADDING; - max_menu_height += line_height * max_height_in_lines as f32 + POPOVER_Y_PADDING; - context_menu_visible = true; - } + min_menu_height += line_height * min_height_in_lines as f32 + POPOVER_Y_PADDING; + max_menu_height += line_height * max_height_in_lines as f32 + POPOVER_Y_PADDING; + context_menu_visible = true; } context_menu_placement = editor .context_menu_options @@ -3881,7 +4317,8 @@ impl EditorElement { let edit_prediction = if edit_prediction_popover_visible { self.editor.update(cx, move |editor, cx| { - let accept_binding = editor.accept_edit_prediction_keybind(window, cx); + let accept_binding = + editor.accept_edit_prediction_keybind(false, window, cx); let mut element = editor.render_edit_prediction_cursor_popover( min_width, max_width, @@ -4320,7 +4757,7 @@ impl EditorElement { } }; - let source_included = source_display_point.map_or(true, |source_display_point| { + let source_included = source_display_point.is_none_or(|source_display_point| { visible_range .to_inclusive() .contains(&source_display_point.row()) @@ -4500,7 +4937,7 @@ impl EditorElement { let intersects_menu = |bounds: Bounds| -> bool { context_menu_layout .as_ref() - .map_or(false, |menu| bounds.intersects(&menu.bounds)) + .is_some_and(|menu| bounds.intersects(&menu.bounds)) }; let can_place_above = { @@ -4620,7 +5057,6 @@ impl EditorElement { row_range: Range, row_infos: &[RowInfo], text_hitbox: &Hitbox, - position_map: &PositionMap, newest_cursor_position: Option, line_height: Pixels, right_margin: Pixels, @@ -4630,14 +5066,15 @@ impl EditorElement { editor: Entity, window: &mut Window, cx: &mut App, - ) -> Vec { + ) -> (Vec, Vec<(DisplayRow, Bounds)>) { let render_diff_hunk_controls = editor.read(cx).render_diff_hunk_controls.clone(); - let point_for_position = position_map.point_for_position(window.mouse_position()); + let hovered_diff_hunk_row = editor.read(cx).hovered_diff_hunk_row; let mut controls = vec![]; + let mut control_bounds = vec![]; let active_positions = [ - Some(point_for_position.previous_valid), + hovered_diff_hunk_row.map(|row| DisplayPoint::new(row, 0)), newest_cursor_position, ]; @@ -4682,9 +5119,10 @@ impl EditorElement { { continue; } + if active_positions .iter() - .any(|p| p.map_or(false, |p| display_row_range.contains(&p.row()))) + .any(|p| p.is_some_and(|p| display_row_range.contains(&p.row()))) { let y = display_row_range.start.as_f32() * line_height + text_hitbox.bounds.top() @@ -4705,6 +5143,9 @@ impl EditorElement { let x = text_hitbox.bounds.right() - right_margin - px(10.) - size.width; + let bounds = Bounds::new(gpui::Point::new(x, y), size); + control_bounds.push((display_row_range.start, bounds)); + window.with_absolute_element_offset(gpui::Point::new(x, y), |window| { element.prepaint(window, cx) }); @@ -4713,7 +5154,7 @@ impl EditorElement { } } - controls + (controls, control_bounds) } fn layout_signature_help( @@ -4748,7 +5189,7 @@ impl EditorElement { let maybe_element = self.editor.update(cx, |editor, cx| { if let Some(popover) = editor.signature_help_state.popover_mut() { - let element = popover.render(max_size, cx); + let element = popover.render(max_size, window, cx); Some(element) } else { None @@ -4794,7 +5235,7 @@ impl EditorElement { let intersects_menu = |bounds: Bounds| -> bool { context_menu_layout .as_ref() - .map_or(false, |menu| bounds.intersects(&menu.bounds)) + .is_some_and(|menu| bounds.intersects(&menu.bounds)) }; let final_origin = if popover_bounds_above.is_contained_within(hitbox) @@ -4879,7 +5320,7 @@ impl EditorElement { let mut end_row = start_row.0; while active_rows .peek() - .map_or(false, |(active_row, has_selection)| { + .is_some_and(|(active_row, has_selection)| { active_row.0 == end_row + 1 && has_selection.selection == contains_non_empty_selection.selection }) @@ -5001,26 +5442,7 @@ impl EditorElement { paint_highlight(range.start, range.end, color, edges); } - let scroll_left = - layout.position_map.snapshot.scroll_position().x * layout.position_map.em_width; - - for (wrap_position, active) in layout.wrap_guides.iter() { - let x = (layout.position_map.text_hitbox.origin.x - + *wrap_position - + layout.position_map.em_width / 2.) - - scroll_left; - - let show_scrollbars = layout - .scrollbars_layout - .as_ref() - .map_or(false, |layout| layout.visible); - - if x < layout.position_map.text_hitbox.origin.x - || (show_scrollbars && x > self.scrollbar_left(&layout.hitbox.bounds)) - { - continue; - } - + for (guide_x, active) in layout.wrap_guides.iter() { let color = if *active { cx.theme().colors().editor_active_wrap_guide } else { @@ -5028,7 +5450,7 @@ impl EditorElement { }; window.paint_quad(fill( Bounds { - origin: point(x, layout.position_map.text_hitbox.origin.y), + origin: point(*guide_x, layout.position_map.text_hitbox.origin.y), size: size(px(1.), layout.position_map.text_hitbox.size.height), }, color, @@ -5130,7 +5552,7 @@ impl EditorElement { let is_singleton = self.editor.read(cx).is_singleton(cx); let line_height = layout.position_map.line_height; - window.set_cursor_style(CursorStyle::Arrow, Some(&layout.gutter_hitbox)); + window.set_cursor_style(CursorStyle::Arrow, &layout.gutter_hitbox); for LineNumberLayout { shaped_line, @@ -5157,9 +5579,9 @@ impl EditorElement { // In singleton buffers, we select corresponding lines on the line number click, so use | -like cursor. // In multi buffers, we open file at the line number clicked, so use a pointing hand cursor. if is_singleton { - window.set_cursor_style(CursorStyle::IBeam, Some(&hitbox)); + window.set_cursor_style(CursorStyle::IBeam, hitbox); } else { - window.set_cursor_style(CursorStyle::PointingHand, Some(&hitbox)); + window.set_cursor_style(CursorStyle::PointingHand, hitbox); } } } @@ -5178,7 +5600,7 @@ impl EditorElement { &layout.position_map.snapshot, line_height, layout.gutter_hitbox.bounds, - &hunk, + hunk, ); Some(( hunk_bounds, @@ -5314,7 +5736,10 @@ impl EditorElement { let end_row_in_current_excerpt = snapshot .blocks_in_range(start_row..end_row) .find_map(|(start_row, block)| { - if matches!(block, Block::ExcerptBoundary { .. }) { + if matches!( + block, + Block::ExcerptBoundary { .. } | Block::BufferHeader { .. } + ) { Some(start_row) } else { None @@ -5369,16 +5794,15 @@ impl EditorElement { cx: &mut App, ) { for (_, hunk_hitbox) in &layout.display_hunks { - if let Some(hunk_hitbox) = hunk_hitbox { - if !self + if let Some(hunk_hitbox) = hunk_hitbox + && !self .editor .read(cx) .buffer() .read(cx) .all_diff_hunks_expanded() - { - window.set_cursor_style(CursorStyle::PointingHand, Some(hunk_hitbox)); - } + { + window.set_cursor_style(CursorStyle::PointingHand, hunk_hitbox); } } @@ -5451,7 +5875,26 @@ impl EditorElement { |window| { let editor = self.editor.read(cx); if editor.mouse_cursor_hidden { - window.set_cursor_style(CursorStyle::None, None); + window.set_window_cursor_style(CursorStyle::None); + } else if let SelectionDragState::ReadyToDrag { + mouse_down_time, .. + } = &editor.selection_drag_state + { + let drag_and_drop_delay = Duration::from_millis( + EditorSettings::get_global(cx).drag_and_drop_selection.delay, + ); + if mouse_down_time.elapsed() >= drag_and_drop_delay { + window.set_cursor_style( + CursorStyle::DragCopy, + &layout.position_map.text_hitbox, + ); + } + } else if matches!( + editor.selection_drag_state, + SelectionDragState::Dragging { .. } + ) { + window + .set_cursor_style(CursorStyle::DragCopy, &layout.position_map.text_hitbox); } else if editor .hovered_link_state .as_ref() @@ -5459,17 +5902,15 @@ impl EditorElement { { window.set_cursor_style( CursorStyle::PointingHand, - Some(&layout.position_map.text_hitbox), + &layout.position_map.text_hitbox, ); } else { - window.set_cursor_style( - CursorStyle::IBeam, - Some(&layout.position_map.text_hitbox), - ); + window.set_cursor_style(CursorStyle::IBeam, &layout.position_map.text_hitbox); }; self.paint_lines_background(layout, window, cx); let invisible_display_ranges = self.paint_highlights(layout, window); + self.paint_document_colors(layout, window); self.paint_lines(&invisible_display_ranges, layout, window, cx); self.paint_redactions(layout, window); self.paint_cursors(layout, window, cx); @@ -5497,6 +5938,7 @@ impl EditorElement { for (range, color) in &layout.highlighted_ranges { self.paint_highlighted_range( range.clone(), + true, *color, Pixels::ZERO, line_end_overshoot, @@ -5511,6 +5953,7 @@ impl EditorElement { for selection in selections.iter() { self.paint_highlighted_range( selection.range.clone(), + true, player_color.selection, corner_radius, corner_radius * 2., @@ -5586,6 +6029,7 @@ impl EditorElement { for range in layout.redacted_ranges.iter() { self.paint_highlighted_range( range.clone(), + true, redaction_color.into(), Pixels::ZERO, line_end_overshoot, @@ -5596,6 +6040,48 @@ impl EditorElement { }); } + fn paint_document_colors(&self, layout: &mut EditorLayout, window: &mut Window) { + let Some((colors_render_mode, image_colors)) = &layout.document_colors else { + return; + }; + if image_colors.is_empty() + || colors_render_mode == &DocumentColorsRenderMode::None + || colors_render_mode == &DocumentColorsRenderMode::Inlay + { + return; + } + + let line_end_overshoot = layout.line_end_overshoot(); + + for (range, color) in image_colors { + match colors_render_mode { + DocumentColorsRenderMode::Inlay | DocumentColorsRenderMode::None => return, + DocumentColorsRenderMode::Background => { + self.paint_highlighted_range( + range.clone(), + true, + *color, + Pixels::ZERO, + line_end_overshoot, + layout, + window, + ); + } + DocumentColorsRenderMode::Border => { + self.paint_highlighted_range( + range.clone(), + false, + *color, + Pixels::ZERO, + line_end_overshoot, + layout, + window, + ); + } + } + } + } + fn paint_cursors(&mut self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) { for cursor in &mut layout.visible_cursors { cursor.paint(layout.content_origin, window, cx); @@ -5606,6 +6092,7 @@ impl EditorElement { let Some(scrollbars_layout) = layout.scrollbars_layout.take() else { return; }; + let any_scrollbar_dragged = self.editor.read(cx).scroll_manager.any_scrollbar_dragged(); for (scrollbar_layout, axis) in scrollbars_layout.iter_scrollbars() { let hitbox = &scrollbar_layout.hitbox; @@ -5637,10 +6124,10 @@ impl EditorElement { if axis == ScrollbarAxis::Vertical { let fast_markers = - self.collect_fast_scrollbar_markers(layout, &scrollbar_layout, cx); + self.collect_fast_scrollbar_markers(layout, scrollbar_layout, cx); // Refresh slow scrollbar markers in the background. Below, we // paint whatever markers have already been computed. - self.refresh_slow_scrollbar_markers(layout, &scrollbar_layout, window, cx); + self.refresh_slow_scrollbar_markers(layout, scrollbar_layout, window, cx); let markers = self.editor.read(cx).scrollbar_marker_state.markers.clone(); for marker in markers.iter().chain(&fast_markers) { @@ -5671,7 +6158,11 @@ impl EditorElement { BorderStyle::Solid, )); - window.set_cursor_style(CursorStyle::Arrow, Some(&hitbox)); + if any_scrollbar_dragged { + window.set_window_cursor_style(CursorStyle::Arrow); + } else { + window.set_cursor_style(CursorStyle::Arrow, hitbox); + } } }) } @@ -5739,7 +6230,7 @@ impl EditorElement { } }); - if self.editor.read(cx).scroll_manager.any_scrollbar_dragged() { + if any_scrollbar_dragged { window.on_mouse_event({ let editor = self.editor.clone(); move |_: &MouseUpEvent, phase, window, cx| { @@ -5911,13 +6402,15 @@ impl EditorElement { background_highlights.iter() { let is_search_highlights = *background_highlight_id - == TypeId::of::(); + == HighlightKey::Type(TypeId::of::()); let is_text_highlights = *background_highlight_id - == TypeId::of::(); + == HighlightKey::Type(TypeId::of::()); let is_symbol_occurrences = *background_highlight_id - == TypeId::of::() + == HighlightKey::Type(TypeId::of::()) || *background_highlight_id - == TypeId::of::(); + == HighlightKey::Type( + TypeId::of::(), + ); if (is_search_highlights && scrollbar_settings.search_results) || (is_text_highlights && scrollbar_settings.selected_text) || (is_symbol_occurrences && scrollbar_settings.selected_symbol) @@ -6025,6 +6518,7 @@ impl EditorElement { fn paint_highlighted_range( &self, range: Range, + fill: bool, color: Hsla, corner_radius: Pixels, line_end_overshoot: Pixels, @@ -6075,7 +6569,7 @@ impl EditorElement { .collect(), }; - highlighted_range.paint(layout.position_map.text_hitbox.bounds, window); + highlighted_range.paint(fill, layout.position_map.text_hitbox.bounds, window); } } @@ -6091,9 +6585,9 @@ impl EditorElement { } fn paint_inline_blame(&mut self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) { - if let Some(mut inline_blame) = layout.inline_blame.take() { + if let Some(mut blame_layout) = layout.inline_blame_layout.take() { window.paint_layer(layout.position_map.text_hitbox.bounds, |window| { - inline_blame.paint(window, cx); + blame_layout.element.paint(window, cx); }) } } @@ -6125,6 +6619,7 @@ impl EditorElement { fn paint_minimap(&self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) { if let Some(mut layout) = layout.minimap.take() { let minimap_hitbox = layout.thumb_layout.hitbox.clone(); + let dragging_minimap = self.editor.read(cx).scroll_manager.is_dragging_minimap(); window.paint_layer(layout.thumb_layout.hitbox.bounds, |window| { window.with_element_namespace("minimap", |window| { @@ -6176,7 +6671,11 @@ impl EditorElement { }); }); - window.set_cursor_style(CursorStyle::Arrow, Some(&minimap_hitbox)); + if dragging_minimap { + window.set_window_cursor_style(CursorStyle::Arrow); + } else { + window.set_cursor_style(CursorStyle::Arrow, &minimap_hitbox); + } let minimap_axis = ScrollbarAxis::Vertical; let pixels_per_line = (minimap_hitbox.size.height / layout.max_scroll_top) @@ -6212,32 +6711,30 @@ impl EditorElement { editor.set_scroll_position(position, window, cx); } cx.stop_propagation(); - } else { - if minimap_hitbox.is_hovered(window) { - editor.scroll_manager.set_is_hovering_minimap_thumb( - !event.dragging() - && layout - .thumb_layout - .thumb_bounds - .is_some_and(|bounds| bounds.contains(&event.position)), - cx, - ); + } else if minimap_hitbox.is_hovered(window) { + editor.scroll_manager.set_is_hovering_minimap_thumb( + !event.dragging() + && layout + .thumb_layout + .thumb_bounds + .is_some_and(|bounds| bounds.contains(&event.position)), + cx, + ); - // Stop hover events from propagating to the - // underlying editor if the minimap hitbox is hovered - if !event.dragging() { - cx.stop_propagation(); - } - } else { - editor.scroll_manager.hide_minimap_thumb(cx); + // Stop hover events from propagating to the + // underlying editor if the minimap hitbox is hovered + if !event.dragging() { + cx.stop_propagation(); } + } else { + editor.scroll_manager.hide_minimap_thumb(cx); } mouse_position = event.position; }); } }); - if self.editor.read(cx).scroll_manager.is_dragging_minimap() { + if dragging_minimap { window.on_mouse_event({ let editor = self.editor.clone(); move |event: &MouseUpEvent, phase, window, cx| { @@ -6318,14 +6815,14 @@ impl EditorElement { } } - fn paint_inline_completion_popover( + fn paint_edit_prediction_popover( &mut self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App, ) { - if let Some(inline_completion_popover) = layout.inline_completion_popover.as_mut() { - inline_completion_popover.paint(window, cx); + if let Some(edit_prediction_popover) = layout.edit_prediction_popover.as_mut() { + edit_prediction_popover.paint(window, cx); } } @@ -6377,7 +6874,7 @@ impl EditorElement { let position_map: &PositionMap = &position_map; let line_height = position_map.line_height; - let max_glyph_width = position_map.em_width; + let max_glyph_advance = position_map.em_advance; let (delta, axis) = match delta { gpui::ScrollDelta::Pixels(mut pixels) => { //Trackpad @@ -6388,15 +6885,15 @@ impl EditorElement { gpui::ScrollDelta::Lines(lines) => { //Not trackpad let pixels = - point(lines.x * max_glyph_width, lines.y * line_height); + point(lines.x * max_glyph_advance, lines.y * line_height); (pixels, None) } }; let current_scroll_position = position_map.snapshot.scroll_position(); - let x = (current_scroll_position.x * max_glyph_width + let x = (current_scroll_position.x * max_glyph_advance - (delta.x * scroll_sensitivity)) - / max_glyph_width; + / max_glyph_advance; let y = (current_scroll_position.y * line_height - (delta.y * scroll_sensitivity)) / line_height; @@ -6423,7 +6920,7 @@ impl EditorElement { } fn paint_mouse_listeners(&mut self, layout: &EditorLayout, window: &mut Window, cx: &mut App) { - if self.editor.read(cx).mode.is_minimap() { + if layout.mode.is_minimap() { return; } @@ -6524,10 +7021,10 @@ impl EditorElement { // Fire click handlers during the bubble phase. DispatchPhase::Bubble => editor.update(cx, |editor, cx| { if let Some(mouse_down) = captured_mouse_down.take() { - let event = ClickEvent { + let event = ClickEvent::Mouse(MouseClickEvent { down: mouse_down, up: event.clone(), - }; + }); Self::click(editor, &event, &position_map, window, cx); } }), @@ -6557,11 +7054,7 @@ impl EditorElement { }); } - fn scrollbar_left(&self, bounds: &Bounds) -> Pixels { - bounds.top_right().x - self.style.scrollbar_width - } - - fn column_pixels(&self, column: usize, window: &mut Window, _: &mut App) -> Pixels { + fn column_pixels(&self, column: usize, window: &Window) -> Pixels { let style = &self.style; let font_size = style.text.font_size.to_pixels(window.rem_size()); let layout = window.text_system().shape_line( @@ -6575,19 +7068,15 @@ impl EditorElement { underline: None, strikethrough: None, }], + None, ); layout.width } - fn max_line_number_width( - &self, - snapshot: &EditorSnapshot, - window: &mut Window, - cx: &mut App, - ) -> Pixels { + fn max_line_number_width(&self, snapshot: &EditorSnapshot, window: &mut Window) -> Pixels { let digit_count = snapshot.widest_line_number().ilog10() + 1; - self.column_pixels(digit_count as usize, window, cx) + self.column_pixels(digit_count as usize, window) } fn shape_line_number( @@ -6608,6 +7097,7 @@ impl EditorElement { text, self.style.text.font_size.to_pixels(window.rem_size()), &[run], + None, ) } @@ -6616,9 +7106,7 @@ impl EditorElement { let unstaged_hollow = ProjectSettings::get_global(cx) .git .hunk_style - .map_or(false, |style| { - matches!(style, GitHunkStyleSetting::UnstagedHollow) - }); + .is_some_and(|style| matches!(style, GitHunkStyleSetting::UnstagedHollow)); unstaged == unstaged_hollow } @@ -6665,7 +7153,7 @@ impl AcceptEditPredictionBinding { pub fn keystroke(&self) -> Option<&Keystroke> { if let Some(binding) = self.0.as_ref() { match &binding.keystrokes() { - [keystroke] => Some(keystroke), + [keystroke, ..] => Some(keystroke), _ => None, } } else { @@ -6752,7 +7240,7 @@ fn render_blame_entry_popover( ) -> Option { let renderer = cx.global::().0.clone(); let blame = blame.read(cx); - let repository = blame.repository(cx)?.clone(); + let repository = blame.repository(cx)?; renderer.render_blame_entry_popover( blame_entry, scroll_handle, @@ -6820,7 +7308,7 @@ pub(crate) struct LineWithInvisibles { enum LineFragment { Text(ShapedLine), Element { - id: FoldId, + id: ChunkRendererId, element: Option, size: Size, len: usize, @@ -6871,14 +7359,17 @@ impl LineWithInvisibles { text: "\n", style: None, is_tab: false, + is_inlay: false, replacement: None, }]) { if let Some(replacement) = highlighted_chunk.replacement { if !line.is_empty() { - let shaped_line = - window - .text_system() - .shape_line(line.clone().into(), font_size, &styles); + let shaped_line = window.text_system().shape_line( + line.clone().into(), + font_size, + &styles, + None, + ); width += shaped_line.width; len += shaped_line.len; fragments.push(LineFragment::Text(shaped_line)); @@ -6898,6 +7389,7 @@ impl LineWithInvisibles { chunk, font_size, &[text_style.to_run(highlighted_chunk.text.len())], + None, ); AvailableSpace::Definite(shaped_line.width) } else { @@ -6942,7 +7434,7 @@ impl LineWithInvisibles { }; let line_layout = window .text_system() - .shape_line(x, font_size, &[run]) + .shape_line(x, font_size, &[run], None) .with_len(highlighted_chunk.text.len()); width += line_layout.width; @@ -6957,6 +7449,7 @@ impl LineWithInvisibles { line.clone().into(), font_size, &styles, + None, ); width += shaped_line.width; len += shaped_line.len; @@ -7004,7 +7497,7 @@ impl LineWithInvisibles { strikethrough: text_style.strikethrough, }); - if editor_mode.is_full() { + if editor_mode.is_full() && !highlighted_chunk.is_inlay { // Line wrap pads its contents with fake whitespaces, // avoid printing them let is_soft_wrapped = is_row_soft_wrapped(row); @@ -7207,6 +7700,17 @@ impl LineWithInvisibles { paint(window, cx); }), + ShowWhitespaceSetting::Trailing => { + let mut previous_start = self.len; + for ([start, end], paint) in invisible_iter.rev() { + if previous_start != end { + break; + } + previous_start = start; + paint(window, cx); + } + } + // For a whitespace to be on a boundary, any of the following conditions need to be met: // - It is a tab // - It is adjacent to an edge (start or end) @@ -7419,7 +7923,7 @@ impl Element for EditorElement { fn request_layout( &mut self, _: Option<&GlobalElementId>, - __inspector_id: Option<&gpui::InspectorElementId>, + _inspector_id: Option<&gpui::InspectorElementId>, window: &mut Window, cx: &mut App, ) -> (gpui::LayoutId, ()) { @@ -7429,51 +7933,21 @@ impl Element for EditorElement { editor.set_style(self.style.clone(), window, cx); let layout_id = match editor.mode { - EditorMode::SingleLine { auto_width } => { + EditorMode::SingleLine => { let rem_size = window.rem_size(); - let height = self.style.text.line_height_in_pixels(rem_size); - if auto_width { - let editor_handle = cx.entity().clone(); - let style = self.style.clone(); - window.request_measured_layout( - Style::default(), - move |_, _, window, cx| { - let editor_snapshot = editor_handle - .update(cx, |editor, cx| editor.snapshot(window, cx)); - let line = Self::layout_lines( - DisplayRow(0)..DisplayRow(1), - &editor_snapshot, - &style, - px(f32::MAX), - |_| false, // Single lines never soft wrap - window, - cx, - ) - .pop() - .unwrap(); - - let font_id = - window.text_system().resolve_font(&style.text.font()); - let font_size = - style.text.font_size.to_pixels(window.rem_size()); - let em_width = - window.text_system().em_width(font_id, font_size).unwrap(); - - size(line.width + em_width, height) - }, - ) - } else { - let mut style = Style::default(); - style.size.height = height.into(); - style.size.width = relative(1.).into(); - window.request_layout(style, None, cx) - } + let mut style = Style::default(); + style.size.height = height.into(); + style.size.width = relative(1.).into(); + window.request_layout(style, None, cx) } - EditorMode::AutoHeight { max_lines } => { - let editor_handle = cx.entity().clone(); + EditorMode::AutoHeight { + min_lines, + max_lines, + } => { + let editor_handle = cx.entity(); let max_line_number_width = - self.max_line_number_width(&editor.snapshot(window, cx), window, cx); + self.max_line_number_width(&editor.snapshot(window, cx), window); window.request_measured_layout( Style::default(), move |known_dimensions, available_space, window, cx| { @@ -7481,6 +7955,7 @@ impl Element for EditorElement { .update(cx, |editor, cx| { compute_auto_height_layout( editor, + min_lines, max_lines, max_line_number_width, known_dimensions, @@ -7537,9 +8012,14 @@ impl Element for EditorElement { line_height: Some(self.style.text.line_height), ..Default::default() }; - let focus_handle = self.editor.focus_handle(cx); - window.set_view_id(self.editor.entity_id()); - window.set_focus_handle(&focus_handle, cx); + + let is_minimap = self.editor.read(cx).mode.is_minimap(); + + if !is_minimap { + let focus_handle = self.editor.focus_handle(cx); + window.set_view_id(self.editor.entity_id()); + window.set_focus_handle(&focus_handle, cx); + } let rem_size = self.rem_size(cx); window.with_rem_size(rem_size, |window| { @@ -7550,19 +8030,19 @@ impl Element for EditorElement { }); let style = self.style.clone(); + let rem_size = window.rem_size(); let font_id = window.text_system().resolve_font(&style.text.font()); - let font_size = style.text.font_size.to_pixels(window.rem_size()); - let line_height = style.text.line_height_in_pixels(window.rem_size()); + let font_size = style.text.font_size.to_pixels(rem_size); + let line_height = style.text.line_height_in_pixels(rem_size); let em_width = window.text_system().em_width(font_id, font_size).unwrap(); let em_advance = window.text_system().em_advance(font_id, font_size).unwrap(); - - let glyph_grid_cell = size(em_width, line_height); + let glyph_grid_cell = size(em_advance, line_height); let gutter_dimensions = snapshot .gutter_dimensions( font_id, font_size, - self.max_line_number_width(&snapshot, window, cx), + self.max_line_number_width(&snapshot, window), cx, ) .or_else(|| { @@ -7581,42 +8061,31 @@ impl Element for EditorElement { .then_some(style.scrollbar_width) .unwrap_or_default(); let minimap_width = self - .editor - .read(cx) - .minimap() - .is_some() - .then(|| match settings.minimap.show { - ShowMinimap::Auto => { - scrollbars_shown.then_some(MinimapLayout::MINIMAP_WIDTH) - } - _ => Some(MinimapLayout::MINIMAP_WIDTH), - }) - .flatten() - .filter(|minimap_width| { - text_width - vertical_scrollbar_width - *minimap_width > *minimap_width - }) + .get_minimap_width( + &settings.minimap, + scrollbars_shown, + text_width, + em_width, + font_size, + rem_size, + cx, + ) .unwrap_or_default(); let right_margin = minimap_width + vertical_scrollbar_width; let editor_width = text_width - gutter_dimensions.margin - 2 * em_width - right_margin; - let editor_margins = EditorMargins { gutter: gutter_dimensions, right: right_margin, }; - // Offset the content_bounds from the text_bounds by the gutter margin (which - // is roughly half a character wide) to make hit testing work more like how we want. - let content_offset = point(editor_margins.gutter.margin, Pixels::ZERO); - - let editor_content_width = editor_width - content_offset.x; - snapshot = self.editor.update(cx, |editor, cx| { editor.last_bounds = Some(bounds); editor.gutter_dimensions = gutter_dimensions; editor.set_visible_line_count(bounds.size.height / line_height, window, cx); + editor.set_visible_column_count(editor_width / em_advance); if matches!( editor.mode, @@ -7628,10 +8097,10 @@ impl Element for EditorElement { let wrap_width = match editor.soft_wrap_mode(cx) { SoftWrap::GitDiff => None, SoftWrap::None => Some(wrap_width_for(MAX_LINE_LEN as u32 / 2)), - SoftWrap::EditorWidth => Some(editor_content_width), + SoftWrap::EditorWidth => Some(editor_width), SoftWrap::Column(column) => Some(wrap_width_for(column)), SoftWrap::Bounded(column) => { - Some(editor_content_width.min(wrap_width_for(column))) + Some(editor_width.min(wrap_width_for(column))) } }; @@ -7643,14 +8112,6 @@ impl Element for EditorElement { } }); - let wrap_guides = self - .editor - .read(cx) - .wrap_guides(cx) - .iter() - .map(|(guide, active)| (self.column_pixels(*guide, window, cx), *active)) - .collect::>(); - let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal); let gutter_hitbox = window.insert_hitbox( gutter_bounds(bounds, gutter_dimensions), @@ -7664,19 +8125,18 @@ impl Element for EditorElement { HitboxBehavior::Normal, ); + // Offset the content_bounds from the text_bounds by the gutter margin (which + // is roughly half a character wide) to make hit testing work more like how we want. + let content_offset = point(editor_margins.gutter.margin, Pixels::ZERO); let content_origin = text_hitbox.origin + content_offset; - let editor_text_bounds = - Bounds::from_corners(content_origin, bounds.bottom_right()); - - let height_in_lines = editor_text_bounds.size.height / line_height; - + let height_in_lines = bounds.size.height / line_height; let max_row = snapshot.max_point().row().as_f32(); // The max scroll position for the top of the window let max_scroll_top = if matches!( snapshot.mode, - EditorMode::SingleLine { .. } + EditorMode::SingleLine | EditorMode::AutoHeight { .. } | EditorMode::Full { sized_by_content: true, @@ -7696,23 +8156,33 @@ impl Element for EditorElement { } }; - // TODO: Autoscrolling for both axes - let mut autoscroll_request = None; - let mut autoscroll_containing_element = false; - let mut autoscroll_horizontally = false; - self.editor.update(cx, |editor, cx| { - autoscroll_request = editor.autoscroll_request(); - autoscroll_containing_element = + let ( + autoscroll_request, + autoscroll_containing_element, + needs_horizontal_autoscroll, + ) = self.editor.update(cx, |editor, cx| { + let autoscroll_request = editor.scroll_manager.take_autoscroll_request(); + + let autoscroll_containing_element = autoscroll_request.is_some() || editor.has_pending_selection(); - // TODO: Is this horizontal or vertical?! - autoscroll_horizontally = editor.autoscroll_vertically( - bounds, - line_height, - max_scroll_top, - window, - cx, - ); - snapshot = editor.snapshot(window, cx); + + let (needs_horizontal_autoscroll, was_scrolled) = editor + .autoscroll_vertically( + bounds, + line_height, + max_scroll_top, + autoscroll_request, + window, + cx, + ); + if was_scrolled.0 { + snapshot = editor.snapshot(window, cx); + } + ( + autoscroll_request, + autoscroll_containing_element, + needs_horizontal_autoscroll, + ) }); let mut scroll_position = snapshot.scroll_position(); @@ -7733,7 +8203,7 @@ impl Element for EditorElement { let is_row_soft_wrapped = |row: usize| { row_infos .get(row) - .map_or(true, |info| info.buffer_row.is_none()) + .is_none_or(|info| info.buffer_row.is_none()) }; let start_anchor = if start_row == Default::default() { @@ -7815,7 +8285,7 @@ impl Element for EditorElement { editor.read(cx).background_highlights_in_range( start_anchor..end_anchor, &snapshot.display_snapshot, - cx.theme().colors(), + cx.theme(), ) }) .unwrap_or_default(); @@ -7826,6 +8296,12 @@ impl Element for EditorElement { cx, ); + let document_colors = self + .editor + .read(cx) + .colors + .as_ref() + .map(|colors| colors.editor_display_highlights(&snapshot)); let redacted_ranges = self.editor.read(cx).redacted_ranges( start_anchor..end_anchor, &snapshot.display_snapshot, @@ -7884,11 +8360,9 @@ impl Element for EditorElement { let mut breakpoint_rows = self.editor.update(cx, |editor, cx| { editor.active_breakpoints(start_row..end_row, window, cx) }); - if cx.has_flag::() { - for (display_row, (_, bp, state)) in &breakpoint_rows { - if bp.is_enabled() && state.is_none_or(|s| s.verified) { - active_rows.entry(*display_row).or_default().breakpoint = true; - } + for (display_row, (_, bp, state)) in &breakpoint_rows { + if bp.is_enabled() && state.is_none_or(|s| s.verified) { + active_rows.entry(*display_row).or_default().breakpoint = true; } } @@ -7909,30 +8383,27 @@ impl Element for EditorElement { // We add the gutter breakpoint indicator to breakpoint_rows after painting // line numbers so we don't paint a line number debug accent color if a user // has their mouse over that line when a breakpoint isn't there - if cx.has_flag::() { - self.editor.update(cx, |editor, _| { - if let Some(phantom_breakpoint) = &mut editor - .gutter_breakpoint_indicator - .0 - .filter(|phantom_breakpoint| phantom_breakpoint.is_active) - { - // Is there a non-phantom breakpoint on this line? - phantom_breakpoint.collides_with_existing_breakpoint = true; - breakpoint_rows - .entry(phantom_breakpoint.display_row) - .or_insert_with(|| { - let position = snapshot.display_point_to_anchor( - DisplayPoint::new(phantom_breakpoint.display_row, 0), - Bias::Right, - ); - let breakpoint = Breakpoint::new_standard(); - phantom_breakpoint.collides_with_existing_breakpoint = - false; - (position, breakpoint, None) - }); - } - }) - } + self.editor.update(cx, |editor, _| { + if let Some(phantom_breakpoint) = &mut editor + .gutter_breakpoint_indicator + .0 + .filter(|phantom_breakpoint| phantom_breakpoint.is_active) + { + // Is there a non-phantom breakpoint on this line? + phantom_breakpoint.collides_with_existing_breakpoint = true; + breakpoint_rows + .entry(phantom_breakpoint.display_row) + .or_insert_with(|| { + let position = snapshot.display_point_to_anchor( + DisplayPoint::new(phantom_breakpoint.display_row, 0), + Bias::Right, + ); + let breakpoint = Breakpoint::new_standard(); + phantom_breakpoint.collides_with_existing_breakpoint = false; + (position, breakpoint, None) + }); + } + }); let mut expand_toggles = window.with_element_namespace("expand_toggles", |window| { @@ -7987,18 +8458,22 @@ impl Element for EditorElement { window, cx, ); - let new_fold_widths = line_layouts - .iter() - .flat_map(|layout| &layout.fragments) - .filter_map(|fragment| { - if let LineFragment::Element { id, size, .. } = fragment { - Some((*id, size.width)) - } else { - None - } - }); - if self.editor.update(cx, |editor, cx| { - editor.update_fold_widths(new_fold_widths, cx) + let new_renderer_widths = (!is_minimap).then(|| { + line_layouts + .iter() + .flat_map(|layout| &layout.fragments) + .filter_map(|fragment| { + if let LineFragment::Element { id, size, .. } = fragment { + Some((*id, size.width)) + } else { + None + } + }) + }); + if new_renderer_widths.is_some_and(|new_renderer_widths| { + self.editor.update(cx, |editor, cx| { + editor.update_renderer_widths(new_renderer_widths, cx) + }) }) { // If the fold widths have changed, we need to prepaint // the element again to account for any changes in @@ -8021,7 +8496,13 @@ impl Element for EditorElement { }) .flatten()?; let mut element = render_inline_blame_entry(blame_entry, &style, cx)?; - let inline_blame_padding = INLINE_BLAME_PADDING_EM_WIDTHS * em_advance; + let inline_blame_padding = ProjectSettings::get_global(cx) + .git + .inline_blame + .unwrap_or_default() + .padding + as f32 + * em_advance; Some( element .layout_as_root(AvailableSpace::min_size(), window, cx) @@ -8047,7 +8528,6 @@ impl Element for EditorElement { glyph_grid_cell, size(longest_line_width, max_row.as_f32() * line_height), longest_line_blame_width, - editor_width, EditorSettings::get_global(cx), ); @@ -8061,32 +8541,40 @@ impl Element for EditorElement { let sticky_header_excerpt_id = sticky_header_excerpt.as_ref().map(|top| top.excerpt.id); - let blocks = window.with_element_namespace("blocks", |window| { - self.render_blocks( - start_row..end_row, - &snapshot, - &hitbox, - &text_hitbox, - editor_width, - &mut scroll_width, - &editor_margins, - em_width, - gutter_dimensions.full_width(), - line_height, - &mut line_layouts, - &local_selections, - &selected_buffer_ids, - is_row_soft_wrapped, - sticky_header_excerpt_id, - window, - cx, - ) - }); + let blocks = (!is_minimap) + .then(|| { + window.with_element_namespace("blocks", |window| { + self.render_blocks( + start_row..end_row, + &snapshot, + &hitbox, + &text_hitbox, + editor_width, + &mut scroll_width, + &editor_margins, + em_width, + gutter_dimensions.full_width(), + line_height, + &mut line_layouts, + &local_selections, + &selected_buffer_ids, + is_row_soft_wrapped, + sticky_header_excerpt_id, + window, + cx, + ) + }) + }) + .unwrap_or_else(|| Ok((Vec::default(), HashMap::default()))); let (mut blocks, row_block_types) = match blocks { Ok(blocks) => blocks, Err(resized_blocks) => { self.editor.update(cx, |editor, cx| { - editor.resize_blocks(resized_blocks, autoscroll_request, cx) + editor.resize_blocks( + resized_blocks, + autoscroll_request.map(|(autoscroll, _)| autoscroll), + cx, + ) }); return self.prepaint(None, _inspector_id, bounds, &mut (), window, cx); } @@ -8115,37 +8603,35 @@ impl Element for EditorElement { MultiBufferRow(end_anchor.to_point(&snapshot.buffer_snapshot).row); let scroll_max = point( - ((scroll_width - editor_content_width) / em_width).max(0.0), + ((scroll_width - editor_width) / em_advance).max(0.0), max_scroll_top, ); self.editor.update(cx, |editor, cx| { - let clamped = editor.scroll_manager.clamp_scroll_left(scroll_max.x); + if editor.scroll_manager.clamp_scroll_left(scroll_max.x) { + scroll_position.x = scroll_position.x.min(scroll_max.x); + } - let autoscrolled = if autoscroll_horizontally { - editor.autoscroll_horizontally( + if needs_horizontal_autoscroll.0 + && let Some(new_scroll_position) = editor.autoscroll_horizontally( start_row, - editor_content_width, + editor_width, scroll_width, - em_width, + em_advance, &line_layouts, + autoscroll_request, + window, cx, ) - } else { - false - }; - - if clamped || autoscrolled { - snapshot = editor.snapshot(window, cx); - scroll_position = snapshot.scroll_position(); + { + scroll_position = new_scroll_position; } }); let scroll_pixel_position = point( - scroll_position.x * em_width, + scroll_position.x * em_advance, scroll_position.y * line_height, ); - let indent_guides = self.layout_indent_guides( content_origin, text_hitbox.origin, @@ -8171,7 +8657,7 @@ impl Element for EditorElement { ) }); - let (inline_completion_popover, inline_completion_popover_origin) = self + let (edit_prediction_popover, edit_prediction_popover_origin) = self .editor .update(cx, |editor, cx| { editor.render_edit_prediction_popover( @@ -8200,7 +8686,7 @@ impl Element for EditorElement { &row_block_types, content_origin, scroll_pixel_position, - inline_completion_popover_origin, + edit_prediction_popover_origin, start_row, end_row, line_height, @@ -8210,7 +8696,7 @@ impl Element for EditorElement { cx, ); - let mut inline_blame = None; + let mut inline_blame_layout = None; let mut inline_code_actions = None; if let Some(newest_selection_head) = newest_selection_head { let display_row = newest_selection_head.row(); @@ -8228,26 +8714,39 @@ impl Element for EditorElement { ); let line_ix = display_row.minus(start_row) as usize; - let row_info = &row_infos[line_ix]; - let line_layout = &line_layouts[line_ix]; - let crease_trailer_layout = crease_trailers[line_ix].as_ref(); - - inline_blame = self.layout_inline_blame( - display_row, - row_info, - line_layout, - crease_trailer_layout, - em_width, - content_origin, - scroll_pixel_position, - line_height, - &text_hitbox, - window, - cx, - ); - if inline_blame.is_some() { - // Blame overrides inline diagnostics - inline_diagnostics.remove(&display_row); + if let (Some(row_info), Some(line_layout), Some(crease_trailer)) = ( + row_infos.get(line_ix), + line_layouts.get(line_ix), + crease_trailers.get(line_ix), + ) { + let crease_trailer_layout = crease_trailer.as_ref(); + if let Some(layout) = self.layout_inline_blame( + display_row, + row_info, + line_layout, + crease_trailer_layout, + em_width, + content_origin, + scroll_pixel_position, + line_height, + &text_hitbox, + window, + cx, + ) { + inline_blame_layout = Some(layout); + // Blame overrides inline diagnostics + inline_diagnostics.remove(&display_row); + } + } else { + log::error!( + "bug: line_ix {} is out of bounds - row_infos.len(): {}, \ + line_layouts.len(): {}, \ + crease_trailers.len(): {}", + line_ix, + row_infos.len(), + line_layouts.len(), + crease_trailers.len(), + ); } } } @@ -8263,28 +8762,6 @@ impl Element for EditorElement { cx, ); - self.editor.update(cx, |editor, cx| { - let clamped = editor.scroll_manager.clamp_scroll_left(scroll_max.x); - - let autoscrolled = if autoscroll_horizontally { - editor.autoscroll_horizontally( - start_row, - editor_content_width, - scroll_width, - em_width, - &line_layouts, - cx, - ) - } else { - false - }; - - if clamped || autoscrolled { - snapshot = editor.snapshot(window, cx); - scroll_position = snapshot.scroll_position(); - } - }); - let line_elements = self.prepaint_lines( start_row, &mut line_layouts, @@ -8402,7 +8879,7 @@ impl Element for EditorElement { let show_breakpoints = snapshot .show_breakpoints .unwrap_or(gutter_settings.breakpoints); - let breakpoints = if cx.has_flag::() && show_breakpoints { + let breakpoints = if show_breakpoints { self.layout_breakpoints( line_height, start_row..end_row, @@ -8417,7 +8894,7 @@ impl Element for EditorElement { cx, ) } else { - vec![] + Vec::new() }; self.layout_signature_help( @@ -8475,6 +8952,17 @@ impl Element for EditorElement { self.prepaint_expand_toggles(&mut expand_toggles, window, cx) }); + let wrap_guides = self.layout_wrap_guides( + em_advance, + scroll_position, + content_origin, + scrollbars_layout.as_ref(), + vertical_scrollbar_width, + &hitbox, + window, + cx, + ); + let minimap = window.with_element_namespace("minimap", |window| { self.layout_minimap( &snapshot, @@ -8499,6 +8987,7 @@ impl Element for EditorElement { underline: None, strikethrough: None, }], + None, ); let space_invisible = window.text_system().shape_line( "•".into(), @@ -8511,10 +9000,30 @@ impl Element for EditorElement { underline: None, strikethrough: None, }], + None, ); let mode = snapshot.mode.clone(); + let (diff_hunk_controls, diff_hunk_control_bounds) = if is_read_only { + (vec![], vec![]) + } else { + self.layout_diff_hunk_controls( + start_row..end_row, + &row_infos, + &text_hitbox, + newest_selection_head, + line_height, + right_margin, + scroll_pixel_position, + &display_hunks, + &highlighted_rows, + self.editor.clone(), + window, + cx, + ) + }; + let position_map = Rc::new(PositionMap { size: bounds.size, visible_row_range, @@ -8527,32 +9036,17 @@ impl Element for EditorElement { snapshot, gutter_hitbox: gutter_hitbox.clone(), text_hitbox: text_hitbox.clone(), + inline_blame_bounds: inline_blame_layout + .as_ref() + .map(|layout| (layout.bounds, layout.entry.clone())), + display_hunks: display_hunks.clone(), + diff_hunk_control_bounds, }); self.editor.update(cx, |editor, _| { editor.last_position_map = Some(position_map.clone()) }); - let diff_hunk_controls = if is_read_only { - vec![] - } else { - self.layout_diff_hunk_controls( - start_row..end_row, - &row_infos, - &text_hitbox, - &position_map, - newest_selection_head, - line_height, - right_margin, - scroll_pixel_position, - &display_hunks, - &highlighted_rows, - self.editor.clone(), - window, - cx, - ) - }; - EditorLayout { mode, position_map, @@ -8570,17 +9064,18 @@ impl Element for EditorElement { highlighted_ranges, highlighted_gutter_ranges, redacted_ranges, + document_colors, line_elements, line_numbers, blamed_display_rows, inline_diagnostics, - inline_blame, + inline_blame_layout, inline_code_actions, blocks, cursors, visible_cursors, selections, - inline_completion_popover, + edit_prediction_popover, diff_hunk_controls, mouse_context_menu, test_indicators, @@ -8600,26 +9095,28 @@ impl Element for EditorElement { fn paint( &mut self, _: Option<&GlobalElementId>, - __inspector_id: Option<&gpui::InspectorElementId>, + _inspector_id: Option<&gpui::InspectorElementId>, bounds: Bounds, _: &mut Self::RequestLayoutState, layout: &mut Self::PrepaintState, window: &mut Window, cx: &mut App, ) { - let focus_handle = self.editor.focus_handle(cx); - let key_context = self - .editor - .update(cx, |editor, cx| editor.key_context(window, cx)); + if !layout.mode.is_minimap() { + let focus_handle = self.editor.focus_handle(cx); + let key_context = self + .editor + .update(cx, |editor, cx| editor.key_context(window, cx)); - window.set_key_context(key_context); - window.handle_input( - &focus_handle, - ElementInputHandler::new(bounds, self.editor.clone()), - cx, - ); - self.register_actions(window, cx); - self.register_key_listeners(window, cx, layout); + window.set_key_context(key_context); + window.handle_input( + &focus_handle, + ElementInputHandler::new(bounds, self.editor.clone()), + cx, + ); + self.register_actions(window, cx); + self.register_key_listeners(window, cx, layout); + } let text_style = TextStyleRefinement { font_size: Some(self.style.text.font_size), @@ -8660,7 +9157,7 @@ impl Element for EditorElement { self.paint_minimap(layout, window, cx); self.paint_scrollbars(layout, window, cx); - self.paint_inline_completion_popover(layout, window, cx); + self.paint_edit_prediction_popover(layout, window, cx); self.paint_mouse_context_menu(layout, window, cx); }); }) @@ -8700,7 +9197,6 @@ impl ScrollbarLayoutInformation { glyph_grid_cell: Size, document_size: Size, longest_line_blame_width: Pixels, - editor_width: Pixels, settings: &EditorSettings, ) -> Self { let vertical_overscroll = match settings.scroll_beyond_last_line { @@ -8711,19 +9207,11 @@ impl ScrollbarLayoutInformation { } }; - let right_margin = if document_size.width + longest_line_blame_width >= editor_width { - glyph_grid_cell.width - } else { - px(0.0) - }; - - let overscroll = size(right_margin + longest_line_blame_width, vertical_overscroll); - - let scroll_range = document_size + overscroll; + let overscroll = size(longest_line_blame_width, vertical_overscroll); ScrollbarLayoutInformation { editor_bounds, - scroll_range, + scroll_range: document_size + overscroll, glyph_grid_cell, } } @@ -8755,7 +9243,7 @@ pub struct EditorLayout { display_hunks: Vec<(DisplayDiffHunk, Option)>, blamed_display_rows: Option>, inline_diagnostics: HashMap, - inline_blame: Option, + inline_blame_layout: Option, inline_code_actions: Option, blocks: Vec, highlighted_ranges: Vec<(Range, Hsla)>, @@ -8770,11 +9258,12 @@ pub struct EditorLayout { expand_toggles: Vec)>>, diff_hunk_controls: Vec, crease_trailers: Vec>, - inline_completion_popover: Option, + edit_prediction_popover: Option, mouse_context_menu: Option, tab_invisible: ShapedLine, space_invisible: ShapedLine, sticky_buffer_header: Option, + document_colors: Option<(DocumentColorsRenderMode, Vec<(Range, Hsla)>)>, } impl EditorLayout { @@ -8827,7 +9316,7 @@ struct EditorScrollbars { impl EditorScrollbars { pub fn from_scrollbar_axes( - settings_visibility: ScrollbarAxes, + show_scrollbar: ScrollbarAxes, layout_information: &ScrollbarLayoutInformation, content_offset: gpui::Point, scroll_position: gpui::Point, @@ -8865,22 +9354,13 @@ impl EditorScrollbars { }; let mut create_scrollbar_layout = |axis| { - settings_visibility - .along(axis) + let viewport_size = viewport_size.along(axis); + let scroll_range = scroll_range.along(axis); + + // We always want a vertical scrollbar track for scrollbar diagnostic visibility. + (show_scrollbar.along(axis) + && (axis == ScrollbarAxis::Vertical || scroll_range > viewport_size)) .then(|| { - ( - viewport_size.along(axis) - content_offset.along(axis), - scroll_range.along(axis), - ) - }) - .filter(|(viewport_size, scroll_range)| { - // The scrollbar should only be rendered if the content does - // not entirely fit into the editor - // However, this only applies to the horizontal scrollbar, as information about the - // vertical scrollbar layout is always needed for scrollbar diagnostics. - axis != ScrollbarAxis::Horizontal || viewport_size < scroll_range - }) - .map(|(viewport_size, scroll_range)| { ScrollbarLayout::new( window.insert_hitbox(scrollbar_bounds_for(axis), HitboxBehavior::Normal), viewport_size, @@ -9179,7 +9659,10 @@ struct MinimapLayout { } impl MinimapLayout { - const MINIMAP_WIDTH: Pixels = px(100.); + /// The minimum width of the minimap in columns. If the minimap is smaller than this, it will be hidden. + const MINIMAP_MIN_WIDTH_COLUMNS: f32 = 20.; + /// The minimap width as a percentage of the editor width. + const MINIMAP_WIDTH_PCT: f32 = 0.15; /// Calculates the scroll top offset the minimap editor has to have based on the /// current scroll progress. fn calculate_minimap_top_offset( @@ -9215,6 +9698,9 @@ pub(crate) struct PositionMap { pub snapshot: EditorSnapshot, pub text_hitbox: Hitbox, pub gutter_hitbox: Hitbox, + pub inline_blame_bounds: Option<(Bounds, BlameEntry)>, + pub display_hunks: Vec<(DisplayDiffHunk, Option)>, + pub diff_hunk_control_bounds: Vec<(DisplayRow, Bounds)>, } #[derive(Debug, Copy, Clone)] @@ -9233,6 +9719,33 @@ impl PointForPosition { None } } + + pub fn intersects_selection(&self, selection: &Selection) -> bool { + let Some(valid_point) = self.as_valid() else { + return false; + }; + let range = selection.range(); + + let candidate_row = valid_point.row(); + let candidate_col = valid_point.column(); + + let start_row = range.start.row(); + let start_col = range.start.column(); + let end_row = range.end.row(); + let end_col = range.end.column(); + + if candidate_row < start_row || candidate_row > end_row { + false + } else if start_row == end_row { + candidate_col >= start_col && candidate_col < end_col + } else if candidate_row == start_row { + candidate_col >= start_col + } else if candidate_row == end_row { + candidate_col < end_col + } else { + true + } + } } impl PositionMap { @@ -9241,7 +9754,7 @@ impl PositionMap { let scroll_position = self.snapshot.scroll_position(); let position = position - text_bounds.origin; let y = position.y.max(px(0.)).min(self.size.height); - let x = position.x + (scroll_position.x * self.em_width); + let x = position.x + (scroll_position.x * self.em_advance); let row = ((y / self.line_height) + scroll_position.y) as u32; let (column, x_overshoot_after_line_end) = if let Some(line) = self @@ -9295,7 +9808,7 @@ pub fn layout_line( let chunks = snapshot.highlighted_chunks(row..row + DisplayRow(1), true, style); LineWithInvisibles::from_chunks( chunks, - &style, + style, MAX_LINE_LEN, 1, &snapshot.mode, @@ -9412,7 +9925,7 @@ impl CursorLayout { .px_0p5() .line_height(text_size + px(2.)) .text_color(cursor_name.color) - .child(cursor_name.string.clone()) + .child(cursor_name.string) .into_any_element(); name_element.prepaint_as_root(name_origin, AvailableSpace::min_size(), window, cx); @@ -9465,17 +9978,18 @@ pub struct HighlightedRangeLine { } impl HighlightedRange { - pub fn paint(&self, bounds: Bounds, window: &mut Window) { + pub fn paint(&self, fill: bool, bounds: Bounds, window: &mut Window) { if self.lines.len() >= 2 && self.lines[0].start_x > self.lines[1].end_x { - self.paint_lines(self.start_y, &self.lines[0..1], bounds, window); + self.paint_lines(self.start_y, &self.lines[0..1], fill, bounds, window); self.paint_lines( self.start_y + self.line_height, &self.lines[1..], + fill, bounds, window, ); } else { - self.paint_lines(self.start_y, &self.lines, bounds, window); + self.paint_lines(self.start_y, &self.lines, fill, bounds, window); } } @@ -9483,6 +9997,7 @@ impl HighlightedRange { &self, start_y: Pixels, lines: &[HighlightedRangeLine], + fill: bool, _bounds: Bounds, window: &mut Window, ) { @@ -9509,7 +10024,11 @@ impl HighlightedRange { }; let top_curve_width = curve_width(first_line.start_x, first_line.end_x); - let mut builder = gpui::PathBuilder::fill(); + let mut builder = if fill { + gpui::PathBuilder::fill() + } else { + gpui::PathBuilder::stroke(px(1.)) + }; builder.move_to(first_top_right - top_curve_width); builder.curve_to(first_top_right + curve_height, first_top_right); @@ -9619,7 +10138,8 @@ pub fn register_action( fn compute_auto_height_layout( editor: &mut Editor, - max_lines: usize, + min_lines: usize, + max_lines: Option, max_line_number_width: Pixels, known_dimensions: Size>, available_width: AvailableSpace, @@ -9658,25 +10178,32 @@ fn compute_auto_height_layout( let overscroll = size(em_width, px(0.)); let editor_width = text_width - gutter_dimensions.margin - overscroll.width - em_width; - if !matches!(editor.soft_wrap_mode(cx), SoftWrap::None) { - if editor.set_wrap_width(Some(editor_width), cx) { - snapshot = editor.snapshot(window, cx); - } + if !matches!(editor.soft_wrap_mode(cx), SoftWrap::None) + && editor.set_wrap_width(Some(editor_width), cx) + { + snapshot = editor.snapshot(window, cx); } let scroll_height = (snapshot.max_point().row().next_row().0 as f32) * line_height; - let height = scroll_height - .max(line_height) - .min(line_height * max_lines as f32); - Some(size(width, height)) + let min_height = line_height * min_lines as f32; + let content_height = scroll_height.max(min_height); + + let final_height = if let Some(max_lines) = max_lines { + let max_height = line_height * max_lines as f32; + content_height.min(max_height) + } else { + content_height + }; + + Some(size(width, final_height)) } #[cfg(test)] mod tests { use super::*; use crate::{ - Editor, MultiBuffer, + Editor, MultiBuffer, SelectionEffects, display_map::{BlockPlacement, BlockProperties}, editor_tests::{init_test, update_test_language_settings}, }; @@ -9686,6 +10213,71 @@ mod tests { use std::num::NonZeroU32; use util::test::sample_text; + #[gpui::test] + async fn test_soft_wrap_editor_width_auto_height_editor(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let window = cx.add_window(|window, cx| { + let buffer = MultiBuffer::build_simple(&"a ".to_string().repeat(100), cx); + let mut editor = Editor::new( + EditorMode::AutoHeight { + min_lines: 1, + max_lines: None, + }, + buffer, + None, + window, + cx, + ); + editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx); + editor + }); + let cx = &mut VisualTestContext::from_window(*window, cx); + let editor = window.root(cx).unwrap(); + let style = cx.update(|_, cx| editor.read(cx).style().unwrap().clone()); + + for x in 1..=100 { + let (_, state) = cx.draw( + Default::default(), + size(px(200. + 0.13 * x as f32), px(500.)), + |_, _| EditorElement::new(&editor, style.clone()), + ); + + assert!( + state.position_map.scroll_max.x == 0., + "Soft wrapped editor should have no horizontal scrolling!" + ); + } + } + + #[gpui::test] + async fn test_soft_wrap_editor_width_full_editor(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let window = cx.add_window(|window, cx| { + let buffer = MultiBuffer::build_simple(&"a ".to_string().repeat(100), cx); + let mut editor = Editor::new(EditorMode::full(), buffer, None, window, cx); + editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx); + editor + }); + let cx = &mut VisualTestContext::from_window(*window, cx); + let editor = window.root(cx).unwrap(); + let style = cx.update(|_, cx| editor.read(cx).style().unwrap().clone()); + + for x in 1..=100 { + let (_, state) = cx.draw( + Default::default(), + size(px(200. + 0.13 * x as f32), px(500.)), + |_, _| EditorElement::new(&editor, style.clone()), + ); + + assert!( + state.position_map.scroll_max.x == 0., + "Soft wrapped editor should have no horizontal scrolling!" + ); + } + } + #[gpui::test] fn test_shape_line_numbers(cx: &mut TestAppContext) { init_test(cx, |_| {}); @@ -9801,7 +10393,7 @@ mod tests { window .update(cx, |editor, window, cx| { editor.cursor_shape = CursorShape::Block; - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([ Point::new(0, 0)..Point::new(1, 0), Point::new(3, 2)..Point::new(3, 3), @@ -9878,7 +10470,6 @@ mod tests { height: Some(3), render: Arc::new(|cx| div().h(3. * cx.window.line_height()).into_any()), priority: 0, - render_in_minimap: true, }], None, cx, @@ -9968,8 +10559,11 @@ mod tests { }); for editor_mode_without_invisibles in [ - EditorMode::SingleLine { auto_width: false }, - EditorMode::AutoHeight { max_lines: 100 }, + EditorMode::SingleLine, + EditorMode::AutoHeight { + min_lines: 1, + max_lines: Some(100), + }, ] { for show_line_numbers in [true, false] { let invisibles = collect_invisibles_from_new_editor( diff --git a/crates/editor/src/git/blame.rs b/crates/editor/src/git/blame.rs index d4c9e37895..b11617ccec 100644 --- a/crates/editor/src/git/blame.rs +++ b/crates/editor/src/git/blame.rs @@ -213,8 +213,8 @@ impl GitBlame { let project_subscription = cx.subscribe(&project, { let buffer = buffer.clone(); - move |this, _, event, cx| match event { - project::Event::WorktreeUpdatedEntries(_, updated) => { + move |this, _, event, cx| { + if let project::Event::WorktreeUpdatedEntries(_, updated) = event { let project_entry_id = buffer.read(cx).entry_id(cx); if updated .iter() @@ -224,7 +224,6 @@ impl GitBlame { this.generate(cx); } } - _ => {} } }); @@ -292,11 +291,11 @@ impl GitBlame { let buffer_id = self.buffer_snapshot.remote_id(); let mut cursor = self.entries.cursor::(&()); - rows.into_iter().map(move |info| { + rows.iter().map(move |info| { let row = info .buffer_row .filter(|_| info.buffer_id == Some(buffer_id))?; - cursor.seek_forward(&row, Bias::Right, &()); + cursor.seek_forward(&row, Bias::Right); cursor.item()?.blame.clone() }) } @@ -312,10 +311,10 @@ impl GitBlame { .as_ref() .and_then(|entry| entry.author.as_ref()) .map(|author| author.len()); - if let Some(author_len) = author_len { - if author_len > max_author_length { - max_author_length = author_len; - } + if let Some(author_len) = author_len + && author_len > max_author_length + { + max_author_length = author_len; } } @@ -389,7 +388,7 @@ impl GitBlame { } } - new_entries.append(cursor.slice(&edit.old.start, Bias::Right, &()), &()); + new_entries.append(cursor.slice(&edit.old.start, Bias::Right), &()); if edit.new.start > new_entries.summary().rows { new_entries.push( @@ -401,7 +400,7 @@ impl GitBlame { ); } - cursor.seek(&edit.old.end, Bias::Right, &()); + cursor.seek(&edit.old.end, Bias::Right); if !edit.new.is_empty() { new_entries.push( GitBlameEntry { @@ -412,27 +411,26 @@ impl GitBlame { ); } - let old_end = cursor.end(&()); + let old_end = cursor.end(); if row_edits .peek() - .map_or(true, |next_edit| next_edit.old.start >= old_end) + .is_none_or(|next_edit| next_edit.old.start >= old_end) + && let Some(entry) = cursor.item() { - if let Some(entry) = cursor.item() { - if old_end > edit.old.end { - new_entries.push( - GitBlameEntry { - rows: cursor.end(&()) - edit.old.end, - blame: entry.blame.clone(), - }, - &(), - ); - } - - cursor.next(&()); + if old_end > edit.old.end { + new_entries.push( + GitBlameEntry { + rows: cursor.end() - edit.old.end, + blame: entry.blame.clone(), + }, + &(), + ); } + + cursor.next(); } } - new_entries.append(cursor.suffix(&()), &()); + new_entries.append(cursor.suffix(), &()); drop(cursor); self.buffer_snapshot = new_snapshot; diff --git a/crates/editor/src/highlight_matching_bracket.rs b/crates/editor/src/highlight_matching_bracket.rs index 35c134a885..aa4e616924 100644 --- a/crates/editor/src/highlight_matching_bracket.rs +++ b/crates/editor/src/highlight_matching_bracket.rs @@ -1,6 +1,7 @@ use crate::{Editor, RangeToAnchorExt}; -use gpui::{Context, Window}; +use gpui::{Context, HighlightStyle, Window}; use language::CursorShape; +use theme::ActiveTheme; enum MatchingBracketHighlight {} @@ -9,7 +10,7 @@ pub fn refresh_matching_bracket_highlights( window: &mut Window, cx: &mut Context, ) { - editor.clear_background_highlights::(cx); + editor.clear_highlights::(cx); let newest_selection = editor.selections.newest::(cx); // Don't highlight brackets if the selection isn't empty @@ -19,6 +20,11 @@ pub fn refresh_matching_bracket_highlights( let snapshot = editor.snapshot(window, cx); let head = newest_selection.head(); + if head > snapshot.buffer_snapshot.len() { + log::error!("bug: cursor offset is out of range while refreshing bracket highlights"); + return; + } + let mut tail = head; if (editor.cursor_shape == CursorShape::Block || editor.cursor_shape == CursorShape::Hollow) && head < snapshot.buffer_snapshot.len() @@ -30,12 +36,19 @@ pub fn refresh_matching_bracket_highlights( .buffer_snapshot .innermost_enclosing_bracket_ranges(head..tail, None) { - editor.highlight_background::( - &[ + editor.highlight_text::( + vec![ opening_range.to_anchors(&snapshot.buffer_snapshot), closing_range.to_anchors(&snapshot.buffer_snapshot), ], - |theme| theme.editor_document_highlight_bracket_background, + HighlightStyle { + background_color: Some( + cx.theme() + .colors() + .editor_document_highlight_bracket_background, + ), + ..Default::default() + }, cx, ) } @@ -99,7 +112,7 @@ mod tests { another_test(1, 2, 3); } "#}); - cx.assert_editor_background_highlights::(indoc! {r#" + cx.assert_editor_text_highlights::(indoc! {r#" pub fn test«(»"Test argument"«)» { another_test(1, 2, 3); } @@ -110,7 +123,7 @@ mod tests { another_test(1, ˇ2, 3); } "#}); - cx.assert_editor_background_highlights::(indoc! {r#" + cx.assert_editor_text_highlights::(indoc! {r#" pub fn test("Test argument") { another_test«(»1, 2, 3«)»; } @@ -121,7 +134,7 @@ mod tests { anotherˇ_test(1, 2, 3); } "#}); - cx.assert_editor_background_highlights::(indoc! {r#" + cx.assert_editor_text_highlights::(indoc! {r#" pub fn test("Test argument") «{» another_test(1, 2, 3); «}» @@ -133,7 +146,7 @@ mod tests { another_test(1, 2, 3); } "#}); - cx.assert_editor_background_highlights::(indoc! {r#" + cx.assert_editor_text_highlights::(indoc! {r#" pub fn test("Test argument") { another_test(1, 2, 3); } @@ -145,8 +158,8 @@ mod tests { another_test(1, 2, 3); } "#}); - cx.assert_editor_background_highlights::(indoc! {r#" - pub fn test("Test argument") { + cx.assert_editor_text_highlights::(indoc! {r#" + pub fn test«("Test argument") { another_test(1, 2, 3); } "#}); diff --git a/crates/editor/src/hover_links.rs b/crates/editor/src/hover_links.rs index 927981e6e6..94f49f601a 100644 --- a/crates/editor/src/hover_links.rs +++ b/crates/editor/src/hover_links.rs @@ -1,7 +1,7 @@ use crate::{ Anchor, Editor, EditorSettings, EditorSnapshot, FindAllReferences, GoToDefinition, GoToTypeDefinition, GotoDefinitionKind, InlayId, Navigated, PointForPosition, SelectPhase, - editor_settings::{GoToDefinitionFallback, MultiCursorModifier}, + editor_settings::GoToDefinitionFallback, hover_popover::{self, InlayHover}, scroll::ScrollAmount, }; @@ -120,11 +120,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - let multi_cursor_setting = EditorSettings::get_global(cx).multi_cursor_modifier; - let hovered_link_modifier = match multi_cursor_setting { - MultiCursorModifier::Alt => modifiers.secondary(), - MultiCursorModifier::CmdOrCtrl => modifiers.alt, - }; + let hovered_link_modifier = Editor::multi_cursor_modifier(false, &modifiers, cx); if !hovered_link_modifier || self.has_pending_selection() { self.hide_hovered_link(cx); return; @@ -275,7 +271,7 @@ impl Editor { Task::ready(Ok(Navigated::No)) }; self.select(SelectPhase::End, window, cx); - return navigate_task; + navigate_task } } @@ -325,7 +321,10 @@ pub fn update_inlay_link_and_hover_points( if let Some(cached_hint) = inlay_hint_cache.hint_by_id(excerpt_id, hovered_hint.id) { match cached_hint.resolve_state { ResolveState::CanResolve(_, _) => { - if let Some(buffer_id) = previous_valid_anchor.buffer_id { + if let Some(buffer_id) = snapshot + .buffer_snapshot + .buffer_id_for_anchor(previous_valid_anchor) + { inlay_hint_cache.spawn_hint_resolve( buffer_id, excerpt_id, @@ -422,24 +421,22 @@ pub fn update_inlay_link_and_hover_points( } if let Some((language_server_id, location)) = hovered_hint_part.location + && secondary_held + && !editor.has_pending_nonempty_selection() { - if secondary_held - && !editor.has_pending_nonempty_selection() - { - go_to_definition_updated = true; - show_link_definition( - shift_held, - editor, - TriggerPoint::InlayHint( - highlight, - location, - language_server_id, - ), - snapshot, - window, - cx, - ); - } + go_to_definition_updated = true; + show_link_definition( + shift_held, + editor, + TriggerPoint::InlayHint( + highlight, + location, + language_server_id, + ), + snapshot, + window, + cx, + ); } } } @@ -565,7 +562,7 @@ pub fn show_link_definition( provider.definitions(&buffer, buffer_position, preferred_kind, cx) })?; if let Some(task) = task { - task.await.ok().map(|definition_result| { + task.await.ok().flatten().map(|definition_result| { ( definition_result.iter().find_map(|link| { link.origin.as_ref().and_then(|origin| { @@ -661,11 +658,11 @@ pub fn show_link_definition( pub(crate) fn find_url( buffer: &Entity, position: text::Anchor, - mut cx: AsyncWindowContext, + cx: AsyncWindowContext, ) -> Option<(Range, String)> { const LIMIT: usize = 2048; - let Ok(snapshot) = buffer.read_with(&mut cx, |buffer, _| buffer.snapshot()) else { + let Ok(snapshot) = buffer.read_with(&cx, |buffer, _| buffer.snapshot()) else { return None; }; @@ -723,11 +720,11 @@ pub(crate) fn find_url( pub(crate) fn find_url_from_range( buffer: &Entity, range: Range, - mut cx: AsyncWindowContext, + cx: AsyncWindowContext, ) -> Option { const LIMIT: usize = 2048; - let Ok(snapshot) = buffer.read_with(&mut cx, |buffer, _| buffer.snapshot()) else { + let Ok(snapshot) = buffer.read_with(&cx, |buffer, _| buffer.snapshot()) else { return None; }; @@ -770,10 +767,11 @@ pub(crate) fn find_url_from_range( let mut finder = LinkFinder::new(); finder.kinds(&[LinkKind::Url]); - if let Some(link) = finder.links(&text).next() { - if link.start() == 0 && link.end() == text.len() { - return Some(link.as_str().to_string()); - } + if let Some(link) = finder.links(&text).next() + && link.start() == 0 + && link.end() == text.len() + { + return Some(link.as_str().to_string()); } None @@ -798,7 +796,7 @@ pub(crate) async fn find_file( ) -> Option { project .update(cx, |project, cx| { - project.resolve_path_in_buffer(&candidate_file_path, buffer, cx) + project.resolve_path_in_buffer(candidate_file_path, buffer, cx) }) .ok()? .await @@ -876,7 +874,7 @@ fn surrounding_filename( .peekable(); while let Some(ch) = forwards.next() { // Skip escaped whitespace - if ch == '\\' && forwards.peek().map_or(false, |ch| ch.is_whitespace()) { + if ch == '\\' && forwards.peek().is_some_and(|ch| ch.is_whitespace()) { token_end += ch.len_utf8(); let whitespace = forwards.next().unwrap(); token_end += whitespace.len_utf8(); @@ -1261,7 +1259,7 @@ mod tests { let snapshot = editor.buffer().read(cx).snapshot(cx); let anchor_range = snapshot.anchor_before(selection_range.start) ..snapshot.anchor_after(selection_range.end); - editor.change_selections(Some(crate::Autoscroll::fit()), window, cx, |s| { + editor.change_selections(Default::default(), window, cx, |s| { s.set_pending_anchor_range(anchor_range, crate::SelectMode::Character) }); }); diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 974870bf2c..fab5345787 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -3,7 +3,7 @@ use crate::{ EditorSnapshot, GlobalDiagnosticRenderer, Hover, display_map::{InlayOffset, ToDisplayPoint, invisibles::is_invisible}, hover_links::{InlayHighlight, RangeInEditor}, - scroll::{Autoscroll, ScrollAmount}, + scroll::ScrollAmount, }; use anyhow::Context as _; use gpui::{ @@ -142,11 +142,11 @@ pub fn hover_at_inlay( .info_popovers .iter() .any(|InfoPopover { symbol_range, .. }| { - if let RangeInEditor::Inlay(range) = symbol_range { - if range == &inlay_hover.range { - // Hover triggered from same location as last time. Don't show again. - return true; - } + if let RangeInEditor::Inlay(range) = symbol_range + && range == &inlay_hover.range + { + // Hover triggered from same location as last time. Don't show again. + return true; } false }) @@ -167,17 +167,16 @@ pub fn hover_at_inlay( let language_registry = project.read_with(cx, |p, _| p.languages().clone())?; let blocks = vec![inlay_hover.tooltip]; - let parsed_content = parse_blocks(&blocks, &language_registry, None, cx).await; + let parsed_content = + parse_blocks(&blocks, Some(&language_registry), None, cx).await; let scroll_handle = ScrollHandle::new(); let subscription = this .update(cx, |_, cx| { - if let Some(parsed_content) = &parsed_content { - Some(cx.observe(parsed_content, |_, _, cx| cx.notify())) - } else { - None - } + parsed_content.as_ref().map(|parsed_content| { + cx.observe(parsed_content, |_, _, cx| cx.notify()) + }) }) .ok() .flatten(); @@ -251,7 +250,9 @@ fn show_hover( let (excerpt_id, _, _) = editor.buffer().read(cx).excerpt_containing(anchor, cx)?; - let language_registry = editor.project.as_ref()?.read(cx).languages().clone(); + let language_registry = editor + .project() + .map(|project| project.read(cx).languages().clone()); let provider = editor.semantics_provider.clone()?; if !ignore_timeout { @@ -267,13 +268,12 @@ fn show_hover( } // Don't request again if the location is the same as the previous request - if let Some(triggered_from) = &editor.hover_state.triggered_from { - if triggered_from + if let Some(triggered_from) = &editor.hover_state.triggered_from + && triggered_from .cmp(&anchor, &snapshot.buffer_snapshot) .is_eq() - { - return None; - } + { + return None; } let hover_popover_delay = EditorSettings::get_global(cx).hover_popover_delay; @@ -381,10 +381,14 @@ fn show_hover( .anchor_after(local_diagnostic.range.end), }; + let scroll_handle = ScrollHandle::new(); + Some(DiagnosticPopover { local_diagnostic, markdown, border_color, + scrollbar_state: ScrollbarState::new(scroll_handle.clone()), + scroll_handle, background_color, keyboard_grace: Rc::new(RefCell::new(ignore_timeout)), anchor, @@ -424,7 +428,7 @@ fn show_hover( }; let hovers_response = if let Some(hover_request) = hover_request { - hover_request.await + hover_request.await.unwrap_or_default() } else { Vec::new() }; @@ -439,15 +443,14 @@ fn show_hover( text: format!("Unicode character U+{:02X}", invisible as u32), kind: HoverBlockKind::PlainText, }]; - let parsed_content = parse_blocks(&blocks, &language_registry, None, cx).await; + let parsed_content = + parse_blocks(&blocks, language_registry.as_ref(), None, cx).await; let scroll_handle = ScrollHandle::new(); let subscription = this .update(cx, |_, cx| { - if let Some(parsed_content) = &parsed_content { - Some(cx.observe(parsed_content, |_, _, cx| cx.notify())) - } else { - None - } + parsed_content.as_ref().map(|parsed_content| { + cx.observe(parsed_content, |_, _, cx| cx.notify()) + }) }) .ok() .flatten(); @@ -489,16 +492,15 @@ fn show_hover( let blocks = hover_result.contents; let language = hover_result.language; - let parsed_content = parse_blocks(&blocks, &language_registry, language, cx).await; + let parsed_content = + parse_blocks(&blocks, language_registry.as_ref(), language, cx).await; let scroll_handle = ScrollHandle::new(); hover_highlights.push(range.clone()); let subscription = this .update(cx, |_, cx| { - if let Some(parsed_content) = &parsed_content { - Some(cx.observe(parsed_content, |_, _, cx| cx.notify())) - } else { - None - } + parsed_content.as_ref().map(|parsed_content| { + cx.observe(parsed_content, |_, _, cx| cx.notify()) + }) }) .ok() .flatten(); @@ -520,7 +522,7 @@ fn show_hover( // Highlight the selected symbol using a background highlight editor.highlight_background::( &hover_highlights, - |theme| theme.element_hover, // todo update theme + |theme| theme.colors().element_hover, // todo update theme cx, ); } @@ -579,7 +581,7 @@ fn same_diagnostic_hover(editor: &Editor, snapshot: &EditorSnapshot, anchor: Anc async fn parse_blocks( blocks: &[HoverBlock], - language_registry: &Arc, + language_registry: Option<&Arc>, language: Option>, cx: &mut AsyncWindowContext, ) -> Option> { @@ -595,18 +597,15 @@ async fn parse_blocks( }) .join("\n\n"); - let rendered_block = cx - .new_window_entity(|_window, cx| { - Markdown::new( - combined_text.into(), - Some(language_registry.clone()), - language.map(|language| language.name()), - cx, - ) - }) - .ok(); - - rendered_block + cx.new_window_entity(|_window, cx| { + Markdown::new( + combined_text.into(), + language_registry.cloned(), + language.map(|language| language.name()), + cx, + ) + }) + .ok() } pub fn hover_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { @@ -618,7 +617,7 @@ pub fn hover_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { let mut base_text_style = window.text_style(); base_text_style.refine(&TextStyleRefinement { - font_family: Some(ui_font_family.clone()), + font_family: Some(ui_font_family), font_fallbacks: ui_font_fallbacks, color: Some(cx.theme().colors().editor_foreground), ..Default::default() @@ -648,7 +647,7 @@ pub fn hover_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { ..Default::default() }, syntax: cx.theme().syntax().clone(), - selection_background_color: { cx.theme().players().local().selection }, + selection_background_color: cx.theme().colors().element_selection_background, heading: StyleRefinement::default() .font_weight(FontWeight::BOLD) .text_base() @@ -667,7 +666,7 @@ pub fn diagnostics_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { let mut base_text_style = window.text_style(); base_text_style.refine(&TextStyleRefinement { - font_family: Some(ui_font_family.clone()), + font_family: Some(ui_font_family), font_fallbacks: ui_font_fallbacks, color: Some(cx.theme().colors().editor_foreground), ..Default::default() @@ -697,7 +696,7 @@ pub fn diagnostics_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { ..Default::default() }, syntax: cx.theme().syntax().clone(), - selection_background_color: { cx.theme().players().local().selection }, + selection_background_color: cx.theme().colors().element_selection_background, height_is_multiple_of_line_height: true, heading: StyleRefinement::default() .font_weight(FontWeight::BOLD) @@ -708,59 +707,54 @@ pub fn diagnostics_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { } pub fn open_markdown_url(link: SharedString, window: &mut Window, cx: &mut App) { - if let Ok(uri) = Url::parse(&link) { - if uri.scheme() == "file" { - if let Some(workspace) = window.root::().flatten() { - workspace.update(cx, |workspace, cx| { - let task = workspace.open_abs_path( - PathBuf::from(uri.path()), - OpenOptions { - visible: Some(OpenVisible::None), - ..Default::default() - }, - window, - cx, - ); + if let Ok(uri) = Url::parse(&link) + && uri.scheme() == "file" + && let Some(workspace) = window.root::().flatten() + { + workspace.update(cx, |workspace, cx| { + let task = workspace.open_abs_path( + PathBuf::from(uri.path()), + OpenOptions { + visible: Some(OpenVisible::None), + ..Default::default() + }, + window, + cx, + ); - cx.spawn_in(window, async move |_, cx| { - let item = task.await?; - // Ruby LSP uses URLs with #L1,1-4,4 - // we'll just take the first number and assume it's a line number - let Some(fragment) = uri.fragment() else { - return anyhow::Ok(()); - }; - let mut accum = 0u32; - for c in fragment.chars() { - if c >= '0' && c <= '9' && accum < u32::MAX / 2 { - accum *= 10; - accum += c as u32 - '0' as u32; - } else if accum > 0 { - break; - } - } - if accum == 0 { - return Ok(()); - } - let Some(editor) = cx.update(|_, cx| item.act_as::(cx))? else { - return Ok(()); - }; - editor.update_in(cx, |editor, window, cx| { - editor.change_selections( - Some(Autoscroll::fit()), - window, - cx, - |selections| { - selections.select_ranges([text::Point::new(accum - 1, 0) - ..text::Point::new(accum - 1, 0)]); - }, - ); - }) - }) - .detach_and_log_err(cx); - }); - return; - } - } + cx.spawn_in(window, async move |_, cx| { + let item = task.await?; + // Ruby LSP uses URLs with #L1,1-4,4 + // we'll just take the first number and assume it's a line number + let Some(fragment) = uri.fragment() else { + return anyhow::Ok(()); + }; + let mut accum = 0u32; + for c in fragment.chars() { + if c >= '0' && c <= '9' && accum < u32::MAX / 2 { + accum *= 10; + accum += c as u32 - '0' as u32; + } else if accum > 0 { + break; + } + } + if accum == 0 { + return Ok(()); + } + let Some(editor) = cx.update(|_, cx| item.act_as::(cx))? else { + return Ok(()); + }; + editor.update_in(cx, |editor, window, cx| { + editor.change_selections(Default::default(), window, cx, |selections| { + selections.select_ranges([ + text::Point::new(accum - 1, 0)..text::Point::new(accum - 1, 0) + ]); + }); + }) + }) + .detach_and_log_err(cx); + }); + return; } cx.open_url(&link); } @@ -830,21 +824,20 @@ impl HoverState { pub fn focused(&self, window: &mut Window, cx: &mut Context) -> bool { let mut hover_popover_is_focused = false; for info_popover in &self.info_popovers { - if let Some(markdown_view) = &info_popover.parsed_content { - if markdown_view.focus_handle(cx).is_focused(window) { - hover_popover_is_focused = true; - } - } - } - if let Some(diagnostic_popover) = &self.diagnostic_popover { - if diagnostic_popover - .markdown - .focus_handle(cx) - .is_focused(window) + if let Some(markdown_view) = &info_popover.parsed_content + && markdown_view.focus_handle(cx).is_focused(window) { hover_popover_is_focused = true; } } + if let Some(diagnostic_popover) = &self.diagnostic_popover + && diagnostic_popover + .markdown + .focus_handle(cx) + .is_focused(window) + { + hover_popover_is_focused = true; + } hover_popover_is_focused } } @@ -869,6 +862,7 @@ impl InfoPopover { let keyboard_grace = Rc::clone(&self.keyboard_grace); div() .id("info_popover") + .occlude() .elevation_2(cx) // Prevent a mouse down/move on the popover from being propagated to the editor, // because that would dismiss the popover. @@ -954,6 +948,8 @@ pub struct DiagnosticPopover { pub keyboard_grace: Rc>, pub anchor: Anchor, _subscription: Subscription, + pub scroll_handle: ScrollHandle, + pub scrollbar_state: ScrollbarState, } impl DiagnosticPopover { @@ -967,10 +963,7 @@ impl DiagnosticPopover { let this = cx.entity().downgrade(); div() .id("diagnostic") - .block() - .max_h(max_size.height) - .overflow_y_scroll() - .max_w(max_size.width) + .occlude() .elevation_2_borderless(cx) // Don't draw the background color if the theme // allows transparent surfaces. @@ -991,27 +984,72 @@ impl DiagnosticPopover { div() .py_1() .px_2() - .child( - MarkdownElement::new( - self.markdown.clone(), - diagnostics_markdown_style(window, cx), - ) - .on_url_click(move |link, window, cx| { - if let Some(renderer) = GlobalDiagnosticRenderer::global(cx) { - this.update(cx, |this, cx| { - renderer.as_ref().open_link(this, link, window, cx); - }) - .ok(); - } - }), - ) .bg(self.background_color) .border_1() .border_color(self.border_color) - .rounded_lg(), + .rounded_lg() + .child( + div() + .id("diagnostic-content-container") + .overflow_y_scroll() + .max_w(max_size.width) + .max_h(max_size.height) + .track_scroll(&self.scroll_handle) + .child( + MarkdownElement::new( + self.markdown.clone(), + diagnostics_markdown_style(window, cx), + ) + .on_url_click( + move |link, window, cx| { + if let Some(renderer) = GlobalDiagnosticRenderer::global(cx) + { + this.update(cx, |this, cx| { + renderer.as_ref().open_link(this, link, window, cx); + }) + .ok(); + } + }, + ), + ), + ) + .child(self.render_vertical_scrollbar(cx)), ) .into_any_element() } + + fn render_vertical_scrollbar(&self, cx: &mut Context) -> Stateful
{ + div() + .occlude() + .id("diagnostic-popover-vertical-scroll") + .on_mouse_move(cx.listener(|_, _, _, cx| { + cx.notify(); + cx.stop_propagation() + })) + .on_hover(|_, _, cx| { + cx.stop_propagation(); + }) + .on_any_mouse_down(|_, _, cx| { + cx.stop_propagation(); + }) + .on_mouse_up( + MouseButton::Left, + cx.listener(|_, _, _, cx| { + cx.stop_propagation(); + }), + ) + .on_scroll_wheel(cx.listener(|_, _, _, cx| { + cx.notify(); + })) + .h_full() + .absolute() + .right_1() + .top_1() + .bottom_0() + .w(px(12.)) + .cursor_default() + .children(Scrollbar::vertical(self.scrollbar_state.clone())) + } } #[cfg(test)] @@ -1095,14 +1133,15 @@ mod tests { //prompt autocompletion menu cx.simulate_keystroke("."); handle_completion_request( - &mut cx, indoc! {" one.|<> two three "}, vec!["first_completion", "second_completion"], + true, counter.clone(), + &mut cx, ) .await; cx.condition(|editor, _| editor.context_menu_visible()) // wait until completion menu is visible diff --git a/crates/editor/src/indent_guides.rs b/crates/editor/src/indent_guides.rs index f6d51c929a..23717eeb15 100644 --- a/crates/editor/src/indent_guides.rs +++ b/crates/editor/src/indent_guides.rs @@ -164,15 +164,15 @@ pub fn indent_guides_in_range( let end_anchor = snapshot.buffer_snapshot.anchor_after(end_offset); let mut fold_ranges = Vec::>::new(); - let mut folds = snapshot.folds_in_range(start_offset..end_offset).peekable(); - while let Some(fold) = folds.next() { + let folds = snapshot.folds_in_range(start_offset..end_offset).peekable(); + for fold in folds { let start = fold.range.start.to_point(&snapshot.buffer_snapshot); let end = fold.range.end.to_point(&snapshot.buffer_snapshot); - if let Some(last_range) = fold_ranges.last_mut() { - if last_range.end >= start { - last_range.end = last_range.end.max(end); - continue; - } + if let Some(last_range) = fold_ranges.last_mut() + && last_range.end >= start + { + last_range.end = last_range.end.max(end); + continue; } fold_ranges.push(start..end); } diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index dcfa8429a0..dbf5ac95b7 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -475,10 +475,7 @@ impl InlayHintCache { let excerpt_cached_hints = excerpt_cached_hints.read(); let mut excerpt_cache = excerpt_cached_hints.ordered_hints.iter().fuse().peekable(); shown_excerpt_hints_to_remove.retain(|(shown_anchor, shown_hint_id)| { - let Some(buffer) = shown_anchor - .buffer_id - .and_then(|buffer_id| multi_buffer.buffer(buffer_id)) - else { + let Some(buffer) = multi_buffer.buffer_for_anchor(*shown_anchor, cx) else { return false; }; let buffer_snapshot = buffer.read(cx).snapshot(); @@ -498,16 +495,14 @@ impl InlayHintCache { cmp::Ordering::Less | cmp::Ordering::Equal => { if !old_kinds.contains(&cached_hint.kind) && new_kinds.contains(&cached_hint.kind) - { - if let Some(anchor) = multi_buffer_snapshot + && let Some(anchor) = multi_buffer_snapshot .anchor_in_excerpt(*excerpt_id, cached_hint.position) - { - to_insert.push(Inlay::hint( - cached_hint_id.id(), - anchor, - cached_hint, - )); - } + { + to_insert.push(Inlay::hint( + cached_hint_id.id(), + anchor, + cached_hint, + )); } excerpt_cache.next(); } @@ -522,16 +517,16 @@ impl InlayHintCache { for cached_hint_id in excerpt_cache { let maybe_missed_cached_hint = &excerpt_cached_hints.hints_by_id[cached_hint_id]; let cached_hint_kind = maybe_missed_cached_hint.kind; - if !old_kinds.contains(&cached_hint_kind) && new_kinds.contains(&cached_hint_kind) { - if let Some(anchor) = multi_buffer_snapshot + if !old_kinds.contains(&cached_hint_kind) + && new_kinds.contains(&cached_hint_kind) + && let Some(anchor) = multi_buffer_snapshot .anchor_in_excerpt(*excerpt_id, maybe_missed_cached_hint.position) - { - to_insert.push(Inlay::hint( - cached_hint_id.id(), - anchor, - maybe_missed_cached_hint, - )); - } + { + to_insert.push(Inlay::hint( + cached_hint_id.id(), + anchor, + maybe_missed_cached_hint, + )); } } } @@ -620,44 +615,44 @@ impl InlayHintCache { ) { if let Some(excerpt_hints) = self.hints.get(&excerpt_id) { let mut guard = excerpt_hints.write(); - if let Some(cached_hint) = guard.hints_by_id.get_mut(&id) { - if let ResolveState::CanResolve(server_id, _) = &cached_hint.resolve_state { - let hint_to_resolve = cached_hint.clone(); - let server_id = *server_id; - cached_hint.resolve_state = ResolveState::Resolving; - drop(guard); - cx.spawn_in(window, async move |editor, cx| { - let resolved_hint_task = editor.update(cx, |editor, cx| { - let buffer = editor.buffer().read(cx).buffer(buffer_id)?; - editor.semantics_provider.as_ref()?.resolve_inlay_hint( - hint_to_resolve, - buffer, - server_id, - cx, - ) - })?; - if let Some(resolved_hint_task) = resolved_hint_task { - let mut resolved_hint = - resolved_hint_task.await.context("hint resolve task")?; - editor.read_with(cx, |editor, _| { - if let Some(excerpt_hints) = - editor.inlay_hint_cache.hints.get(&excerpt_id) + if let Some(cached_hint) = guard.hints_by_id.get_mut(&id) + && let ResolveState::CanResolve(server_id, _) = &cached_hint.resolve_state + { + let hint_to_resolve = cached_hint.clone(); + let server_id = *server_id; + cached_hint.resolve_state = ResolveState::Resolving; + drop(guard); + cx.spawn_in(window, async move |editor, cx| { + let resolved_hint_task = editor.update(cx, |editor, cx| { + let buffer = editor.buffer().read(cx).buffer(buffer_id)?; + editor.semantics_provider.as_ref()?.resolve_inlay_hint( + hint_to_resolve, + buffer, + server_id, + cx, + ) + })?; + if let Some(resolved_hint_task) = resolved_hint_task { + let mut resolved_hint = + resolved_hint_task.await.context("hint resolve task")?; + editor.read_with(cx, |editor, _| { + 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) + && cached_hint.resolve_state == ResolveState::Resolving { - 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; - } - } + resolved_hint.resolve_state = ResolveState::Resolved; + *cached_hint = resolved_hint; } - })?; - } + } + })?; + } - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - } + anyhow::Ok(()) + }) + .detach_and_log_err(cx); } } } @@ -956,7 +951,7 @@ fn fetch_and_update_hints( .update(cx, |editor, cx| { if got_throttled { let query_not_around_visible_range = match editor - .excerpts_for_inlay_hints_query(None, cx) + .visible_excerpts(None, cx) .remove(&query.excerpt_id) { Some((_, _, current_visible_range)) => { @@ -990,8 +985,8 @@ fn fetch_and_update_hints( let buffer = editor.buffer().read(cx).buffer(query.buffer_id)?; - if !editor.registered_buffers.contains_key(&query.buffer_id) { - if let Some(project) = editor.project.as_ref() { + if !editor.registered_buffers.contains_key(&query.buffer_id) + && let Some(project) = editor.project.as_ref() { project.update(cx, |project, cx| { editor.registered_buffers.insert( query.buffer_id, @@ -999,7 +994,6 @@ fn fetch_and_update_hints( ); }) } - } editor .semantics_provider @@ -1240,14 +1234,12 @@ fn apply_hint_update( .inlay_hint_cache .allowed_hint_kinds .contains(&new_hint.kind) - { - if let Some(new_hint_position) = + && let Some(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)); - } + { + 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); @@ -1302,6 +1294,7 @@ fn apply_hint_update( #[cfg(test)] pub mod tests { + use crate::SelectionEffects; use crate::editor_tests::update_test_language_settings; use crate::scroll::ScrollAmount; use crate::{ExcerptRange, scroll::Autoscroll, test::editor_lsp_test_context::rust_lang}; @@ -1384,7 +1377,9 @@ pub mod tests { editor .update(cx, |editor, window, cx| { - editor.change_selections(None, window, cx, |s| s.select_ranges([13..13])); + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges([13..13]) + }); editor.handle_input("some change", window, cx); }) .unwrap(); @@ -1698,7 +1693,9 @@ pub mod tests { rs_editor .update(cx, |editor, window, cx| { - editor.change_selections(None, window, cx, |s| s.select_ranges([13..13])); + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges([13..13]) + }); editor.handle_input("some rs change", window, cx); }) .unwrap(); @@ -1733,7 +1730,9 @@ pub mod tests { md_editor .update(cx, |editor, window, cx| { - editor.change_selections(None, window, cx, |s| s.select_ranges([13..13])); + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges([13..13]) + }); editor.handle_input("some md change", window, cx); }) .unwrap(); @@ -2155,7 +2154,9 @@ pub mod tests { ] { editor .update(cx, |editor, window, cx| { - editor.change_selections(None, window, cx, |s| s.select_ranges([13..13])); + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges([13..13]) + }); editor.handle_input(change_after_opening, window, cx); }) .unwrap(); @@ -2199,7 +2200,9 @@ pub mod tests { edits.push(cx.spawn(|mut cx| async move { task_editor .update(&mut cx, |editor, window, cx| { - editor.change_selections(None, window, cx, |s| s.select_ranges([13..13])); + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges([13..13]) + }); editor.handle_input(async_later_change, window, cx); }) .unwrap(); @@ -2447,9 +2450,12 @@ pub mod tests { editor .update(cx, |editor, window, cx| { - editor.change_selections(Some(Autoscroll::center()), window, cx, |s| { - s.select_ranges([selection_in_cached_range..selection_in_cached_range]) - }); + editor.change_selections( + SelectionEffects::scroll(Autoscroll::center()), + window, + cx, + |s| s.select_ranges([selection_in_cached_range..selection_in_cached_range]), + ); }) .unwrap(); cx.executor().advance_clock(Duration::from_millis( @@ -2511,9 +2517,7 @@ pub mod tests { cx: &mut gpui::TestAppContext, ) -> Range { let ranges = editor - .update(cx, |editor, _window, cx| { - editor.excerpts_for_inlay_hints_query(None, cx) - }) + .update(cx, |editor, _window, cx| editor.visible_excerpts(None, cx)) .unwrap(); assert_eq!( ranges.len(), @@ -2712,15 +2716,24 @@ pub mod tests { editor .update(cx, |editor, window, cx| { - editor.change_selections(Some(Autoscroll::Next), window, cx, |s| { - s.select_ranges([Point::new(4, 0)..Point::new(4, 0)]) - }); - editor.change_selections(Some(Autoscroll::Next), window, cx, |s| { - s.select_ranges([Point::new(22, 0)..Point::new(22, 0)]) - }); - editor.change_selections(Some(Autoscroll::Next), window, cx, |s| { - s.select_ranges([Point::new(50, 0)..Point::new(50, 0)]) - }); + editor.change_selections( + SelectionEffects::scroll(Autoscroll::Next), + window, + cx, + |s| s.select_ranges([Point::new(4, 0)..Point::new(4, 0)]), + ); + editor.change_selections( + SelectionEffects::scroll(Autoscroll::Next), + window, + cx, + |s| s.select_ranges([Point::new(22, 0)..Point::new(22, 0)]), + ); + editor.change_selections( + SelectionEffects::scroll(Autoscroll::Next), + window, + cx, + |s| s.select_ranges([Point::new(50, 0)..Point::new(50, 0)]), + ); }) .unwrap(); cx.executor().run_until_parked(); @@ -2745,9 +2758,12 @@ pub mod tests { editor .update(cx, |editor, window, cx| { - editor.change_selections(Some(Autoscroll::Next), window, cx, |s| { - s.select_ranges([Point::new(100, 0)..Point::new(100, 0)]) - }); + editor.change_selections( + SelectionEffects::scroll(Autoscroll::Next), + window, + cx, + |s| s.select_ranges([Point::new(100, 0)..Point::new(100, 0)]), + ); }) .unwrap(); cx.executor().advance_clock(Duration::from_millis( @@ -2778,9 +2794,12 @@ pub mod tests { editor .update(cx, |editor, window, cx| { - editor.change_selections(Some(Autoscroll::Next), window, cx, |s| { - s.select_ranges([Point::new(4, 0)..Point::new(4, 0)]) - }); + editor.change_selections( + SelectionEffects::scroll(Autoscroll::Next), + window, + cx, + |s| s.select_ranges([Point::new(4, 0)..Point::new(4, 0)]), + ); }) .unwrap(); cx.executor().advance_clock(Duration::from_millis( @@ -2812,7 +2831,7 @@ pub mod tests { editor_edited.store(true, Ordering::Release); editor .update(cx, |editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([Point::new(57, 0)..Point::new(57, 0)]) }); editor.handle_input("++++more text++++", window, cx); @@ -3130,7 +3149,7 @@ pub mod tests { cx.executor().run_until_parked(); editor .update(cx, |editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([Point::new(10, 0)..Point::new(10, 0)]) }) }) @@ -3412,7 +3431,7 @@ pub mod tests { cx.executor().run_until_parked(); editor .update(cx, |editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([Point::new(10, 0)..Point::new(10, 0)]) }) }) @@ -3519,7 +3538,7 @@ pub mod tests { let excerpt_hints = excerpt_hints.read(); for id in &excerpt_hints.ordered_hints { let hint = &excerpt_hints.hints_by_id[id]; - let mut label = hint.text(); + let mut label = hint.text().to_string(); if hint.padding_left { label.insert(0, ' '); } diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index d822215949..641e8a97ed 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1,6 +1,8 @@ use crate::{ Anchor, Autoscroll, Editor, EditorEvent, EditorSettings, ExcerptId, ExcerptRange, FormatTarget, - MultiBuffer, MultiBufferSnapshot, NavigationData, SearchWithinRange, ToPoint as _, + MultiBuffer, MultiBufferSnapshot, NavigationData, ReportEditorEvent, SearchWithinRange, + SelectionEffects, ToPoint as _, + display_map::HighlightKey, editor_settings::SeedQuerySetting, persistence::{DB, SerializedEditor}, scroll::ScrollAnchor, @@ -40,7 +42,8 @@ use ui::{IconDecorationKind, prelude::*}; use util::{ResultExt, TryFutureExt, paths::PathExt}; use workspace::{ CollaboratorId, ItemId, ItemNavHistory, ToolbarItemLocation, ViewId, Workspace, WorkspaceId, - item::{FollowableItem, Item, ItemEvent, ProjectItem}, + invalid_buffer_view::InvalidBufferView, + item::{FollowableItem, Item, ItemEvent, ProjectItem, SaveOptions}, searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle}, }; use workspace::{ @@ -101,9 +104,9 @@ impl FollowableItem for Editor { multibuffer = MultiBuffer::new(project.read(cx).capability()); let mut sorted_excerpts = state.excerpts.clone(); sorted_excerpts.sort_by_key(|e| e.id); - let mut sorted_excerpts = sorted_excerpts.into_iter().peekable(); + let sorted_excerpts = sorted_excerpts.into_iter().peekable(); - while let Some(excerpt) = sorted_excerpts.next() { + for excerpt in sorted_excerpts { let Ok(buffer_id) = BufferId::new(excerpt.buffer_id) else { continue; }; @@ -199,7 +202,7 @@ impl FollowableItem for Editor { if buffer .as_singleton() .and_then(|buffer| buffer.read(cx).file()) - .map_or(false, |file| file.is_private()) + .is_some_and(|file| file.is_private()) { return None; } @@ -291,7 +294,7 @@ impl FollowableItem for Editor { EditorEvent::ExcerptsRemoved { ids, .. } => { update .deleted_excerpts - .extend(ids.iter().map(ExcerptId::to_proto)); + .extend(ids.iter().copied().map(ExcerptId::to_proto)); true } EditorEvent::ScrollPositionChanged { autoscroll, .. } if !autoscroll => { @@ -522,8 +525,8 @@ fn serialize_selection( ) -> proto::Selection { proto::Selection { id: selection.id as u64, - start: Some(serialize_anchor(&selection.start, &buffer)), - end: Some(serialize_anchor(&selection.end, &buffer)), + start: Some(serialize_anchor(&selection.start, buffer)), + end: Some(serialize_anchor(&selection.end, buffer)), reversed: selection.reversed, } } @@ -611,12 +614,13 @@ impl Item for Editor { if newest_selection.head() == offset { false } else { - let nav_history = self.nav_history.take(); self.set_scroll_anchor(scroll_anchor, window, cx); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select_ranges([offset..offset]) - }); - self.nav_history = nav_history; + self.change_selections( + SelectionEffects::default().nav_history(false), + window, + cx, + |s| s.select_ranges([offset..offset]), + ); true } } else { @@ -651,6 +655,10 @@ impl Item for Editor { } } + fn suggested_filename(&self, cx: &App) -> SharedString { + self.buffer.read(cx).title(cx).to_string().into() + } + fn tab_icon(&self, _: &Window, cx: &App) -> Option { ItemSettings::get_global(cx) .file_icons @@ -671,7 +679,7 @@ impl Item for Editor { let buffer = buffer.read(cx); let path = buffer.project_path(cx)?; let buffer_id = buffer.remote_id(); - let project = self.project.as_ref()?.read(cx); + let project = self.project()?.read(cx); let entry = project.entry_for_path(&path, cx)?; let (repo, repo_path) = project .git_store() @@ -708,7 +716,7 @@ impl Item for Editor { .read(cx) .as_singleton() .and_then(|buffer| buffer.read(cx).file()) - .map_or(false, |file| file.disk_state() == DiskState::Deleted); + .is_some_and(|file| file.disk_state() == DiskState::Deleted); h_flex() .gap_2() @@ -773,9 +781,13 @@ impl Item for Editor { } } + fn on_removed(&self, cx: &App) { + self.report_editor_event(ReportEditorEvent::Closed, None, cx); + } + fn deactivated(&mut self, _: &mut Window, cx: &mut Context) { let selection = self.selections.newest_anchor(); - self.push_to_nav_history(selection.head(), None, true, cx); + self.push_to_nav_history(selection.head(), None, true, false, cx); } fn workspace_deactivated(&mut self, _: &mut Window, cx: &mut Context) { @@ -805,24 +817,42 @@ impl Item for Editor { fn save( &mut self, - format: bool, + options: SaveOptions, project: Entity, window: &mut Window, cx: &mut Context, ) -> Task> { - self.report_editor_event("Editor Saved", None, cx); + // Add meta data tracking # of auto saves + if options.autosave { + self.report_editor_event(ReportEditorEvent::Saved { auto_saved: true }, None, cx); + } else { + self.report_editor_event(ReportEditorEvent::Saved { auto_saved: false }, None, cx); + } + let buffers = self.buffer().clone().read(cx).all_buffers(); let buffers = buffers .into_iter() .map(|handle| handle.read(cx).base_buffer().unwrap_or(handle.clone())) .collect::>(); + + // let mut buffers_to_save = + let buffers_to_save = if self.buffer.read(cx).is_singleton() && !options.autosave { + buffers.clone() + } else { + buffers + .iter() + .filter(|buffer| buffer.read(cx).is_dirty()) + .cloned() + .collect() + }; + cx.spawn_in(window, async move |this, cx| { - if format { + if options.format { this.update_in(cx, |editor, window, cx| { editor.perform_format( project.clone(), FormatTrigger::Save, - FormatTarget::Buffers, + FormatTarget::Buffers(buffers_to_save.clone()), window, cx, ) @@ -830,33 +860,28 @@ impl Item for Editor { .await?; } - if buffers.len() == 1 { - // Apply full save routine for singleton buffers, to allow to `touch` the file via the editor. + if !buffers_to_save.is_empty() { project - .update(cx, |project, cx| project.save_buffers(buffers, cx))? + .update(cx, |project, cx| { + project.save_buffers(buffers_to_save.clone(), cx) + })? .await?; - } else { - // For multi-buffers, only format and save the buffers with changes. - // For clean buffers, we simulate saving by calling `Buffer::did_save`, - // so that language servers or other downstream listeners of save events get notified. - let (dirty_buffers, clean_buffers) = buffers.into_iter().partition(|buffer| { - buffer - .read_with(cx, |buffer, _| buffer.is_dirty() || buffer.has_conflict()) - .unwrap_or(false) - }); + } - project - .update(cx, |project, cx| project.save_buffers(dirty_buffers, cx))? - .await?; - for buffer in clean_buffers { - buffer - .update(cx, |buffer, cx| { - let version = buffer.saved_version().clone(); - let mtime = buffer.saved_mtime(); - buffer.did_save(version, mtime, cx); - }) - .ok(); - } + // Notify about clean buffers for language server events + let buffers_that_were_not_saved: Vec<_> = buffers + .into_iter() + .filter(|b| !buffers_to_save.contains(b)) + .collect(); + + for buffer in buffers_that_were_not_saved { + buffer + .update(cx, |buffer, cx| { + let version = buffer.saved_version().clone(); + let mtime = buffer.saved_mtime(); + buffer.did_save(version, mtime, cx); + }) + .ok(); } Ok(()) @@ -880,7 +905,11 @@ impl Item for Editor { .path .extension() .map(|a| a.to_string_lossy().to_string()); - self.report_editor_event("Editor Saved", file_extension, cx); + self.report_editor_event( + ReportEditorEvent::Saved { auto_saved: false }, + file_extension, + cx, + ); project.update(cx, |project, cx| project.save_buffer_as(buffer, path, cx)) } @@ -902,10 +931,10 @@ impl Item for Editor { })?; buffer .update(cx, |buffer, cx| { - if let Some(transaction) = transaction { - if !buffer.is_singleton() { - buffer.push_transaction(&transaction.0, cx); - } + if let Some(transaction) = transaction + && !buffer.is_singleton() + { + buffer.push_transaction(&transaction.0, cx); } }) .ok(); @@ -981,8 +1010,8 @@ impl Item for Editor { ) { self.workspace = Some((workspace.weak_handle(), workspace.database_id())); if let Some(workspace) = &workspace.weak_handle().upgrade() { - cx.subscribe(&workspace, |editor, _, event: &workspace::Event, _cx| { - if matches!(event, workspace::Event::ModalOpened) { + cx.subscribe(workspace, |editor, _, event: &workspace::Event, _cx| { + if let workspace::Event::ModalOpened = event { editor.mouse_context_menu.take(); editor.inline_blame_popover.take(); } @@ -1008,6 +1037,10 @@ impl Item for Editor { f(ItemEvent::UpdateBreadcrumbs); } + EditorEvent::BreadcrumbsChanged => { + f(ItemEvent::UpdateBreadcrumbs); + } + EditorEvent::DirtyChanged => { f(ItemEvent::UpdateTab); } @@ -1210,7 +1243,20 @@ impl SerializableItem for Editor { abs_path: None, contents: None, .. - } => Task::ready(Err(anyhow!("No path or contents found for buffer"))), + } => window.spawn(cx, async move |cx| { + let buffer = project + .update(cx, |project, cx| project.create_buffer(cx))? + .await?; + + cx.update(|window, cx| { + cx.new(|cx| { + let mut editor = Editor::for_buffer(buffer, Some(project), window, cx); + + editor.read_metadata_from_db(item_id, workspace_id, window, cx); + editor + }) + }) + }), } } @@ -1247,7 +1293,7 @@ impl SerializableItem for Editor { project .read(cx) .worktree_for_id(worktree_id, cx) - .and_then(|worktree| worktree.read(cx).absolutize(&file.path()).ok()) + .and_then(|worktree| worktree.read(cx).absolutize(file.path()).ok()) .or_else(|| { let full_path = file.full_path(cx); let project_path = project.read(cx).find_project_path(&full_path, cx)?; @@ -1325,40 +1371,47 @@ impl ProjectItem for Editor { let mut editor = Self::for_buffer(buffer.clone(), Some(project), window, cx); if let Some((excerpt_id, buffer_id, snapshot)) = editor.buffer().read(cx).snapshot(cx).as_singleton() + && WorkspaceSettings::get(None, cx).restore_on_file_reopen + && let Some(restoration_data) = Self::project_item_kind() + .and_then(|kind| pane.as_ref()?.project_item_restoration_data.get(&kind)) + .and_then(|data| data.downcast_ref::()) + .and_then(|data| { + let file = project::File::from_dyn(buffer.read(cx).file())?; + data.entries.get(&file.abs_path(cx)) + }) { - if WorkspaceSettings::get(None, cx).restore_on_file_reopen { - if let Some(restoration_data) = Self::project_item_kind() - .and_then(|kind| pane.as_ref()?.project_item_restoration_data.get(&kind)) - .and_then(|data| data.downcast_ref::()) - .and_then(|data| { - let file = project::File::from_dyn(buffer.read(cx).file())?; - data.entries.get(&file.abs_path(cx)) - }) - { - editor.fold_ranges( - clip_ranges(&restoration_data.folds, &snapshot), - false, - window, - cx, - ); - if !restoration_data.selections.is_empty() { - editor.change_selections(None, window, cx, |s| { - s.select_ranges(clip_ranges(&restoration_data.selections, &snapshot)); - }); - } - let (top_row, offset) = restoration_data.scroll_position; - let anchor = Anchor::in_buffer( - *excerpt_id, - buffer_id, - snapshot.anchor_before(Point::new(top_row, 0)), - ); - editor.set_scroll_anchor(ScrollAnchor { anchor, offset }, window, cx); - } + editor.fold_ranges( + clip_ranges(&restoration_data.folds, snapshot), + false, + window, + cx, + ); + if !restoration_data.selections.is_empty() { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges(clip_ranges(&restoration_data.selections, snapshot)); + }); } + let (top_row, offset) = restoration_data.scroll_position; + let anchor = Anchor::in_buffer( + *excerpt_id, + buffer_id, + snapshot.anchor_before(Point::new(top_row, 0)), + ); + editor.set_scroll_anchor(ScrollAnchor { anchor, offset }, window, cx); } editor } + + fn for_broken_project_item( + abs_path: PathBuf, + is_local: bool, + e: &anyhow::Error, + window: &mut Window, + cx: &mut App, + ) -> Option { + Some(InvalidBufferView::new(abs_path, is_local, e, window, cx)) + } } fn clip_ranges<'a>( @@ -1422,7 +1475,7 @@ impl SearchableItem for Editor { fn get_matches(&self, _window: &mut Window, _: &mut App) -> Vec> { self.background_highlights - .get(&TypeId::of::()) + .get(&HighlightKey::Type(TypeId::of::())) .map_or(Vec::new(), |(_color, ranges)| { ranges.iter().cloned().collect() }) @@ -1445,12 +1498,12 @@ impl SearchableItem for Editor { ) { let existing_range = self .background_highlights - .get(&TypeId::of::()) + .get(&HighlightKey::Type(TypeId::of::())) .map(|(_, range)| range.as_ref()); let updated = existing_range != Some(matches); self.highlight_background::( matches, - |theme| theme.search_match_background, + |theme| theme.colors().search_match_background, cx, ); if updated { @@ -1511,7 +1564,7 @@ impl SearchableItem for Editor { fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context) -> String { let setting = EditorSettings::get_global(cx).seed_search_query_from_cursor; let snapshot = &self.snapshot(window, cx).buffer_snapshot; - let selection = self.selections.newest::(cx); + let selection = self.selections.newest_adjusted(cx); match setting { SeedQuerySetting::Never => String::new(), @@ -1548,7 +1601,7 @@ impl SearchableItem for Editor { ) { self.unfold_ranges(&[matches[index].clone()], false, true, cx); let range = self.range_for_match(&matches[index]); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.change_selections(Default::default(), window, cx, |s| { s.select_ranges([range]); }) } @@ -1560,7 +1613,7 @@ impl SearchableItem for Editor { cx: &mut Context, ) { self.unfold_ranges(matches, false, false, cx); - self.change_selections(None, window, cx, |s| { + self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges(matches.iter().cloned()) }); } @@ -1597,24 +1650,10 @@ impl SearchableItem for Editor { let text = self.buffer.read(cx); let text = text.snapshot(cx); let mut edits = vec![]; - let mut last_point: Option = None; for m in matches { - let point = m.start.to_point(&text); let text = text.text_for_range(m.clone()).collect::>(); - // Check if the row for the current match is different from the last - // match. If that's not the case and we're still replacing matches - // in the same row/line, skip this match if the `one_match_per_line` - // option is enabled. - if last_point.is_none() { - last_point = Some(point); - } else if last_point.is_some() && point.row != last_point.unwrap().row { - last_point = Some(point); - } else if query.one_match_per_line().is_some_and(|enabled| enabled) { - continue; - } - let text: Cow<_> = if text.len() == 1 { text.first().cloned().unwrap().into() } else { @@ -1692,7 +1731,7 @@ impl SearchableItem for Editor { let buffer = self.buffer().read(cx).snapshot(cx); let search_within_ranges = self .background_highlights - .get(&TypeId::of::()) + .get(&HighlightKey::Type(TypeId::of::())) .map_or(vec![], |(_color, ranges)| { ranges.iter().cloned().collect::>() }); @@ -1810,7 +1849,7 @@ pub fn entry_diagnostic_aware_icon_name_and_color( diagnostic_severity: Option, ) -> Option<(IconName, Color)> { match diagnostic_severity { - Some(DiagnosticSeverity::ERROR) => Some((IconName::X, Color::Error)), + Some(DiagnosticSeverity::ERROR) => Some((IconName::Close, Color::Error)), Some(DiagnosticSeverity::WARNING) => Some((IconName::Triangle, Color::Warning)), _ => None, } @@ -2096,5 +2135,38 @@ mod tests { assert!(editor.has_conflict(cx)); // The editor should have a conflict }); } + + // Test case 5: Deserialize with no path, no content, no language, and no old mtime (new, empty, unsaved buffer) + { + let project = Project::test(fs.clone(), [path!("/file.rs").as_ref()], cx).await; + let (workspace, cx) = + cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + + let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap(); + + let item_id = 10000 as ItemId; + let serialized_editor = SerializedEditor { + abs_path: None, + contents: None, + language: None, + mtime: None, + }; + + DB.save_serialized_editor(item_id, workspace_id, serialized_editor) + .await + .unwrap(); + + let deserialized = + deserialize_editor(item_id, workspace_id, workspace, project, cx).await; + + deserialized.update(cx, |editor, cx| { + assert_eq!(editor.text(cx), ""); + assert!(!editor.is_dirty(cx)); + assert!(!editor.has_conflict(cx)); + + let buffer = editor.buffer().read(cx).as_singleton().unwrap().read(cx); + assert!(buffer.file().is_none()); + }); + } } } diff --git a/crates/editor/src/jsx_tag_auto_close.rs b/crates/editor/src/jsx_tag_auto_close.rs index df50ab9b2f..e6c518beae 100644 --- a/crates/editor/src/jsx_tag_auto_close.rs +++ b/crates/editor/src/jsx_tag_auto_close.rs @@ -8,7 +8,7 @@ use util::ResultExt as _; use language::{BufferSnapshot, JsxTagAutoCloseConfig, Node}; use text::{Anchor, OffsetRangeExt as _}; -use crate::Editor; +use crate::{Editor, SelectionEffects}; pub struct JsxTagCompletionState { edit_index: usize, @@ -37,7 +37,7 @@ pub(crate) fn should_auto_close( let text = buffer .text_for_range(edited_range.clone()) .collect::(); - let edited_range = edited_range.to_offset(&buffer); + let edited_range = edited_range.to_offset(buffer); if !text.ends_with(">") { continue; } @@ -51,12 +51,11 @@ pub(crate) fn should_auto_close( continue; }; let mut jsx_open_tag_node = node; - if node.grammar_name() != config.open_tag_node_name { - if let Some(parent) = node.parent() { - if parent.grammar_name() == config.open_tag_node_name { - jsx_open_tag_node = parent; - } - } + if node.grammar_name() != config.open_tag_node_name + && let Some(parent) = node.parent() + && parent.grammar_name() == config.open_tag_node_name + { + jsx_open_tag_node = parent; } if jsx_open_tag_node.grammar_name() != config.open_tag_node_name { continue; @@ -87,9 +86,9 @@ pub(crate) fn should_auto_close( }); } if to_auto_edit.is_empty() { - return None; + None } else { - return Some(to_auto_edit); + Some(to_auto_edit) } } @@ -182,12 +181,12 @@ pub(crate) fn generate_auto_close_edits( */ { let tag_node_name_equals = |node: &Node, name: &str| { - let is_empty = name.len() == 0; + let is_empty = name.is_empty(); if let Some(node_name) = node.named_child(TS_NODE_TAG_NAME_CHILD_INDEX) { let range = node_name.byte_range(); return buffer.text_for_range(range).equals_str(name); } - return is_empty; + is_empty }; let tree_root_node = { @@ -208,7 +207,7 @@ pub(crate) fn generate_auto_close_edits( cur = descendant; } - assert!(ancestors.len() > 0); + assert!(!ancestors.is_empty()); let mut tree_root_node = open_tag; @@ -228,7 +227,7 @@ pub(crate) fn generate_auto_close_edits( let has_open_tag_with_same_tag_name = ancestor .named_child(0) .filter(|n| n.kind() == config.open_tag_node_name) - .map_or(false, |element_open_tag_node| { + .is_some_and(|element_open_tag_node| { tag_node_name_equals(&element_open_tag_node, &tag_name) }); if has_open_tag_with_same_tag_name { @@ -264,8 +263,7 @@ pub(crate) fn generate_auto_close_edits( } let is_after_open_tag = |node: &Node| { - return node.start_byte() < open_tag.start_byte() - && node.end_byte() < open_tag.start_byte(); + node.start_byte() < open_tag.start_byte() && node.end_byte() < open_tag.start_byte() }; // perf: use cursor for more efficient traversal @@ -284,10 +282,8 @@ pub(crate) fn generate_auto_close_edits( unclosed_open_tag_count -= 1; } } else if has_erroneous_close_tag && kind == erroneous_close_tag_node_name { - if tag_node_name_equals(&node, &tag_name) { - if !is_after_open_tag(&node) { - unclosed_open_tag_count -= 1; - } + if tag_node_name_equals(&node, &tag_name) && !is_after_open_tag(&node) { + unclosed_open_tag_count -= 1; } } else if kind == config.jsx_element_node_name { // perf: filter only open,close,element,erroneous nodes @@ -304,7 +300,7 @@ pub(crate) fn generate_auto_close_edits( let edit_range = edit_anchor..edit_anchor; edits.push((edit_range, format!("", tag_name))); } - return Ok(edits); + Ok(edits) } pub(crate) fn refresh_enabled_in_any_buffer( @@ -370,7 +366,7 @@ pub(crate) fn construct_initial_buffer_versions_map< initial_buffer_versions.insert(buffer_id, buffer_version); } } - return initial_buffer_versions; + initial_buffer_versions } pub(crate) fn handle_from( @@ -458,12 +454,9 @@ pub(crate) fn handle_from( let ensure_no_edits_since_start = || -> Option<()> { let has_edits_since_start = this .read_with(cx, |this, cx| { - this.buffer - .read(cx) - .buffer(buffer_id) - .map_or(true, |buffer| { - buffer.read(cx).has_edits_since(&buffer_version_initial) - }) + this.buffer.read(cx).buffer(buffer_id).is_none_or(|buffer| { + buffer.read(cx).has_edits_since(&buffer_version_initial) + }) }) .ok()?; @@ -514,7 +507,7 @@ pub(crate) fn handle_from( { let selections = this - .read_with(cx, |this, _| this.selections.disjoint_anchors().clone()) + .read_with(cx, |this, _| this.selections.disjoint_anchors()) .ok()?; for selection in selections.iter() { let Some(selection_buffer_offset_head) = @@ -600,9 +593,14 @@ pub(crate) fn handle_from( }) .collect::>(); this.update_in(cx, |this, window, cx| { - this.change_selections_without_showing_completions(None, window, cx, |s| { - s.select(base_selections); - }); + this.change_selections( + SelectionEffects::no_scroll().completions(false), + window, + cx, + |s| { + s.select(base_selections); + }, + ); }) .ok()?; } @@ -810,10 +808,7 @@ mod jsx_tag_autoclose_tests { ); buf }); - let buffer_c = cx.new(|cx| { - let buf = language::Buffer::local(", + render_mode: DocumentColorsRenderMode, +} + +#[derive(Debug, Default)] +struct BufferColors { + colors: Vec<(Range, DocumentColor, InlayId)>, + inlay_colors: HashMap, + cache_version_used: usize, +} + +impl LspColorData { + pub fn new(cx: &App) -> Self { + Self { + buffer_colors: HashMap::default(), + render_mode: EditorSettings::get_global(cx).lsp_document_colors, + } + } + + pub fn render_mode_updated( + &mut self, + new_render_mode: DocumentColorsRenderMode, + ) -> Option { + if self.render_mode == new_render_mode { + return None; + } + self.render_mode = new_render_mode; + match new_render_mode { + DocumentColorsRenderMode::Inlay => Some(InlaySplice { + to_remove: Vec::new(), + to_insert: self + .buffer_colors + .iter() + .flat_map(|(_, buffer_colors)| buffer_colors.colors.iter()) + .map(|(range, color, id)| { + Inlay::color( + id.id(), + range.start, + Rgba { + r: color.color.red, + g: color.color.green, + b: color.color.blue, + a: color.color.alpha, + }, + ) + }) + .collect(), + }), + DocumentColorsRenderMode::None => Some(InlaySplice { + to_remove: self + .buffer_colors + .drain() + .flat_map(|(_, buffer_colors)| buffer_colors.inlay_colors) + .map(|(id, _)| id) + .collect(), + to_insert: Vec::new(), + }), + DocumentColorsRenderMode::Border | DocumentColorsRenderMode::Background => { + Some(InlaySplice { + to_remove: self + .buffer_colors + .iter_mut() + .flat_map(|(_, buffer_colors)| buffer_colors.inlay_colors.drain()) + .map(|(id, _)| id) + .collect(), + to_insert: Vec::new(), + }) + } + } + } + + fn set_colors( + &mut self, + buffer_id: BufferId, + colors: Vec<(Range, DocumentColor, InlayId)>, + cache_version: Option, + ) -> bool { + let buffer_colors = self.buffer_colors.entry(buffer_id).or_default(); + if let Some(cache_version) = cache_version { + buffer_colors.cache_version_used = cache_version; + } + if buffer_colors.colors == colors { + return false; + } + + buffer_colors.inlay_colors = colors + .iter() + .enumerate() + .map(|(i, (_, _, id))| (*id, i)) + .collect(); + buffer_colors.colors = colors; + true + } + + pub fn editor_display_highlights( + &self, + snapshot: &EditorSnapshot, + ) -> (DocumentColorsRenderMode, Vec<(Range, Hsla)>) { + let render_mode = self.render_mode; + let highlights = if render_mode == DocumentColorsRenderMode::None + || render_mode == DocumentColorsRenderMode::Inlay + { + Vec::new() + } else { + self.buffer_colors + .iter() + .flat_map(|(_, buffer_colors)| &buffer_colors.colors) + .map(|(range, color, _)| { + let display_range = range.clone().to_display_points(snapshot); + let color = Hsla::from(Rgba { + r: color.color.red, + g: color.color.green, + b: color.color.blue, + a: color.color.alpha, + }); + (display_range, color) + }) + .collect() + }; + (render_mode, highlights) + } +} + +impl Editor { + pub(super) fn refresh_colors( + &mut self, + ignore_cache: bool, + buffer_id: Option, + _: &Window, + cx: &mut Context, + ) { + if !self.mode().is_full() { + return; + } + let Some(project) = self.project.clone() else { + return; + }; + if self + .colors + .as_ref() + .is_none_or(|colors| colors.render_mode == DocumentColorsRenderMode::None) + { + return; + } + + let visible_buffers = self + .visible_excerpts(None, cx) + .into_values() + .map(|(buffer, ..)| buffer) + .filter(|editor_buffer| { + buffer_id.is_none_or(|buffer_id| buffer_id == editor_buffer.read(cx).remote_id()) + }) + .unique_by(|buffer| buffer.read(cx).remote_id()) + .collect::>(); + + let all_colors_task = project.read(cx).lsp_store().update(cx, |lsp_store, cx| { + visible_buffers + .into_iter() + .filter_map(|buffer| { + let buffer_id = buffer.read(cx).remote_id(); + let fetch_strategy = if ignore_cache { + LspFetchStrategy::IgnoreCache + } else { + LspFetchStrategy::UseCache { + known_cache_version: self.colors.as_ref().and_then(|colors| { + Some(colors.buffer_colors.get(&buffer_id)?.cache_version_used) + }), + } + }; + let colors_task = lsp_store.document_colors(fetch_strategy, buffer, cx)?; + Some(async move { (buffer_id, colors_task.await) }) + }) + .collect::>() + }); + cx.spawn(async move |editor, cx| { + let all_colors = join_all(all_colors_task).await; + if all_colors.is_empty() { + return; + } + let Ok((multi_buffer_snapshot, editor_excerpts)) = editor.update(cx, |editor, cx| { + let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx); + let editor_excerpts = multi_buffer_snapshot.excerpts().fold( + HashMap::default(), + |mut acc, (excerpt_id, buffer_snapshot, excerpt_range)| { + let excerpt_data = acc + .entry(buffer_snapshot.remote_id()) + .or_insert_with(Vec::new); + let excerpt_point_range = + excerpt_range.context.to_point_utf16(buffer_snapshot); + excerpt_data.push(( + excerpt_id, + buffer_snapshot.clone(), + excerpt_point_range, + )); + acc + }, + ); + (multi_buffer_snapshot, editor_excerpts) + }) else { + return; + }; + + let mut new_editor_colors = HashMap::default(); + for (buffer_id, colors) in all_colors { + let Some(excerpts) = editor_excerpts.get(&buffer_id) else { + continue; + }; + match colors { + Ok(colors) => { + for color in colors.colors { + let color_start = point_from_lsp(color.lsp_range.start); + let color_end = point_from_lsp(color.lsp_range.end); + + for (excerpt_id, buffer_snapshot, excerpt_range) in excerpts { + if !excerpt_range.contains(&color_start.0) + || !excerpt_range.contains(&color_end.0) + { + continue; + } + let Some(color_start_anchor) = multi_buffer_snapshot + .anchor_in_excerpt( + *excerpt_id, + buffer_snapshot.anchor_before( + buffer_snapshot + .clip_point_utf16(color_start, Bias::Left), + ), + ) + else { + continue; + }; + let Some(color_end_anchor) = multi_buffer_snapshot + .anchor_in_excerpt( + *excerpt_id, + buffer_snapshot.anchor_after( + buffer_snapshot + .clip_point_utf16(color_end, Bias::Right), + ), + ) + else { + continue; + }; + + let new_entry = + new_editor_colors.entry(buffer_id).or_insert_with(|| { + (Vec::<(Range, DocumentColor)>::new(), None) + }); + new_entry.1 = colors.cache_version; + let new_buffer_colors = &mut new_entry.0; + + let (Ok(i) | Err(i)) = + new_buffer_colors.binary_search_by(|(probe, _)| { + probe + .start + .cmp(&color_start_anchor, &multi_buffer_snapshot) + .then_with(|| { + probe + .end + .cmp(&color_end_anchor, &multi_buffer_snapshot) + }) + }); + new_buffer_colors + .insert(i, (color_start_anchor..color_end_anchor, color)); + break; + } + } + } + Err(e) => log::error!("Failed to retrieve document colors: {e}"), + } + } + + editor + .update(cx, |editor, cx| { + let mut colors_splice = InlaySplice::default(); + let Some(colors) = &mut editor.colors else { + return; + }; + let mut updated = false; + for (buffer_id, (new_buffer_colors, new_cache_version)) in new_editor_colors { + let mut new_buffer_color_inlays = + Vec::with_capacity(new_buffer_colors.len()); + let mut existing_buffer_colors = colors + .buffer_colors + .entry(buffer_id) + .or_default() + .colors + .iter() + .peekable(); + for (new_range, new_color) in new_buffer_colors { + let rgba_color = Rgba { + r: new_color.color.red, + g: new_color.color.green, + b: new_color.color.blue, + a: new_color.color.alpha, + }; + + loop { + match existing_buffer_colors.peek() { + Some((existing_range, existing_color, existing_inlay_id)) => { + match existing_range + .start + .cmp(&new_range.start, &multi_buffer_snapshot) + .then_with(|| { + existing_range + .end + .cmp(&new_range.end, &multi_buffer_snapshot) + }) { + cmp::Ordering::Less => { + colors_splice.to_remove.push(*existing_inlay_id); + existing_buffer_colors.next(); + continue; + } + cmp::Ordering::Equal => { + if existing_color == &new_color { + new_buffer_color_inlays.push(( + new_range, + new_color, + *existing_inlay_id, + )); + } else { + colors_splice + .to_remove + .push(*existing_inlay_id); + + let inlay = Inlay::color( + post_inc(&mut editor.next_color_inlay_id), + new_range.start, + rgba_color, + ); + let inlay_id = inlay.id; + colors_splice.to_insert.push(inlay); + new_buffer_color_inlays + .push((new_range, new_color, inlay_id)); + } + existing_buffer_colors.next(); + break; + } + cmp::Ordering::Greater => { + let inlay = Inlay::color( + post_inc(&mut editor.next_color_inlay_id), + new_range.start, + rgba_color, + ); + let inlay_id = inlay.id; + colors_splice.to_insert.push(inlay); + new_buffer_color_inlays + .push((new_range, new_color, inlay_id)); + break; + } + } + } + None => { + let inlay = Inlay::color( + post_inc(&mut editor.next_color_inlay_id), + new_range.start, + rgba_color, + ); + let inlay_id = inlay.id; + colors_splice.to_insert.push(inlay); + new_buffer_color_inlays + .push((new_range, new_color, inlay_id)); + break; + } + } + } + } + + if existing_buffer_colors.peek().is_some() { + colors_splice + .to_remove + .extend(existing_buffer_colors.map(|(_, _, id)| *id)); + } + updated |= colors.set_colors( + buffer_id, + new_buffer_color_inlays, + new_cache_version, + ); + } + + if colors.render_mode == DocumentColorsRenderMode::Inlay + && (!colors_splice.to_insert.is_empty() + || !colors_splice.to_remove.is_empty()) + { + editor.splice_inlays(&colors_splice.to_remove, colors_splice.to_insert, cx); + updated = true; + } + + if updated { + cx.notify(); + } + }) + .ok(); + }) + .detach(); + } +} diff --git a/crates/editor/src/lsp_ext.rs b/crates/editor/src/lsp_ext.rs index dd91b59b48..18ad2d71c8 100644 --- a/crates/editor/src/lsp_ext.rs +++ b/crates/editor/src/lsp_ext.rs @@ -3,9 +3,8 @@ use std::time::Duration; use crate::Editor; use collections::HashMap; -use futures::stream::FuturesUnordered; use gpui::AsyncApp; -use gpui::{App, AppContext as _, Entity, Task}; +use gpui::{App, Entity, Task}; use itertools::Itertools; use language::Buffer; use language::Language; @@ -18,63 +17,42 @@ use project::Project; use project::TaskSourceKind; use project::lsp_store::lsp_ext_command::GetLspRunnables; use smol::future::FutureExt as _; -use smol::stream::StreamExt; use task::ResolvedTask; use task::TaskContext; use text::BufferId; +use ui::SharedString; use util::ResultExt as _; pub(crate) fn find_specific_language_server_in_selection( editor: &Editor, cx: &mut App, filter_language: F, - language_server_name: &str, -) -> Task, LanguageServerId, Entity)>> + language_server_name: LanguageServerName, +) -> Option<(Anchor, Arc, LanguageServerId, Entity)> where F: Fn(&Language) -> bool, { - let Some(project) = &editor.project else { - return Task::ready(None); - }; - - let applicable_buffers = editor + let project = editor.project.clone()?; + editor .selections .disjoint_anchors() .iter() - .filter(|selection| selection.start == selection.end) - .filter_map(|selection| Some((selection.start, selection.start.buffer_id?))) - .filter_map(|(trigger_anchor, buffer_id)| { + .filter_map(|selection| Some((selection.head(), selection.head().buffer_id?))) + .unique_by(|(_, buffer_id)| *buffer_id) + .find_map(|(trigger_anchor, buffer_id)| { let buffer = editor.buffer().read(cx).buffer(buffer_id)?; let language = buffer.read(cx).language_at(trigger_anchor.text_anchor)?; if filter_language(&language) { - Some((trigger_anchor, buffer, language)) + let server_id = buffer.update(cx, |buffer, cx| { + project + .read(cx) + .language_server_id_for_name(buffer, &language_server_name, cx) + })?; + Some((trigger_anchor, language, server_id, buffer)) } else { None } }) - .unique_by(|(_, buffer, _)| buffer.read(cx).remote_id()) - .collect::>(); - - let applicable_buffer_tasks = applicable_buffers - .into_iter() - .map(|(trigger_anchor, buffer, language)| { - let task = buffer.update(cx, |buffer, cx| { - project.update(cx, |project, cx| { - project.language_server_id_for_name(buffer, language_server_name, cx) - }) - }); - (trigger_anchor, buffer, language, task) - }) - .collect::>(); - cx.background_spawn(async move { - for (trigger_anchor, buffer, language, task) in applicable_buffer_tasks { - if let Some(server_id) = task.await { - return Some((trigger_anchor, language, server_id, buffer)); - } - } - - None - }) } async fn lsp_task_context( @@ -98,7 +76,7 @@ async fn lsp_task_context( let project_env = project .update(cx, |project, cx| { - project.buffer_environment(&buffer, &worktree_store, cx) + project.buffer_environment(buffer, &worktree_store, cx) }) .ok()? .await; @@ -116,9 +94,9 @@ pub fn lsp_tasks( for_position: Option, cx: &mut App, ) -> Task, ResolvedTask)>)>> { - let mut lsp_task_sources = task_sources + let lsp_task_sources = task_sources .iter() - .map(|(name, buffer_ids)| { + .filter_map(|(name, buffer_ids)| { let buffers = buffer_ids .iter() .filter(|&&buffer_id| match for_position { @@ -127,52 +105,65 @@ pub fn lsp_tasks( }) .filter_map(|&buffer_id| project.read(cx).buffer_for_id(buffer_id, cx)) .collect::>(); - language_server_for_buffers(project.clone(), name.clone(), buffers, cx) + + let server_id = buffers.iter().find_map(|buffer| { + project.read_with(cx, |project, cx| { + project.language_server_id_for_name(buffer.read(cx), name, cx) + }) + }); + server_id.zip(Some(buffers)) }) - .collect::>(); + .collect::>(); cx.spawn(async move |cx| { cx.spawn(async move |cx| { - let mut lsp_tasks = Vec::new(); - while let Some(server_to_query) = lsp_task_sources.next().await { - if let Some((server_id, buffers)) = server_to_query { - let source_kind = TaskSourceKind::Lsp(server_id); + let mut lsp_tasks = HashMap::default(); + for (server_id, buffers) in lsp_task_sources { + let mut new_lsp_tasks = Vec::new(); + for buffer in buffers { + let source_kind = match buffer.update(cx, |buffer, _| { + buffer.language().map(|language| language.name()) + }) { + Ok(Some(language_name)) => TaskSourceKind::Lsp { + server: server_id, + language_name: SharedString::from(language_name), + }, + Ok(None) => continue, + Err(_) => return Vec::new(), + }; let id_base = source_kind.to_id_base(); - let mut new_lsp_tasks = Vec::new(); - for buffer in buffers { - let lsp_buffer_context = lsp_task_context(&project, &buffer, cx) - .await - .unwrap_or_default(); + let lsp_buffer_context = lsp_task_context(&project, &buffer, cx) + .await + .unwrap_or_default(); - if let Ok(runnables_task) = project.update(cx, |project, cx| { - let buffer_id = buffer.read(cx).remote_id(); - project.request_lsp( - buffer, - LanguageServerToQuery::Other(server_id), - GetLspRunnables { - buffer_id, - position: for_position, - }, - cx, - ) - }) { - if let Some(new_runnables) = runnables_task.await.log_err() { - new_lsp_tasks.extend( - new_runnables.runnables.into_iter().filter_map( - |(location, runnable)| { - let resolved_task = runnable - .resolve_task(&id_base, &lsp_buffer_context)?; - Some((location, resolved_task)) - }, - ), - ); - } - } + if let Ok(runnables_task) = project.update(cx, |project, cx| { + let buffer_id = buffer.read(cx).remote_id(); + project.request_lsp( + buffer, + LanguageServerToQuery::Other(server_id), + GetLspRunnables { + buffer_id, + position: for_position, + }, + cx, + ) + }) && let Some(new_runnables) = runnables_task.await.log_err() + { + new_lsp_tasks.extend(new_runnables.runnables.into_iter().filter_map( + |(location, runnable)| { + let resolved_task = + runnable.resolve_task(&id_base, &lsp_buffer_context)?; + Some((location, resolved_task)) + }, + )); } - lsp_tasks.push((source_kind, new_lsp_tasks)); + lsp_tasks + .entry(source_kind) + .or_insert_with(Vec::new) + .append(&mut new_lsp_tasks); } } - lsp_tasks + lsp_tasks.into_iter().collect() }) .race({ // `lsp::LSP_REQUEST_TIMEOUT` is larger than we want for the modal to open fast @@ -186,27 +177,3 @@ pub fn lsp_tasks( .await }) } - -fn language_server_for_buffers( - project: Entity, - name: LanguageServerName, - candidates: Vec>, - cx: &mut App, -) -> Task>)>> { - cx.spawn(async move |cx| { - for buffer in &candidates { - let server_id = buffer - .update(cx, |buffer, cx| { - project.update(cx, |project, cx| { - project.language_server_id_for_name(buffer, &name.0, cx) - }) - }) - .ok()? - .await; - if let Some(server_id) = server_id { - return Some((server_id, candidates)); - } - } - None - }) -} diff --git a/crates/editor/src/mouse_context_menu.rs b/crates/editor/src/mouse_context_menu.rs index 441a3821c6..3bc334c54c 100644 --- a/crates/editor/src/mouse_context_menu.rs +++ b/crates/editor/src/mouse_context_menu.rs @@ -1,8 +1,8 @@ use crate::{ - Copy, CopyAndTrim, CopyPermalinkToLine, Cut, DebuggerEvaluateSelectedText, DisplayPoint, - DisplaySnapshot, Editor, FindAllReferences, GoToDeclaration, GoToDefinition, - GoToImplementation, GoToTypeDefinition, Paste, Rename, RevealInFileManager, SelectMode, - SelectionExt, ToDisplayPoint, ToggleCodeActions, + Copy, CopyAndTrim, CopyPermalinkToLine, Cut, DisplayPoint, DisplaySnapshot, Editor, + EvaluateSelectedText, FindAllReferences, GoToDeclaration, GoToDefinition, GoToImplementation, + GoToTypeDefinition, Paste, Rename, RevealInFileManager, RunToCursor, SelectMode, + SelectionEffects, SelectionExt, ToDisplayPoint, ToggleCodeActions, actions::{Format, FormatSelections}, selections_collection::SelectionsCollection, }; @@ -61,13 +61,13 @@ impl MouseContextMenu { source, offset: position - (source_position + content_origin), }; - return Some(MouseContextMenu::new( + Some(MouseContextMenu::new( editor, menu_position, context_menu, window, cx, - )); + )) } pub(crate) fn new( @@ -102,11 +102,11 @@ impl MouseContextMenu { let display_snapshot = &editor .display_map .update(cx, |display_map, cx| display_map.snapshot(cx)); - let selection_init_range = selection_init.display_range(&display_snapshot); + let selection_init_range = selection_init.display_range(display_snapshot); let selection_now_range = editor .selections .newest_anchor() - .display_range(&display_snapshot); + .display_range(display_snapshot); if selection_now_range == selection_init_range { return; } @@ -177,7 +177,7 @@ pub fn deploy_context_menu( let anchor = buffer.anchor_before(point.to_point(&display_map)); if !display_ranges(&display_map, &editor.selections).any(|r| r.contains(&point)) { // Move the cursor to the clicked location so that dispatched actions make sense - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.clear_disjoint(); s.set_pending_anchor_range(anchor..anchor, SelectMode::Character); }); @@ -190,28 +190,33 @@ pub fn deploy_context_menu( .all::(cx) .into_iter() .any(|s| !s.is_empty()); - let has_git_repo = anchor.buffer_id.is_some_and(|buffer_id| { - project - .read(cx) - .git_store() - .read(cx) - .repository_and_path_for_buffer_id(buffer_id, cx) - .is_some() - }); - - let evaluate_selection = command_palette_hooks::CommandPaletteFilter::try_global(cx) - .map_or(false, |filter| { - !filter.is_hidden(&DebuggerEvaluateSelectedText) + let has_git_repo = buffer + .buffer_id_for_anchor(anchor) + .is_some_and(|buffer_id| { + project + .read(cx) + .git_store() + .read(cx) + .repository_and_path_for_buffer_id(buffer_id, cx) + .is_some() }); + let evaluate_selection = window.is_action_available(&EvaluateSelectedText, cx); + let run_to_cursor = window.is_action_available(&RunToCursor, cx); + ui::ContextMenu::build(window, cx, |menu, _window, _cx| { let builder = menu .on_blur_subscription(Subscription::new(|| {})) - .when(evaluate_selection && has_selections, |builder| { - builder - .action("Evaluate Selection", Box::new(DebuggerEvaluateSelectedText)) - .separator() + .when(run_to_cursor, |builder| { + builder.action("Run to Cursor", Box::new(RunToCursor)) }) + .when(evaluate_selection && has_selections, |builder| { + builder.action("Evaluate Selection", Box::new(EvaluateSelectedText)) + }) + .when( + run_to_cursor || (evaluate_selection && has_selections), + |builder| builder.separator(), + ) .action("Go to Definition", Box::new(GoToDefinition)) .action("Go to Declaration", Box::new(GoToDeclaration)) .action("Go to Type Definition", Box::new(GoToTypeDefinition)) @@ -236,31 +241,25 @@ pub fn deploy_context_menu( .action("Copy and Trim", Box::new(CopyAndTrim)) .action("Paste", Box::new(Paste)) .separator() - .map(|builder| { - let reveal_in_finder_label = if cfg!(target_os = "macos") { + .action_disabled_when( + !has_reveal_target, + if cfg!(target_os = "macos") { "Reveal in Finder" } else { "Reveal in File Manager" - }; - const OPEN_IN_TERMINAL_LABEL: &str = "Open in Terminal"; - if has_reveal_target { - builder - .action(reveal_in_finder_label, Box::new(RevealInFileManager)) - .action(OPEN_IN_TERMINAL_LABEL, Box::new(OpenInTerminal)) - } else { - builder - .disabled_action(reveal_in_finder_label, Box::new(RevealInFileManager)) - .disabled_action(OPEN_IN_TERMINAL_LABEL, Box::new(OpenInTerminal)) - } - }) - .map(|builder| { - const COPY_PERMALINK_LABEL: &str = "Copy Permalink"; - if has_git_repo { - builder.action(COPY_PERMALINK_LABEL, Box::new(CopyPermalinkToLine)) - } else { - builder.disabled_action(COPY_PERMALINK_LABEL, Box::new(CopyPermalinkToLine)) - } - }); + }, + Box::new(RevealInFileManager), + ) + .action_disabled_when( + !has_reveal_target, + "Open in Terminal", + Box::new(OpenInTerminal), + ) + .action_disabled_when( + !has_git_repo, + "Copy Permalink", + Box::new(CopyPermalinkToLine), + ); match focus { Some(focus) => builder.context(focus), None => builder, @@ -278,10 +277,10 @@ pub fn deploy_context_menu( cx, ), None => { - let character_size = editor.character_size(window); + let character_size = editor.character_dimensions(window); let menu_position = MenuPosition::PinnedToEditor { source: source_anchor, - offset: gpui::point(character_size.width, character_size.height), + offset: gpui::point(character_size.em_width, character_size.line_height), }; Some(MouseContextMenu::new( editor, diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index c6720d40ff..7a008e3ba2 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -2,7 +2,7 @@ //! in editor given a given motion (e.g. it handles converting a "move left" command into coordinates in editor). It is exposed mostly for use by vim crate. use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint}; -use crate::{CharKind, DisplayRow, EditorStyle, ToOffset, ToPoint, scroll::ScrollAnchor}; +use crate::{DisplayRow, EditorStyle, ToOffset, ToPoint, scroll::ScrollAnchor}; use gpui::{Pixels, WindowTextSystem}; use language::Point; use multi_buffer::{MultiBufferRow, MultiBufferSnapshot}; @@ -58,8 +58,8 @@ pub fn saturating_left(map: &DisplaySnapshot, mut point: DisplayPoint) -> Displa map.clip_point(point, Bias::Left) } -/// Returns a column to the right of the current point, doing nothing -// if that point is at the end of the line. +/// Returns a column to the right of the current point, wrapping +/// to the next line if that point is at the end of line. pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { if point.column() < map.line_len(point.row()) { *point.column_mut() += 1; @@ -230,7 +230,7 @@ pub fn indented_line_beginning( if stop_at_soft_boundaries && soft_line_start > indent_start && display_point != soft_line_start { soft_line_start - } else if stop_at_indent && display_point != indent_start { + } else if stop_at_indent && (display_point > indent_start || display_point == line_start) { indent_start } else { line_start @@ -264,7 +264,19 @@ pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> Displa let raw_point = point.to_point(map); let classifier = map.buffer_snapshot.char_classifier_at(raw_point); + let mut is_first_iteration = true; find_preceding_boundary_display_point(map, point, FindRange::MultiLine, |left, right| { + // Make alt-left skip punctuation to respect VSCode behaviour. For example: hello.| goes to |hello. + if is_first_iteration + && classifier.is_punctuation(right) + && !classifier.is_punctuation(left) + && left != '\n' + { + is_first_iteration = false; + return false; + } + is_first_iteration = false; + (classifier.kind(left) != classifier.kind(right) && !classifier.is_whitespace(right)) || left == '\n' }) @@ -305,8 +317,19 @@ pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> Dis pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { let raw_point = point.to_point(map); let classifier = map.buffer_snapshot.char_classifier_at(raw_point); - + let mut is_first_iteration = true; find_boundary(map, point, FindRange::MultiLine, |left, right| { + // Make alt-right skip punctuation to respect VSCode behaviour. For example: |.hello goes to .hello| + if is_first_iteration + && classifier.is_punctuation(left) + && !classifier.is_punctuation(right) + && right != '\n' + { + is_first_iteration = false; + return false; + } + is_first_iteration = false; + (classifier.kind(left) != classifier.kind(right) && !classifier.is_whitespace(left)) || right == '\n' }) @@ -416,17 +439,17 @@ pub fn start_of_excerpt( }; match direction { Direction::Prev => { - let mut start = excerpt.start_anchor().to_display_point(&map); + let mut start = excerpt.start_anchor().to_display_point(map); if start >= display_point && start.row() > DisplayRow(0) { let Some(excerpt) = map.buffer_snapshot.excerpt_before(excerpt.id()) else { return display_point; }; - start = excerpt.start_anchor().to_display_point(&map); + start = excerpt.start_anchor().to_display_point(map); } start } Direction::Next => { - let mut end = excerpt.end_anchor().to_display_point(&map); + let mut end = excerpt.end_anchor().to_display_point(map); *end.row_mut() += 1; map.clip_point(end, Bias::Right) } @@ -444,7 +467,7 @@ pub fn end_of_excerpt( }; match direction { Direction::Prev => { - let mut start = excerpt.start_anchor().to_display_point(&map); + let mut start = excerpt.start_anchor().to_display_point(map); if start.row() > DisplayRow(0) { *start.row_mut() -= 1; } @@ -453,7 +476,7 @@ pub fn end_of_excerpt( start } Direction::Next => { - let mut end = excerpt.end_anchor().to_display_point(&map); + let mut end = excerpt.end_anchor().to_display_point(map); *end.column_mut() = 0; if end <= display_point { *end.row_mut() += 1; @@ -462,7 +485,7 @@ pub fn end_of_excerpt( else { return display_point; }; - end = excerpt.end_anchor().to_display_point(&map); + end = excerpt.end_anchor().to_display_point(map); *end.column_mut() = 0; } end @@ -487,10 +510,10 @@ pub fn find_preceding_boundary_point( if find_range == FindRange::SingleLine && ch == '\n' { break; } - if let Some(prev_ch) = prev_ch { - if is_boundary(ch, prev_ch) { - break; - } + if let Some(prev_ch) = prev_ch + && is_boundary(ch, prev_ch) + { + break; } offset -= ch.len_utf8(); @@ -539,13 +562,13 @@ pub fn find_boundary_point( if find_range == FindRange::SingleLine && ch == '\n' { break; } - if let Some(prev_ch) = prev_ch { - if is_boundary(prev_ch, ch) { - if return_point_before_boundary { - return map.clip_point(prev_offset.to_display_point(map), Bias::Right); - } else { - break; - } + if let Some(prev_ch) = prev_ch + && is_boundary(prev_ch, ch) + { + if return_point_before_boundary { + return map.clip_point(prev_offset.to_display_point(map), Bias::Right); + } else { + break; } } prev_offset = offset; @@ -580,13 +603,13 @@ pub fn find_preceding_boundary_trail( // Find the boundary let start_offset = offset; for ch in forward { - if let Some(prev_ch) = prev_ch { - if is_boundary(prev_ch, ch) { - if start_offset == offset { - trail_offset = Some(offset); - } else { - break; - } + if let Some(prev_ch) = prev_ch + && is_boundary(prev_ch, ch) + { + if start_offset == offset { + trail_offset = Some(offset); + } else { + break; } } offset -= ch.len_utf8(); @@ -628,13 +651,13 @@ pub fn find_boundary_trail( // Find the boundary let start_offset = offset; for ch in forward { - if let Some(prev_ch) = prev_ch { - if is_boundary(prev_ch, ch) { - if start_offset == offset { - trail_offset = Some(offset); - } else { - break; - } + if let Some(prev_ch) = prev_ch + && is_boundary(prev_ch, ch) + { + if start_offset == offset { + trail_offset = Some(offset); + } else { + break; } } offset += ch.len_utf8(); @@ -698,38 +721,6 @@ pub fn chars_before( }) } -pub(crate) fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool { - let raw_point = point.to_point(map); - let classifier = map.buffer_snapshot.char_classifier_at(raw_point); - let ix = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left); - let text = &map.buffer_snapshot; - let next_char_kind = text.chars_at(ix).next().map(|c| classifier.kind(c)); - let prev_char_kind = text - .reversed_chars_at(ix) - .next() - .map(|c| classifier.kind(c)); - prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word)) -} - -pub(crate) fn surrounding_word( - map: &DisplaySnapshot, - position: DisplayPoint, -) -> Range { - let position = map - .clip_point(position, Bias::Left) - .to_offset(map, Bias::Left); - let (range, _) = map.buffer_snapshot.surrounding_word(position, false); - let start = range - .start - .to_point(&map.buffer_snapshot) - .to_display_point(map); - let end = range - .end - .to_point(&map.buffer_snapshot) - .to_display_point(map); - start..end -} - /// Returns a list of lines (represented as a [`DisplayPoint`] range) contained /// within a passed range. /// @@ -766,7 +757,7 @@ pub fn split_display_range_by_lines( mod tests { use super::*; use crate::{ - Buffer, DisplayMap, DisplayRow, ExcerptRange, FoldPlaceholder, InlayId, MultiBuffer, + Buffer, DisplayMap, DisplayRow, ExcerptRange, FoldPlaceholder, MultiBuffer, display_map::Inlay, test::{editor_test_context::EditorTestContext, marked_display_snapshot}, }; @@ -782,10 +773,15 @@ mod tests { fn assert(marked_text: &str, cx: &mut gpui::App) { let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); - assert_eq!( - previous_word_start(&snapshot, display_points[1]), - display_points[0] - ); + let actual = previous_word_start(&snapshot, display_points[1]); + let expected = display_points[0]; + if actual != expected { + eprintln!( + "previous_word_start mismatch for '{}': actual={:?}, expected={:?}", + marked_text, actual, expected + ); + } + assert_eq!(actual, expected); } assert("\nˇ ˇlorem", cx); @@ -796,12 +792,17 @@ mod tests { assert("\nlorem\nˇ ˇipsum", cx); assert("\n\nˇ\nˇ", cx); assert(" ˇlorem ˇipsum", cx); - assert("loremˇ-ˇipsum", cx); + assert("ˇlorem-ˇipsum", cx); assert("loremˇ-#$@ˇipsum", cx); assert("ˇlorem_ˇipsum", cx); assert(" ˇdefγˇ", cx); assert(" ˇbcΔˇ", cx); - assert(" abˇ——ˇcd", cx); + // Test punctuation skipping behavior + assert("ˇhello.ˇ", cx); + assert("helloˇ...ˇ", cx); + assert("helloˇ.---..ˇtest", cx); + assert("test ˇ.--ˇtest", cx); + assert("oneˇ,;:!?ˇtwo", cx); } #[gpui::test] @@ -906,26 +907,26 @@ mod tests { let inlays = (0..buffer_snapshot.len()) .flat_map(|offset| { [ - Inlay { - id: InlayId::InlineCompletion(post_inc(&mut id)), - position: buffer_snapshot.anchor_at(offset, Bias::Left), - text: "test".into(), - }, - Inlay { - id: InlayId::InlineCompletion(post_inc(&mut id)), - position: buffer_snapshot.anchor_at(offset, Bias::Right), - text: "test".into(), - }, - Inlay { - id: InlayId::Hint(post_inc(&mut id)), - position: buffer_snapshot.anchor_at(offset, Bias::Left), - text: "test".into(), - }, - Inlay { - id: InlayId::Hint(post_inc(&mut id)), - position: buffer_snapshot.anchor_at(offset, Bias::Right), - text: "test".into(), - }, + Inlay::edit_prediction( + post_inc(&mut id), + buffer_snapshot.anchor_at(offset, Bias::Left), + "test", + ), + Inlay::edit_prediction( + post_inc(&mut id), + buffer_snapshot.anchor_at(offset, Bias::Right), + "test", + ), + Inlay::mock_hint( + post_inc(&mut id), + buffer_snapshot.anchor_at(offset, Bias::Left), + "test", + ), + Inlay::mock_hint( + post_inc(&mut id), + buffer_snapshot.anchor_at(offset, Bias::Right), + "test", + ), ] }) .collect(); @@ -955,10 +956,15 @@ mod tests { fn assert(marked_text: &str, cx: &mut gpui::App) { let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); - assert_eq!( - next_word_end(&snapshot, display_points[0]), - display_points[1] - ); + let actual = next_word_end(&snapshot, display_points[0]); + let expected = display_points[1]; + if actual != expected { + eprintln!( + "next_word_end mismatch for '{}': actual={:?}, expected={:?}", + marked_text, actual, expected + ); + } + assert_eq!(actual, expected); } assert("\nˇ loremˇ", cx); @@ -967,11 +973,18 @@ mod tests { assert(" loremˇ ˇ\nipsum\n", cx); assert("\nˇ\nˇ\n\n", cx); assert("loremˇ ipsumˇ ", cx); - assert("loremˇ-ˇipsum", cx); + assert("loremˇ-ipsumˇ", cx); assert("loremˇ#$@-ˇipsum", cx); assert("loremˇ_ipsumˇ", cx); assert(" ˇbcΔˇ", cx); assert(" abˇ——ˇcd", cx); + // Test punctuation skipping behavior + assert("ˇ.helloˇ", cx); + assert("display_pointsˇ[0ˇ]", cx); + assert("ˇ...ˇhello", cx); + assert("helloˇ.---..ˇtest", cx); + assert("testˇ.--ˇ test", cx); + assert("oneˇ,;:!?ˇtwo", cx); } #[gpui::test] @@ -1046,30 +1059,6 @@ mod tests { }); } - #[gpui::test] - fn test_surrounding_word(cx: &mut gpui::App) { - init_test(cx); - - fn assert(marked_text: &str, cx: &mut gpui::App) { - let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); - assert_eq!( - surrounding_word(&snapshot, display_points[1]), - display_points[0]..display_points[2], - "{}", - marked_text - ); - } - - assert("ˇˇloremˇ ipsum", cx); - assert("ˇloˇremˇ ipsum", cx); - assert("ˇloremˇˇ ipsum", cx); - assert("loremˇ ˇ ˇipsum", cx); - assert("lorem\nˇˇˇ\nipsum", cx); - assert("lorem\nˇˇipsumˇ", cx); - assert("loremˇ,ˇˇ ipsum", cx); - assert("ˇloremˇˇ, ipsum", cx); - } - #[gpui::test] async fn test_move_up_and_down_with_excerpts(cx: &mut gpui::TestAppContext) { cx.update(|cx| { diff --git a/crates/editor/src/proposed_changes_editor.rs b/crates/editor/src/proposed_changes_editor.rs index d6e253271b..2d4710a8d4 100644 --- a/crates/editor/src/proposed_changes_editor.rs +++ b/crates/editor/src/proposed_changes_editor.rs @@ -1,4 +1,4 @@ -use crate::{ApplyAllDiffHunks, Editor, EditorEvent, SemanticsProvider}; +use crate::{ApplyAllDiffHunks, Editor, EditorEvent, SelectionEffects, SemanticsProvider}; use buffer_diff::BufferDiff; use collections::HashSet; use futures::{channel::mpsc, future::join_all}; @@ -12,7 +12,7 @@ use text::ToOffset; use ui::{ButtonLike, KeyBinding, prelude::*}; use workspace::{ Item, ItemHandle as _, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, - searchable::SearchableItemHandle, + item::SaveOptions, searchable::SearchableItemHandle, }; pub struct ProposedChangesEditor { @@ -213,7 +213,9 @@ impl ProposedChangesEditor { self.buffer_entries = buffer_entries; self.editor.update(cx, |editor, cx| { - editor.change_selections(None, window, cx, |selections| selections.refresh()); + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| { + selections.refresh() + }); editor.buffer.update(cx, |buffer, cx| { for diff in new_diffs { buffer.add_diff(diff, cx) @@ -239,24 +241,13 @@ impl ProposedChangesEditor { event: &BufferEvent, _cx: &mut Context, ) { - match event { - BufferEvent::Operation { .. } => { - self.recalculate_diffs_tx - .unbounded_send(RecalculateDiff { - buffer, - debounce: true, - }) - .ok(); - } - // BufferEvent::DiffBaseChanged => { - // self.recalculate_diffs_tx - // .unbounded_send(RecalculateDiff { - // buffer, - // debounce: false, - // }) - // .ok(); - // } - _ => (), + if let BufferEvent::Operation { .. } = event { + self.recalculate_diffs_tx + .unbounded_send(RecalculateDiff { + buffer, + debounce: true, + }) + .ok(); } } } @@ -351,13 +342,13 @@ impl Item for ProposedChangesEditor { fn save( &mut self, - format: bool, + options: SaveOptions, project: Entity, window: &mut Window, cx: &mut Context, ) -> Task> { self.editor.update(cx, |editor, cx| { - Item::save(editor, format, project, window, cx) + Item::save(editor, options, project, window, cx) }) } } @@ -440,7 +431,7 @@ impl SemanticsProvider for BranchBufferSemanticsProvider { buffer: &Entity, position: text::Anchor, cx: &mut App, - ) -> Option>> { + ) -> Option>>> { let buffer = self.to_base(buffer, &[position], cx)?; self.0.hover(&buffer, position, cx) } @@ -476,7 +467,7 @@ impl SemanticsProvider for BranchBufferSemanticsProvider { } fn supports_inlay_hints(&self, buffer: &Entity, cx: &mut App) -> bool { - if let Some(buffer) = self.to_base(&buffer, &[], cx) { + if let Some(buffer) = self.to_base(buffer, &[], cx) { self.0.supports_inlay_hints(&buffer, cx) } else { false @@ -489,7 +480,7 @@ impl SemanticsProvider for BranchBufferSemanticsProvider { position: text::Anchor, cx: &mut App, ) -> Option>>> { - let buffer = self.to_base(&buffer, &[position], cx)?; + let buffer = self.to_base(buffer, &[position], cx)?; self.0.document_highlights(&buffer, position, cx) } @@ -499,8 +490,8 @@ impl SemanticsProvider for BranchBufferSemanticsProvider { position: text::Anchor, kind: crate::GotoDefinitionKind, cx: &mut App, - ) -> Option>>> { - let buffer = self.to_base(&buffer, &[position], cx)?; + ) -> Option>>>> { + let buffer = self.to_base(buffer, &[position], cx)?; self.0.definitions(&buffer, position, kind, cx) } diff --git a/crates/editor/src/rust_analyzer_ext.rs b/crates/editor/src/rust_analyzer_ext.rs index 86153334fb..cf74ee0a9e 100644 --- a/crates/editor/src/rust_analyzer_ext.rs +++ b/crates/editor/src/rust_analyzer_ext.rs @@ -26,6 +26,17 @@ fn is_rust_language(language: &Language) -> bool { } pub fn apply_related_actions(editor: &Entity, window: &mut Window, cx: &mut App) { + if editor.read(cx).project().is_some_and(|project| { + project + .read(cx) + .language_server_statuses(cx) + .any(|(_, status)| status.name == RUST_ANALYZER_NAME) + }) { + register_action(editor, window, cancel_flycheck_action); + register_action(editor, window, run_flycheck_action); + register_action(editor, window, clear_flycheck_action); + } + if editor .read(cx) .buffer() @@ -35,12 +46,9 @@ pub fn apply_related_actions(editor: &Entity, window: &mut Window, cx: & .filter_map(|buffer| buffer.read(cx).language()) .any(|language| is_rust_language(language)) { - register_action(&editor, window, go_to_parent_module); - register_action(&editor, window, expand_macro_recursively); - register_action(&editor, window, open_docs); - register_action(&editor, window, cancel_flycheck_action); - register_action(&editor, window, run_flycheck_action); - register_action(&editor, window, clear_flycheck_action); + register_action(editor, window, go_to_parent_module); + register_action(editor, window, expand_macro_recursively); + register_action(editor, window, open_docs); } } @@ -57,21 +65,21 @@ pub fn go_to_parent_module( return; }; - let server_lookup = find_specific_language_server_in_selection( - editor, - cx, - is_rust_language, - RUST_ANALYZER_NAME, - ); + let Some((trigger_anchor, _, server_to_query, buffer)) = + find_specific_language_server_in_selection( + editor, + cx, + is_rust_language, + RUST_ANALYZER_NAME, + ) + else { + return; + }; let project = project.clone(); let lsp_store = project.read(cx).lsp_store(); let upstream_client = lsp_store.read(cx).upstream_client(); cx.spawn_in(window, async move |editor, cx| { - let Some((trigger_anchor, _, server_to_query, buffer)) = server_lookup.await else { - return anyhow::Ok(()); - }; - let location_links = if let Some((client, project_id)) = upstream_client { let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id())?; @@ -121,7 +129,7 @@ pub fn go_to_parent_module( ) })? .await?; - Ok(()) + anyhow::Ok(()) }) .detach_and_log_err(cx); } @@ -132,9 +140,6 @@ pub fn expand_macro_recursively( window: &mut Window, cx: &mut Context, ) { - if editor.selections.count() == 0 { - return; - } let Some(project) = &editor.project else { return; }; @@ -142,21 +147,19 @@ pub fn expand_macro_recursively( return; }; - let server_lookup = find_specific_language_server_in_selection( - editor, - cx, - is_rust_language, - RUST_ANALYZER_NAME, - ); - + let Some((trigger_anchor, rust_language, server_to_query, buffer)) = + find_specific_language_server_in_selection( + editor, + cx, + is_rust_language, + RUST_ANALYZER_NAME, + ) + else { + return; + }; let project = project.clone(); let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client(); cx.spawn_in(window, async move |_editor, cx| { - let Some((trigger_anchor, rust_language, server_to_query, buffer)) = server_lookup.await - else { - return Ok(()); - }; - let macro_expansion = if let Some((client, project_id)) = upstream_client { let buffer_id = buffer.update(cx, |buffer, _| buffer.remote_id())?; let request = proto::LspExtExpandMacro { @@ -234,20 +237,20 @@ pub fn open_docs(editor: &mut Editor, _: &OpenDocs, window: &mut Window, cx: &mu return; }; - let server_lookup = find_specific_language_server_in_selection( - editor, - cx, - is_rust_language, - RUST_ANALYZER_NAME, - ); + let Some((trigger_anchor, _, server_to_query, buffer)) = + find_specific_language_server_in_selection( + editor, + cx, + is_rust_language, + RUST_ANALYZER_NAME, + ) + else { + return; + }; let project = project.clone(); let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client(); cx.spawn_in(window, async move |_editor, cx| { - let Some((trigger_anchor, _, server_to_query, buffer)) = server_lookup.await else { - return Ok(()); - }; - let docs_urls = if let Some((client, project_id)) = upstream_client { let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id())?; let request = proto::LspExtOpenDocs { @@ -290,11 +293,11 @@ pub fn open_docs(editor: &mut Editor, _: &OpenDocs, window: &mut Window, cx: &mu workspace.update(cx, |_workspace, cx| { // Check if the local document exists, otherwise fallback to the online document. // Open with the default browser. - if let Some(local_url) = docs_urls.local { - if fs::metadata(Path::new(&local_url[8..])).is_ok() { - cx.open_url(&local_url); - return; - } + if let Some(local_url) = docs_urls.local + && fs::metadata(Path::new(&local_url[8..])).is_ok() + { + cx.open_url(&local_url); + return; } if let Some(web_url) = docs_urls.web { @@ -314,7 +317,7 @@ fn cancel_flycheck_action( let Some(project) = &editor.project else { return; }; - let Some(buffer_id) = editor + let buffer_id = editor .selections .disjoint_anchors() .iter() @@ -326,10 +329,7 @@ fn cancel_flycheck_action( .read(cx) .entry_id(cx)?; project.path_for_entry(entry_id, cx) - }) - else { - return; - }; + }); cancel_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx); } @@ -342,7 +342,7 @@ fn run_flycheck_action( let Some(project) = &editor.project else { return; }; - let Some(buffer_id) = editor + let buffer_id = editor .selections .disjoint_anchors() .iter() @@ -354,10 +354,7 @@ fn run_flycheck_action( .read(cx) .entry_id(cx)?; project.path_for_entry(entry_id, cx) - }) - else { - return; - }; + }); run_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx); } @@ -370,7 +367,7 @@ fn clear_flycheck_action( let Some(project) = &editor.project else { return; }; - let Some(buffer_id) = editor + let buffer_id = editor .selections .disjoint_anchors() .iter() @@ -382,9 +379,6 @@ fn clear_flycheck_action( .read(cx) .entry_id(cx)?; project.path_for_entry(entry_id, cx) - }) - else { - return; - }; + }); clear_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx); } diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index a8081b95bd..8231448618 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -12,7 +12,8 @@ use crate::{ }; pub use autoscroll::{Autoscroll, AutoscrollStrategy}; use core::fmt::Debug; -use gpui::{App, Axis, Context, Global, Pixels, Task, Window, point, px}; +use gpui::{Along, App, Axis, Context, Global, Pixels, Task, Window, point, px}; +use language::language_settings::{AllLanguageSettings, SoftWrap}; use language::{Bias, Point}; pub use scroll_amount::ScrollAmount; use settings::Settings; @@ -26,6 +27,8 @@ use workspace::{ItemId, WorkspaceId}; pub const SCROLL_EVENT_SEPARATION: Duration = Duration::from_millis(28); const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1); +pub struct WasScrolled(pub(crate) bool); + #[derive(Default)] pub struct ScrollbarAutoHide(pub bool); @@ -46,14 +49,14 @@ impl ScrollAnchor { } pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> gpui::Point { - let mut scroll_position = self.offset; - if self.anchor == Anchor::min() { - scroll_position.y = 0.; - } else { - let scroll_top = self.anchor.to_display_point(snapshot).row().as_f32(); - scroll_position.y += scroll_top; - } - scroll_position + self.offset.apply_along(Axis::Vertical, |offset| { + if self.anchor == Anchor::min() { + 0. + } else { + let scroll_top = self.anchor.to_display_point(snapshot).row().as_f32(); + (offset + scroll_top).max(0.) + } + }) } pub fn top_row(&self, buffer: &MultiBufferSnapshot) -> u32 { @@ -151,12 +154,16 @@ pub struct ScrollManager { pub(crate) vertical_scroll_margin: f32, anchor: ScrollAnchor, ongoing: OngoingScroll, + /// The second element indicates whether the autoscroll request is local + /// (true) or remote (false). Local requests are initiated by user actions, + /// while remote requests come from external sources. autoscroll_request: Option<(Autoscroll, bool)>, last_autoscroll: Option<(gpui::Point, f32, f32, AutoscrollStrategy)>, show_scrollbars: bool, hide_scrollbar_task: Option>, active_scrollbar: Option, visible_line_count: Option, + visible_column_count: Option, forbid_vertical_scroll: bool, minimap_thumb_state: Option, } @@ -173,6 +180,7 @@ impl ScrollManager { active_scrollbar: None, last_autoscroll: None, visible_line_count: None, + visible_column_count: None, forbid_vertical_scroll: false, minimap_thumb_state: None, } @@ -209,66 +217,56 @@ impl ScrollManager { workspace_id: Option, window: &mut Window, cx: &mut Context, - ) { - let (new_anchor, top_row) = if scroll_position.y <= 0. { - ( - ScrollAnchor { - anchor: Anchor::min(), - offset: scroll_position.max(&gpui::Point::default()), - }, - 0, - ) - } else { - let scroll_top = scroll_position.y; - let scroll_top = match EditorSettings::get_global(cx).scroll_beyond_last_line { - ScrollBeyondLastLine::OnePage => scroll_top, - ScrollBeyondLastLine::Off => { - if let Some(height_in_lines) = self.visible_line_count { - let max_row = map.max_point().row().0 as f32; - scroll_top.min(max_row - height_in_lines + 1.).max(0.) - } else { - scroll_top - } + ) -> WasScrolled { + let scroll_top = scroll_position.y.max(0.); + let scroll_top = match EditorSettings::get_global(cx).scroll_beyond_last_line { + ScrollBeyondLastLine::OnePage => scroll_top, + ScrollBeyondLastLine::Off => { + if let Some(height_in_lines) = self.visible_line_count { + let max_row = map.max_point().row().0 as f32; + scroll_top.min(max_row - height_in_lines + 1.).max(0.) + } else { + scroll_top } - ScrollBeyondLastLine::VerticalScrollMargin => { - if let Some(height_in_lines) = self.visible_line_count { - let max_row = map.max_point().row().0 as f32; - scroll_top - .min(max_row - height_in_lines + 1. + self.vertical_scroll_margin) - .max(0.) - } else { - scroll_top - } + } + ScrollBeyondLastLine::VerticalScrollMargin => { + if let Some(height_in_lines) = self.visible_line_count { + let max_row = map.max_point().row().0 as f32; + scroll_top + .min(max_row - height_in_lines + 1. + self.vertical_scroll_margin) + .max(0.) + } else { + scroll_top } - }; - - let scroll_top_buffer_point = - DisplayPoint::new(DisplayRow(scroll_top as u32), 0).to_point(map); - let top_anchor = map - .buffer_snapshot - .anchor_at(scroll_top_buffer_point, Bias::Right); - - ( - ScrollAnchor { - anchor: top_anchor, - offset: point( - scroll_position.x.max(0.), - scroll_top - top_anchor.to_display_point(map).row().as_f32(), - ), - }, - scroll_top_buffer_point.row, - ) + } }; + let scroll_top_row = DisplayRow(scroll_top as u32); + let scroll_top_buffer_point = map + .clip_point( + DisplayPoint::new(scroll_top_row, scroll_position.x as u32), + Bias::Left, + ) + .to_point(map); + let top_anchor = map + .buffer_snapshot + .anchor_at(scroll_top_buffer_point, Bias::Right); + self.set_anchor( - new_anchor, - top_row, + ScrollAnchor { + anchor: top_anchor, + offset: point( + scroll_position.x.max(0.), + scroll_top - top_anchor.to_display_point(map).row().as_f32(), + ), + }, + scroll_top_buffer_point.row, local, autoscroll, workspace_id, window, cx, - ); + ) } fn set_anchor( @@ -280,7 +278,7 @@ impl ScrollManager { workspace_id: Option, window: &mut Window, cx: &mut Context, - ) { + ) -> WasScrolled { let adjusted_anchor = if self.forbid_vertical_scroll { ScrollAnchor { offset: gpui::Point::new(anchor.offset.x, self.anchor.offset.y), @@ -290,10 +288,14 @@ impl ScrollManager { anchor }; + self.autoscroll_request.take(); + if self.anchor == adjusted_anchor { + return WasScrolled(false); + } + self.anchor = adjusted_anchor; cx.emit(EditorEvent::ScrollPositionChanged { local, autoscroll }); self.show_scrollbars(window, cx); - self.autoscroll_request.take(); if let Some(workspace_id) = workspace_id { let item_id = cx.entity().entity_id().as_u64() as ItemId; @@ -315,6 +317,8 @@ impl ScrollManager { .detach() } cx.notify(); + + WasScrolled(true) } pub fn show_scrollbars(&mut self, window: &mut Window, cx: &mut Context) { @@ -344,8 +348,8 @@ impl ScrollManager { self.show_scrollbars } - pub fn autoscroll_request(&self) -> Option { - self.autoscroll_request.map(|(autoscroll, _)| autoscroll) + pub fn take_autoscroll_request(&mut self) -> Option<(Autoscroll, bool)> { + self.autoscroll_request.take() } pub fn active_scrollbar_state(&self) -> Option<&ActiveScrollbarState> { @@ -476,6 +480,10 @@ impl Editor { .map(|line_count| line_count as u32 - 1) } + pub fn visible_column_count(&self) -> Option { + self.scroll_manager.visible_column_count + } + pub(crate) fn set_visible_line_count( &mut self, lines: f32, @@ -487,8 +495,9 @@ impl Editor { if opened_first_time { cx.spawn_in(window, async move |editor, cx| { editor - .update(cx, |editor, cx| { - editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx) + .update_in(cx, |editor, window, cx| { + editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx); + editor.refresh_colors(false, None, window, cx); }) .ok() }) @@ -496,6 +505,10 @@ impl Editor { } } + pub(crate) fn set_visible_column_count(&mut self, columns: f32) { + self.scroll_manager.visible_column_count = Some(columns); + } + pub fn apply_scroll_delta( &mut self, scroll_delta: gpui::Point, @@ -516,13 +529,13 @@ impl Editor { scroll_position: gpui::Point, window: &mut Window, cx: &mut Context, - ) { + ) -> WasScrolled { let mut position = scroll_position; if self.scroll_manager.forbid_vertical_scroll { let current_position = self.scroll_position(cx); position.y = current_position.y; } - self.set_scroll_position_internal(position, true, false, window, cx); + self.set_scroll_position_internal(position, true, false, window, cx) } /// Scrolls so that `row` is at the top of the editor view. @@ -554,7 +567,7 @@ impl Editor { autoscroll: bool, window: &mut Window, cx: &mut Context, - ) { + ) -> WasScrolled { let map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); self.set_scroll_position_taking_display_map( scroll_position, @@ -563,7 +576,7 @@ impl Editor { map, window, cx, - ); + ) } fn set_scroll_position_taking_display_map( @@ -574,7 +587,7 @@ impl Editor { display_map: DisplaySnapshot, window: &mut Window, cx: &mut Context, - ) { + ) -> WasScrolled { hide_hover(self, cx); let workspace_id = self.workspace.as_ref().and_then(|workspace| workspace.1); @@ -588,7 +601,7 @@ impl Editor { scroll_position }; - self.scroll_manager.set_scroll_position( + let editor_was_scrolled = self.scroll_manager.set_scroll_position( adjusted_position, &display_map, local, @@ -599,6 +612,8 @@ impl Editor { ); self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx); + self.refresh_colors(false, None, window, cx); + editor_was_scrolled } pub fn scroll_position(&self, cx: &mut Context) -> gpui::Point { @@ -660,7 +675,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } @@ -669,18 +684,52 @@ impl Editor { return; } - let cur_position = self.scroll_position(cx); + let mut current_position = self.scroll_position(cx); let Some(visible_line_count) = self.visible_line_count() else { return; }; - let new_pos = cur_position + point(0., amount.lines(visible_line_count)); - self.set_scroll_position(new_pos, window, cx); + let Some(mut visible_column_count) = self.visible_column_count() else { + return; + }; + + // If the user has a preferred line length, and has the editor + // configured to wrap at the preferred line length, or bounded to it, + // use that value over the visible column count. This was mostly done so + // that tests could actually be written for vim's `z l`, `z h`, `z + // shift-l` and `z shift-h` commands, as there wasn't a good way to + // configure the editor to only display a certain number of columns. If + // that ever happens, this could probably be removed. + let settings = AllLanguageSettings::get_global(cx); + if matches!( + settings.defaults.soft_wrap, + SoftWrap::PreferredLineLength | SoftWrap::Bounded + ) && (settings.defaults.preferred_line_length as f32) < visible_column_count + { + visible_column_count = settings.defaults.preferred_line_length as f32; + } + + // If the scroll position is currently at the left edge of the document + // (x == 0.0) and the intent is to scroll right, the gutter's margin + // should first be added to the current position, otherwise the cursor + // will end at the column position minus the margin, which looks off. + if current_position.x == 0.0 + && amount.columns(visible_column_count) > 0. + && let Some(last_position_map) = &self.last_position_map + { + current_position.x += self.gutter_dimensions.margin / last_position_map.em_advance; + } + let new_position = current_position + + point( + amount.columns(visible_column_count), + amount.lines(visible_line_count), + ); + self.set_scroll_position(new_position, window, cx); } /// Returns an ordering. The newest selection is: /// Ordering::Equal => on screen - /// Ordering::Less => above the screen - /// Ordering::Greater => below the screen + /// Ordering::Less => above or to the left of the screen + /// Ordering::Greater => below or to the right of the screen pub fn newest_selection_on_screen(&self, cx: &mut App) -> Ordering { let snapshot = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let newest_head = self @@ -698,10 +747,12 @@ impl Editor { return Ordering::Less; } - if let Some(visible_lines) = self.visible_line_count() { - if newest_head.row() <= DisplayRow(screen_top.row().0 + visible_lines as u32) { - return Ordering::Equal; - } + if let (Some(visible_lines), Some(visible_columns)) = + (self.visible_line_count(), self.visible_column_count()) + && newest_head.row() <= DisplayRow(screen_top.row().0 + visible_lines as u32) + && newest_head.column() <= screen_top.column() + visible_columns as u32 + { + return Ordering::Equal; } Ordering::Greater diff --git a/crates/editor/src/scroll/actions.rs b/crates/editor/src/scroll/actions.rs index 72827b2fee..f8104665f9 100644 --- a/crates/editor/src/scroll/actions.rs +++ b/crates/editor/src/scroll/actions.rs @@ -16,7 +16,7 @@ impl Editor { return; } - if matches!(self.mode, EditorMode::SingleLine { .. }) { + if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } diff --git a/crates/editor/src/scroll/autoscroll.rs b/crates/editor/src/scroll/autoscroll.rs index 7c022e62a4..057d622903 100644 --- a/crates/editor/src/scroll/autoscroll.rs +++ b/crates/editor/src/scroll/autoscroll.rs @@ -1,12 +1,13 @@ use crate::{ - DisplayRow, Editor, EditorMode, LineWithInvisibles, RowExt, display_map::ToDisplayPoint, + DisplayRow, Editor, EditorMode, LineWithInvisibles, RowExt, SelectionEffects, + display_map::ToDisplayPoint, scroll::WasScrolled, }; use gpui::{Bounds, Context, Pixels, Window, px}; use language::Point; use multi_buffer::Anchor; use std::{cmp, f32}; -#[derive(PartialEq, Eq, Clone, Copy)] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum Autoscroll { Next, Strategy(AutoscrollStrategy, Option), @@ -66,7 +67,16 @@ impl Autoscroll { } } -#[derive(PartialEq, Eq, Default, Clone, Copy)] +impl Into for Option { + fn into(self) -> SelectionEffects { + match self { + Some(autoscroll) => SelectionEffects::scroll(autoscroll), + None => SelectionEffects::no_scroll(), + } + } +} + +#[derive(Debug, PartialEq, Eq, Default, Clone, Copy)] pub enum AutoscrollStrategy { Fit, Newest, @@ -89,42 +99,43 @@ impl AutoscrollStrategy { } } -impl Editor { - pub fn autoscroll_request(&self) -> Option { - self.scroll_manager.autoscroll_request() - } +pub(crate) struct NeedsHorizontalAutoscroll(pub(crate) bool); - pub fn autoscroll_vertically( +impl Editor { + pub(crate) fn autoscroll_vertically( &mut self, bounds: Bounds, line_height: Pixels, max_scroll_top: f32, + autoscroll_request: Option<(Autoscroll, bool)>, window: &mut Window, cx: &mut Context, - ) -> bool { + ) -> (NeedsHorizontalAutoscroll, WasScrolled) { let viewport_height = bounds.size.height; let visible_lines = viewport_height / line_height; let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut scroll_position = self.scroll_manager.scroll_position(&display_map); let original_y = scroll_position.y; - if let Some(last_bounds) = self.expect_bounds_change.take() { - if scroll_position.y != 0. { - scroll_position.y += (bounds.top() - last_bounds.top()) / line_height; - if scroll_position.y < 0. { - scroll_position.y = 0.; - } + if let Some(last_bounds) = self.expect_bounds_change.take() + && scroll_position.y != 0. + { + scroll_position.y += (bounds.top() - last_bounds.top()) / line_height; + if scroll_position.y < 0. { + scroll_position.y = 0.; } } if scroll_position.y > max_scroll_top { scroll_position.y = max_scroll_top; } - if original_y != scroll_position.y { - self.set_scroll_position(scroll_position, window, cx); - } + let editor_was_scrolled = if original_y != scroll_position.y { + self.set_scroll_position(scroll_position, window, cx) + } else { + WasScrolled(false) + }; - let Some((autoscroll, local)) = self.scroll_manager.autoscroll_request.take() else { - return false; + let Some((autoscroll, local)) = autoscroll_request else { + return (NeedsHorizontalAutoscroll(false), editor_was_scrolled); }; let mut target_top; @@ -202,7 +213,7 @@ impl Editor { target_bottom = target_top + 1.; } - match strategy { + let was_autoscrolled = match strategy { AutoscrollStrategy::Fit | AutoscrollStrategy::Newest => { let margin = margin.min(self.scroll_manager.vertical_scroll_margin); let target_top = (target_top - margin).max(0.0); @@ -215,39 +226,42 @@ impl Editor { if needs_scroll_up && !needs_scroll_down { scroll_position.y = target_top; - self.set_scroll_position_internal(scroll_position, local, true, window, cx); - } - if !needs_scroll_up && needs_scroll_down { + } else if !needs_scroll_up && needs_scroll_down { scroll_position.y = target_bottom - visible_lines; - self.set_scroll_position_internal(scroll_position, local, true, window, cx); + } + + if needs_scroll_up ^ needs_scroll_down { + self.set_scroll_position_internal(scroll_position, local, true, window, cx) + } else { + WasScrolled(false) } } AutoscrollStrategy::Center => { scroll_position.y = (target_top - margin).max(0.0); - self.set_scroll_position_internal(scroll_position, local, true, window, cx); + self.set_scroll_position_internal(scroll_position, local, true, window, cx) } AutoscrollStrategy::Focused => { let margin = margin.min(self.scroll_manager.vertical_scroll_margin); scroll_position.y = (target_top - margin).max(0.0); - self.set_scroll_position_internal(scroll_position, local, true, window, cx); + self.set_scroll_position_internal(scroll_position, local, true, window, cx) } AutoscrollStrategy::Top => { scroll_position.y = (target_top).max(0.0); - self.set_scroll_position_internal(scroll_position, local, true, window, cx); + self.set_scroll_position_internal(scroll_position, local, true, window, cx) } AutoscrollStrategy::Bottom => { scroll_position.y = (target_bottom - visible_lines).max(0.0); - self.set_scroll_position_internal(scroll_position, local, true, window, cx); + self.set_scroll_position_internal(scroll_position, local, true, window, cx) } AutoscrollStrategy::TopRelative(lines) => { scroll_position.y = target_top - lines as f32; - self.set_scroll_position_internal(scroll_position, local, true, window, cx); + self.set_scroll_position_internal(scroll_position, local, true, window, cx) } AutoscrollStrategy::BottomRelative(lines) => { scroll_position.y = target_bottom + lines as f32; - self.set_scroll_position_internal(scroll_position, local, true, window, cx); + self.set_scroll_position_internal(scroll_position, local, true, window, cx) } - } + }; self.scroll_manager.last_autoscroll = Some(( self.scroll_manager.anchor.offset, @@ -256,7 +270,8 @@ impl Editor { strategy, )); - true + let was_scrolled = WasScrolled(editor_was_scrolled.0 || was_autoscrolled.0); + (NeedsHorizontalAutoscroll(true), was_scrolled) } pub(crate) fn autoscroll_horizontally( @@ -264,12 +279,17 @@ impl Editor { start_row: DisplayRow, viewport_width: Pixels, scroll_width: Pixels, - max_glyph_width: Pixels, + em_advance: Pixels, layouts: &[LineWithInvisibles], + autoscroll_request: Option<(Autoscroll, bool)>, + window: &mut Window, cx: &mut Context, - ) -> bool { + ) -> Option> { + let (_, local) = autoscroll_request?; + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let selections = self.selections.all::(cx); + let mut scroll_position = self.scroll_manager.scroll_position(&display_map); let mut target_left; let mut target_right; @@ -285,16 +305,17 @@ impl Editor { if head.row() >= start_row && head.row() < DisplayRow(start_row.0 + layouts.len() as u32) { - let start_column = head.column().saturating_sub(3); - let end_column = cmp::min(display_map.line_len(head.row()), head.column() + 3); + let start_column = head.column(); + let end_column = cmp::min(display_map.line_len(head.row()), head.column()); target_left = target_left.min( layouts[head.row().minus(start_row) as usize] - .x_for_index(start_column as usize), + .x_for_index(start_column as usize) + + self.gutter_dimensions.margin, ); target_right = target_right.max( layouts[head.row().minus(start_row) as usize] .x_for_index(end_column as usize) - + max_glyph_width, + + em_advance, ); } } @@ -306,20 +327,26 @@ impl Editor { target_right = target_right.min(scroll_width); if target_right - target_left > viewport_width { - return false; + return None; } - let scroll_left = self.scroll_manager.anchor.offset.x * max_glyph_width; + let scroll_left = self.scroll_manager.anchor.offset.x * em_advance; let scroll_right = scroll_left + viewport_width; - if target_left < scroll_left { - self.scroll_manager.anchor.offset.x = target_left / max_glyph_width; - true + let was_scrolled = if target_left < scroll_left { + scroll_position.x = target_left / em_advance; + self.set_scroll_position_internal(scroll_position, local, true, window, cx) } else if target_right > scroll_right { - self.scroll_manager.anchor.offset.x = (target_right - viewport_width) / max_glyph_width; - true + scroll_position.x = (target_right - viewport_width) / em_advance; + self.set_scroll_position_internal(scroll_position, local, true, window, cx) } else { - false + WasScrolled(false) + }; + + if was_scrolled.0 { + Some(scroll_position) + } else { + None } } diff --git a/crates/editor/src/scroll/scroll_amount.rs b/crates/editor/src/scroll/scroll_amount.rs index 0c0319b821..5992c9023c 100644 --- a/crates/editor/src/scroll/scroll_amount.rs +++ b/crates/editor/src/scroll/scroll_amount.rs @@ -5,6 +5,8 @@ use ui::{Pixels, px}; pub enum ScrollDirection { Upwards, Downwards, + Rightwards, + Leftwards, } impl ScrollDirection { @@ -19,6 +21,10 @@ pub enum ScrollAmount { Line(f32), // Scroll N pages (positive is towards the end of the document) Page(f32), + // Scroll N columns (positive is towards the right of the document) + Column(f32), + // Scroll N page width (positive is towards the right of the document) + PageWidth(f32), } impl ScrollAmount { @@ -32,6 +38,17 @@ impl ScrollAmount { } (visible_line_count * count).trunc() } + Self::Column(_count) => 0.0, + Self::PageWidth(_count) => 0.0, + } + } + + pub fn columns(&self, visible_column_count: f32) -> f32 { + match self { + Self::Line(_count) => 0.0, + Self::Page(_count) => 0.0, + Self::Column(count) => *count, + Self::PageWidth(count) => (visible_column_count * count).trunc(), } } @@ -39,20 +56,26 @@ impl ScrollAmount { match self { ScrollAmount::Line(x) => px(line_height.0 * x), ScrollAmount::Page(x) => px(height.0 * x), + // This function seems to only be leveraged by the popover that is + // displayed by the editor when, for example, viewing a function's + // documentation. Right now that only supports vertical scrolling, + // so I'm leaving this at 0.0 for now to try and make it clear that + // this should not have an impact on that? + ScrollAmount::Column(_) => px(0.0), + ScrollAmount::PageWidth(_) => px(0.0), } } pub fn is_full_page(&self) -> bool { - match self { - ScrollAmount::Page(count) if count.abs() == 1.0 => true, - _ => false, - } + matches!(self, ScrollAmount::Page(count) if count.abs() == 1.0) } pub fn direction(&self) -> ScrollDirection { match self { Self::Line(amount) if amount.is_sign_positive() => ScrollDirection::Downwards, Self::Page(amount) if amount.is_sign_positive() => ScrollDirection::Downwards, + Self::Column(amount) if amount.is_sign_positive() => ScrollDirection::Rightwards, + Self::Column(amount) if amount.is_sign_negative() => ScrollDirection::Leftwards, _ => ScrollDirection::Upwards, } } diff --git a/crates/editor/src/selections_collection.rs b/crates/editor/src/selections_collection.rs index cec720f9d6..0a02390b64 100644 --- a/crates/editor/src/selections_collection.rs +++ b/crates/editor/src/selections_collection.rs @@ -81,9 +81,9 @@ impl SelectionsCollection { count } - /// The non-pending, non-overlapping selections. There could still be a pending - /// selection that overlaps these if the mouse is being dragged, etc. Returned as - /// selections over Anchors. + /// The non-pending, non-overlapping selections. There could be a pending selection that + /// overlaps these if the mouse is being dragged, etc. This could also be empty if there is a + /// pending selection. Returned as selections over Anchors. pub fn disjoint_anchors(&self) -> Arc<[Selection]> { self.disjoint.clone() } @@ -94,6 +94,20 @@ impl SelectionsCollection { (0..disjoint.len()).map(move |ix| disjoint[ix].range()) } + /// Non-overlapping selections using anchors, including the pending selection. + pub fn all_anchors(&self, cx: &mut App) -> Arc<[Selection]> { + if self.pending.is_none() { + self.disjoint_anchors() + } else { + let all_offset_selections = self.all::(cx); + let buffer = self.buffer(cx); + all_offset_selections + .into_iter() + .map(|selection| selection_to_anchor_selection(selection, &buffer)) + .collect() + } + } + pub fn pending_anchor(&self) -> Option> { self.pending .as_ref() @@ -105,8 +119,8 @@ impl SelectionsCollection { cx: &mut App, ) -> Option> { let map = self.display_map(cx); - let selection = resolve_selections(self.pending_anchor().as_ref(), &map).next(); - selection + + resolve_selections(self.pending_anchor().as_ref(), &map).next() } pub(crate) fn pending_mode(&self) -> Option { @@ -262,18 +276,18 @@ impl SelectionsCollection { cx: &mut App, ) -> Selection { let map = self.display_map(cx); - let selection = resolve_selections([self.newest_anchor()], &map) + + resolve_selections([self.newest_anchor()], &map) .next() - .unwrap(); - selection + .unwrap() } pub fn newest_display(&self, cx: &mut App) -> Selection { let map = self.display_map(cx); - let selection = resolve_selections_display([self.newest_anchor()], &map) + + resolve_selections_display([self.newest_anchor()], &map) .next() - .unwrap(); - selection + .unwrap() } pub fn oldest_anchor(&self) -> &Selection { @@ -289,10 +303,10 @@ impl SelectionsCollection { cx: &mut App, ) -> Selection { let map = self.display_map(cx); - let selection = resolve_selections([self.oldest_anchor()], &map) + + resolve_selections([self.oldest_anchor()], &map) .next() - .unwrap(); - selection + .unwrap() } pub fn first_anchor(&self) -> Selection { @@ -411,7 +425,7 @@ impl<'a> MutableSelectionsCollection<'a> { self.collection.display_map(self.cx) } - pub fn buffer(&self) -> Ref { + pub fn buffer(&self) -> Ref<'_, MultiBufferSnapshot> { self.collection.buffer(self.cx) } @@ -534,21 +548,11 @@ impl<'a> MutableSelectionsCollection<'a> { } } - self.collection.disjoint = Arc::from_iter(selections.into_iter().map(|selection| { - let end_bias = if selection.end > selection.start { - Bias::Left - } else { - Bias::Right - }; - Selection { - id: selection.id, - start: buffer.anchor_after(selection.start), - end: buffer.anchor_at(selection.end, end_bias), - reversed: selection.reversed, - goal: selection.goal, - } - })); - + self.collection.disjoint = Arc::from_iter( + selections + .into_iter() + .map(|selection| selection_to_anchor_selection(selection, &buffer)), + ); self.collection.pending = None; self.selections_changed = true; } @@ -659,6 +663,7 @@ impl<'a> MutableSelectionsCollection<'a> { .collect(); self.select(selections); } + pub fn reverse_selections(&mut self) { let map = &self.display_map(); let mut new_selections: Vec> = Vec::new(); @@ -879,6 +884,27 @@ impl DerefMut for MutableSelectionsCollection<'_> { } } +fn selection_to_anchor_selection( + selection: Selection, + buffer: &MultiBufferSnapshot, +) -> Selection +where + T: ToOffset + Ord, +{ + let end_bias = if selection.end > selection.start { + Bias::Left + } else { + Bias::Right + }; + Selection { + id: selection.id, + start: buffer.anchor_after(selection.start), + end: buffer.anchor_at(selection.end, end_bias), + reversed: selection.reversed, + goal: selection.goal, + } +} + // Panics if passed selections are not in order fn resolve_selections_display<'a>( selections: impl 'a + IntoIterator>, diff --git a/crates/editor/src/signature_help.rs b/crates/editor/src/signature_help.rs index 9d69b10193..cb21f35d7e 100644 --- a/crates/editor/src/signature_help.rs +++ b/crates/editor/src/signature_help.rs @@ -1,18 +1,22 @@ use crate::actions::ShowSignatureHelp; -use crate::{Editor, EditorSettings, ToggleAutoSignatureHelp}; +use crate::hover_popover::open_markdown_url; +use crate::{Editor, EditorSettings, ToggleAutoSignatureHelp, hover_markdown_style}; use gpui::{ - App, Context, HighlightStyle, MouseButton, Size, StyledText, Task, TextStyle, Window, - combine_highlights, + App, Context, Div, Entity, HighlightStyle, MouseButton, ScrollHandle, Size, Stateful, + StyledText, Task, TextStyle, Window, combine_highlights, }; use language::BufferSnapshot; +use markdown::{Markdown, MarkdownElement}; use multi_buffer::{Anchor, ToOffset}; use settings::Settings; use std::ops::Range; use text::Rope; use theme::ThemeSettings; use ui::{ - ActiveTheme, AnyElement, InteractiveElement, IntoElement, ParentElement, Pixels, SharedString, - StatefulInteractiveElement, Styled, StyledExt, div, relative, + ActiveTheme, AnyElement, ButtonCommon, ButtonStyle, Clickable, FluentBuilder, IconButton, + IconButtonShape, IconName, IconSize, InteractiveElement, IntoElement, Label, LabelCommon, + LabelSize, ParentElement, Pixels, Scrollbar, ScrollbarState, SharedString, + StatefulInteractiveElement, Styled, StyledExt, div, px, relative, }; // Language-specific settings may define quotes as "brackets", so filter them out separately. @@ -37,15 +41,14 @@ impl Editor { .map(|auto_signature_help| !auto_signature_help) .or_else(|| Some(!EditorSettings::get_global(cx).auto_signature_help)); match self.auto_signature_help { - Some(auto_signature_help) if auto_signature_help => { + Some(true) => { self.show_signature_help(&ShowSignatureHelp, window, cx); } - Some(_) => { + Some(false) => { self.hide_signature_help(cx, SignatureHelpHiddenBy::AutoClose); } None => {} } - cx.notify(); } pub(super) fn hide_signature_help( @@ -54,7 +57,7 @@ impl Editor { signature_help_hidden_by: SignatureHelpHiddenBy, ) -> bool { if self.signature_help_state.is_shown() { - self.signature_help_state.kill_task(); + self.signature_help_state.task = None; self.signature_help_state.hide(signature_help_hidden_by); cx.notify(); true @@ -166,7 +169,7 @@ impl Editor { else { return; }; - let Some(lsp_store) = self.project.as_ref().map(|p| p.read(cx).lsp_store()) else { + let Some(lsp_store) = self.project().map(|p| p.read(cx).lsp_store()) else { return; }; let task = lsp_store.update(cx, |lsp_store, cx| { @@ -179,7 +182,9 @@ impl Editor { let signature_help = task.await; editor .update(cx, |editor, cx| { - let Some(mut signature_help) = signature_help.into_iter().next() else { + let Some(mut signature_help) = + signature_help.unwrap_or_default().into_iter().next() + else { editor .signature_help_state .hide(SignatureHelpHiddenBy::AutoClose); @@ -187,31 +192,62 @@ impl Editor { }; if let Some(language) = language { - let text = Rope::from(signature_help.label.clone()); - let highlights = language - .highlight_text(&text, 0..signature_help.label.len()) - .into_iter() - .flat_map(|(range, highlight_id)| { - Some((range, highlight_id.style(&cx.theme().syntax())?)) - }); - signature_help.highlights = - combine_highlights(signature_help.highlights, highlights).collect() + for signature in &mut signature_help.signatures { + let text = Rope::from(signature.label.as_ref()); + let highlights = language + .highlight_text(&text, 0..signature.label.len()) + .into_iter() + .flat_map(|(range, highlight_id)| { + Some((range, highlight_id.style(cx.theme().syntax())?)) + }); + signature.highlights = + combine_highlights(signature.highlights.clone(), highlights) + .collect(); + } } let settings = ThemeSettings::get_global(cx); - let text_style = TextStyle { + let style = TextStyle { color: cx.theme().colors().text, font_family: settings.buffer_font.family.clone(), font_fallbacks: settings.buffer_font.fallbacks.clone(), font_size: settings.buffer_font_size(cx).into(), font_weight: settings.buffer_font.weight, line_height: relative(settings.buffer_line_height.value()), - ..Default::default() + ..TextStyle::default() }; + let scroll_handle = ScrollHandle::new(); + let signatures = signature_help + .signatures + .into_iter() + .map(|s| SignatureHelp { + label: s.label, + documentation: s.documentation, + highlights: s.highlights, + active_parameter: s.active_parameter, + parameter_documentation: s + .active_parameter + .and_then(|idx| s.parameters.get(idx)) + .and_then(|param| param.documentation.clone()), + }) + .collect::>(); + + if signatures.is_empty() { + editor + .signature_help_state + .hide(SignatureHelpHiddenBy::AutoClose); + return; + } + + let current_signature = signature_help + .active_signature + .min(signatures.len().saturating_sub(1)); let signature_help_popover = SignatureHelpPopover { - label: signature_help.label.into(), - highlights: signature_help.highlights, - style: text_style, + scrollbar_state: ScrollbarState::new(scroll_handle.clone()), + style, + signatures, + current_signature, + scroll_handle, }; editor .signature_help_state @@ -231,15 +267,11 @@ pub struct SignatureHelpState { } impl SignatureHelpState { - pub fn set_task(&mut self, task: Task<()>) { + fn set_task(&mut self, task: Task<()>) { self.task = Some(task); self.hidden_by = None; } - pub fn kill_task(&mut self) { - self.task = None; - } - #[cfg(test)] pub fn popover(&self) -> Option<&SignatureHelpPopover> { self.popover.as_ref() @@ -249,25 +281,31 @@ impl SignatureHelpState { self.popover.as_mut() } - pub fn set_popover(&mut self, popover: SignatureHelpPopover) { + fn set_popover(&mut self, popover: SignatureHelpPopover) { self.popover = Some(popover); self.hidden_by = None; } - pub fn hide(&mut self, hidden_by: SignatureHelpHiddenBy) { + fn hide(&mut self, hidden_by: SignatureHelpHiddenBy) { if self.hidden_by.is_none() { self.popover = None; self.hidden_by = Some(hidden_by); } } - pub fn hidden_by_selection(&self) -> bool { + fn hidden_by_selection(&self) -> bool { self.hidden_by == Some(SignatureHelpHiddenBy::Selection) } pub fn is_shown(&self) -> bool { self.popover.is_some() } + + pub fn has_multiple_signatures(&self) -> bool { + self.popover + .as_ref() + .is_some_and(|popover| popover.signatures.len() > 1) + } } #[cfg(test)] @@ -278,28 +316,170 @@ impl SignatureHelpState { } #[derive(Clone, Debug, PartialEq)] +pub struct SignatureHelp { + pub(crate) label: SharedString, + documentation: Option>, + highlights: Vec<(Range, HighlightStyle)>, + active_parameter: Option, + parameter_documentation: Option>, +} + +#[derive(Clone, Debug)] pub struct SignatureHelpPopover { - pub label: SharedString, pub style: TextStyle, - pub highlights: Vec<(Range, HighlightStyle)>, + pub signatures: Vec, + pub current_signature: usize, + scroll_handle: ScrollHandle, + scrollbar_state: ScrollbarState, } impl SignatureHelpPopover { - pub fn render(&mut self, max_size: Size, cx: &mut Context) -> AnyElement { - div() - .id("signature_help_popover") - .elevation_2(cx) - .overflow_y_scroll() - .max_w(max_size.width) - .max_h(max_size.height) - .on_mouse_move(|_, _, cx| cx.stop_propagation()) - .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) + pub fn render( + &mut self, + max_size: Size, + window: &mut Window, + cx: &mut Context, + ) -> AnyElement { + let Some(signature) = self.signatures.get(self.current_signature) else { + return div().into_any_element(); + }; + + let main_content = div() + .occlude() + .p_2() .child( - div().px_2().py_0p5().child( - StyledText::new(self.label.clone()) - .with_default_highlights(&self.style, self.highlights.iter().cloned()), - ), + div() + .id("signature_help_container") + .overflow_y_scroll() + .max_w(max_size.width) + .max_h(max_size.height) + .track_scroll(&self.scroll_handle) + .child( + StyledText::new(signature.label.clone()).with_default_highlights( + &self.style, + signature.highlights.iter().cloned(), + ), + ) + .when_some( + signature.parameter_documentation.clone(), + |this, param_doc| { + this.child(div().h_px().bg(cx.theme().colors().border_variant).my_1()) + .child( + MarkdownElement::new( + param_doc, + hover_markdown_style(window, cx), + ) + .code_block_renderer(markdown::CodeBlockRenderer::Default { + copy_button: false, + border: false, + copy_button_on_hover: false, + }) + .on_url_click(open_markdown_url), + ) + }, + ) + .when_some(signature.documentation.clone(), |this, description| { + this.child(div().h_px().bg(cx.theme().colors().border_variant).my_1()) + .child( + MarkdownElement::new(description, hover_markdown_style(window, cx)) + .code_block_renderer(markdown::CodeBlockRenderer::Default { + copy_button: false, + border: false, + copy_button_on_hover: false, + }) + .on_url_click(open_markdown_url), + ) + }), ) + .child(self.render_vertical_scrollbar(cx)); + let controls = if self.signatures.len() > 1 { + let prev_button = IconButton::new("signature_help_prev", IconName::ChevronUp) + .shape(IconButtonShape::Square) + .style(ButtonStyle::Subtle) + .icon_size(IconSize::Small) + .tooltip(move |window, cx| { + ui::Tooltip::for_action( + "Previous Signature", + &crate::SignatureHelpPrevious, + window, + cx, + ) + }) + .on_click(cx.listener(|editor, _, window, cx| { + editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx); + })); + + let next_button = IconButton::new("signature_help_next", IconName::ChevronDown) + .shape(IconButtonShape::Square) + .style(ButtonStyle::Subtle) + .icon_size(IconSize::Small) + .tooltip(move |window, cx| { + ui::Tooltip::for_action("Next Signature", &crate::SignatureHelpNext, window, cx) + }) + .on_click(cx.listener(|editor, _, window, cx| { + editor.signature_help_next(&crate::SignatureHelpNext, window, cx); + })); + + let page = Label::new(format!( + "{}/{}", + self.current_signature + 1, + self.signatures.len() + )) + .size(LabelSize::Small); + + Some( + div() + .flex() + .flex_col() + .items_center() + .gap_0p5() + .px_0p5() + .py_0p5() + .children([ + prev_button.into_any_element(), + div().child(page).into_any_element(), + next_button.into_any_element(), + ]) + .into_any_element(), + ) + } else { + None + }; + div() + .elevation_2(cx) + .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) + .on_mouse_move(|_, _, cx| cx.stop_propagation()) + .flex() + .flex_row() + .when_some(controls, |this, controls| { + this.children(vec![ + div().flex().items_end().child(controls), + div().w_px().bg(cx.theme().colors().border_variant), + ]) + }) + .child(main_content) .into_any_element() } + + fn render_vertical_scrollbar(&self, cx: &mut Context) -> Stateful
{ + div() + .occlude() + .id("signature_help_scrollbar") + .on_mouse_move(cx.listener(|_, _, _, cx| { + cx.notify(); + cx.stop_propagation() + })) + .on_hover(|_, _, cx| cx.stop_propagation()) + .on_any_mouse_down(|_, _, cx| cx.stop_propagation()) + .on_mouse_up(MouseButton::Left, |_, _, cx| cx.stop_propagation()) + .on_scroll_wheel(cx.listener(|_, _, _, cx| cx.notify())) + .h_full() + .absolute() + .right_1() + .top_1() + .bottom_1() + .w(px(12.)) + .cursor_default() + .children(Scrollbar::vertical(self.scrollbar_state.clone())) + } } diff --git a/crates/editor/src/tasks.rs b/crates/editor/src/tasks.rs index 0d497e4cac..8be2a3a2e1 100644 --- a/crates/editor/src/tasks.rs +++ b/crates/editor/src/tasks.rs @@ -89,7 +89,7 @@ impl Editor { .lsp_task_source()?; if lsp_settings .get(&lsp_tasks_source) - .map_or(true, |s| s.enable_lsp_tasks) + .is_none_or(|s| s.enable_lsp_tasks) { let buffer_id = buffer.read(cx).remote_id(); Some((lsp_tasks_source, buffer_id)) diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index f84db2990e..960fecf59a 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -5,7 +5,7 @@ use std::{rc::Rc, sync::LazyLock}; pub use crate::rust_analyzer_ext::expand_macro_recursively; use crate::{ - DisplayPoint, Editor, EditorMode, FoldPlaceholder, MultiBuffer, + DisplayPoint, Editor, EditorMode, FoldPlaceholder, MultiBuffer, SelectionEffects, display_map::{ Block, BlockPlacement, CustomBlockId, DisplayMap, DisplayRow, DisplaySnapshot, ToDisplayPoint, @@ -45,6 +45,7 @@ pub fn test_font() -> Font { } // Returns a snapshot from text containing '|' character markers with the markers removed, and DisplayPoints for each one. +#[track_caller] pub fn marked_display_snapshot( text: &str, cx: &mut gpui::App, @@ -52,7 +53,7 @@ pub fn marked_display_snapshot( let (unmarked_text, markers) = marked_text_offsets(text); let font = Font { - family: "Zed Plex Mono".into(), + family: ".ZedMono".into(), features: FontFeatures::default(), fallbacks: None, weight: FontWeight::default(), @@ -83,6 +84,7 @@ pub fn marked_display_snapshot( (snapshot, markers) } +#[track_caller] pub fn select_ranges( editor: &mut Editor, marked_text: &str, @@ -91,7 +93,9 @@ pub fn select_ranges( ) { let (unmarked_text, text_ranges) = marked_text_ranges(marked_text, true); assert_eq!(editor.text(cx), unmarked_text); - editor.change_selections(None, window, cx, |s| s.select_ranges(text_ranges)); + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges(text_ranges) + }); } #[track_caller] @@ -180,12 +184,12 @@ pub fn editor_content_with_blocks(editor: &Entity, cx: &mut VisualTestCo for (row, block) in blocks { match block { Block::Custom(custom_block) => { - if let BlockPlacement::Near(x) = &custom_block.placement { - if snapshot.intersects_fold(x.to_point(&snapshot.buffer_snapshot)) { - continue; - } + if let BlockPlacement::Near(x) = &custom_block.placement + && snapshot.intersects_fold(x.to_point(&snapshot.buffer_snapshot)) + { + continue; }; - let content = block_content_for_tests(&editor, custom_block.id, cx) + let content = block_content_for_tests(editor, custom_block.id, cx) .expect("block content not found"); // 2: "related info 1 for diagnostic 0" if let Some(height) = custom_block.height { @@ -226,26 +230,23 @@ pub fn editor_content_with_blocks(editor: &Entity, cx: &mut VisualTestCo lines[row as usize].push_str("§ -----"); } } - Block::ExcerptBoundary { - excerpt, - height, - starts_new_buffer, - } => { - if starts_new_buffer { - lines[row.0 as usize].push_str(&cx.update(|_, cx| { - format!( - "§ {}", - excerpt - .buffer - .file() - .unwrap() - .file_name(cx) - .to_string_lossy() - ) - })); - } else { - lines[row.0 as usize].push_str("§ -----") + Block::ExcerptBoundary { height, .. } => { + for row in row.0..row.0 + height { + lines[row as usize].push_str("§ -----"); } + } + Block::BufferHeader { excerpt, height } => { + lines[row.0 as usize].push_str(&cx.update(|_, cx| { + format!( + "§ {}", + excerpt + .buffer + .file() + .unwrap() + .file_name(cx) + .to_string_lossy() + ) + })); for row in row.0 + 1..row.0 + height { lines[row as usize].push_str("§ -----"); } diff --git a/crates/editor/src/test/editor_lsp_test_context.rs b/crates/editor/src/test/editor_lsp_test_context.rs index 1f625b4510..3f78fa2f3e 100644 --- a/crates/editor/src/test/editor_lsp_test_context.rs +++ b/crates/editor/src/test/editor_lsp_test_context.rs @@ -14,7 +14,8 @@ use futures::Future; use gpui::{Context, Entity, Focusable as _, VisualTestContext, Window}; use indoc::indoc; use language::{ - FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageQueries, point_to_lsp, + BlockCommentConfig, FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageQueries, + point_to_lsp, }; use lsp::{notification, request}; use multi_buffer::ToPointUtf16; @@ -169,6 +170,12 @@ impl EditorLspTestContext { .expect("Opened test file wasn't an editor") }); editor.update_in(&mut cx, |editor, window, cx| { + let nav_history = workspace + .read(cx) + .active_pane() + .read(cx) + .nav_history_for_item(&cx.entity()); + editor.set_nav_history(Some(nav_history)); window.focus(&editor.focus_handle(cx)) }); @@ -263,7 +270,12 @@ impl EditorLspTestContext { path_suffixes: vec!["html".into()], ..Default::default() }, - block_comment: Some(("".into())), + block_comment: Some(BlockCommentConfig { + start: "".into(), + tab_size: 0, + }), completion_query_characters: ['-'].into_iter().collect(), ..Default::default() }, @@ -288,6 +300,7 @@ impl EditorLspTestContext { self.to_lsp_range(ranges[0].clone()) } + #[expect(clippy::wrong_self_convention, reason = "This is test code")] pub fn to_lsp_range(&mut self, range: Range) -> lsp::Range { let snapshot = self.update_editor(|editor, window, cx| editor.snapshot(window, cx)); let start_point = range.start.to_point(&snapshot.buffer_snapshot); @@ -314,6 +327,7 @@ impl EditorLspTestContext { }) } + #[expect(clippy::wrong_self_convention, reason = "This is test code")] pub fn to_lsp(&mut self, offset: usize) -> lsp::Position { let snapshot = self.update_editor(|editor, window, cx| editor.snapshot(window, cx)); let point = offset.to_point(&snapshot.buffer_snapshot); @@ -345,7 +359,7 @@ impl EditorLspTestContext { T: 'static + request::Request, T::Params: 'static + Send, F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncApp) -> Fut, - Fut: 'static + Send + Future>, + Fut: 'static + Future>, { let url = self.buffer_lsp_url.clone(); self.lsp.set_request_handler::(move |params, cx| { diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index e8165e9c65..8c54c265ed 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -1,6 +1,6 @@ use crate::{ - AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer, RowExt, - display_map::ToDisplayPoint, + AnchorRangeExt, DisplayPoint, Editor, MultiBuffer, RowExt, + display_map::{HighlightKey, ToDisplayPoint}, }; use buffer_diff::DiffHunkStatusKind; use collections::BTreeMap; @@ -109,6 +109,7 @@ impl EditorTestContext { } } + #[track_caller] pub fn new_multibuffer( cx: &mut gpui::TestAppContext, excerpts: [&str; COUNT], @@ -118,13 +119,7 @@ impl EditorTestContext { for excerpt in excerpts.into_iter() { let (text, ranges) = marked_text_ranges(excerpt, false); let buffer = cx.new(|cx| Buffer::local(text, cx)); - multibuffer.push_excerpts( - buffer, - ranges - .into_iter() - .map(|range| ExcerptRange::new(range.clone())), - cx, - ); + multibuffer.push_excerpts(buffer, ranges.into_iter().map(ExcerptRange::new), cx); } multibuffer }); @@ -296,31 +291,29 @@ impl EditorTestContext { pub fn set_head_text(&mut self, diff_base: &str) { self.cx.run_until_parked(); - let fs = self.update_editor(|editor, _, cx| { - editor.project.as_ref().unwrap().read(cx).fs().as_fake() - }); + let fs = + self.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).fs().as_fake()); let path = self.update_buffer(|buffer, _| buffer.file().unwrap().path().clone()); fs.set_head_for_repo( &Self::root_path().join(".git"), &[(path.into(), diff_base.to_string())], + "deadbeef", ); self.cx.run_until_parked(); } pub fn clear_index_text(&mut self) { self.cx.run_until_parked(); - let fs = self.update_editor(|editor, _, cx| { - editor.project.as_ref().unwrap().read(cx).fs().as_fake() - }); + let fs = + self.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).fs().as_fake()); fs.set_index_for_repo(&Self::root_path().join(".git"), &[]); self.cx.run_until_parked(); } pub fn set_index_text(&mut self, diff_base: &str) { self.cx.run_until_parked(); - let fs = self.update_editor(|editor, _, cx| { - editor.project.as_ref().unwrap().read(cx).fs().as_fake() - }); + let fs = + self.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).fs().as_fake()); let path = self.update_buffer(|buffer, _| buffer.file().unwrap().path().clone()); fs.set_index_for_repo( &Self::root_path().join(".git"), @@ -331,9 +324,8 @@ impl EditorTestContext { #[track_caller] pub fn assert_index_text(&mut self, expected: Option<&str>) { - let fs = self.update_editor(|editor, _, cx| { - editor.project.as_ref().unwrap().read(cx).fs().as_fake() - }); + let fs = + self.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).fs().as_fake()); let path = self.update_buffer(|buffer, _| buffer.file().unwrap().path().clone()); let mut found = None; fs.with_git_state(&Self::root_path().join(".git"), false, |git_state| { @@ -351,6 +343,7 @@ impl EditorTestContext { /// editor state was needed to cause the failure. /// /// See the `util::test::marked_text_ranges` function for more information. + #[track_caller] pub fn set_state(&mut self, marked_text: &str) -> ContextHandle { let state_context = self.add_assertion_context(format!( "Initial Editor State: \"{}\"", @@ -359,7 +352,7 @@ impl EditorTestContext { let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true); self.editor.update_in(&mut self.cx, |editor, window, cx| { editor.set_text(unmarked_text, window, cx); - editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + editor.change_selections(Default::default(), window, cx, |s| { s.select_ranges(selection_ranges) }) }); @@ -367,6 +360,7 @@ impl EditorTestContext { } /// Only change the editor's selections + #[track_caller] pub fn set_selections_state(&mut self, marked_text: &str) -> ContextHandle { let state_context = self.add_assertion_context(format!( "Initial Editor State: \"{}\"", @@ -375,7 +369,7 @@ impl EditorTestContext { let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true); self.editor.update_in(&mut self.cx, |editor, window, cx| { assert_eq!(editor.text(cx), unmarked_text); - editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + editor.change_selections(Default::default(), window, cx, |s| { s.select_ranges(selection_ranges) }) }); @@ -426,7 +420,7 @@ impl EditorTestContext { if expected_text == "[FOLDED]\n" { assert!(is_folded, "excerpt {} should be folded", ix); let is_selected = selections.iter().any(|s| s.head().excerpt_id == excerpt_id); - if expected_selections.len() > 0 { + if !expected_selections.is_empty() { assert!( is_selected, "excerpt {ix} should be selected. got {:?}", @@ -505,7 +499,7 @@ impl EditorTestContext { let snapshot = editor.snapshot(window, cx); editor .background_highlights - .get(&TypeId::of::()) + .get(&HighlightKey::Type(TypeId::of::())) .map(|h| h.1.clone()) .unwrap_or_default() .iter() @@ -532,7 +526,9 @@ impl EditorTestContext { #[track_caller] pub fn assert_editor_selections(&mut self, expected_selections: Vec>) { let expected_marked_text = - generate_marked_text(&self.buffer_text(), &expected_selections, true); + generate_marked_text(&self.buffer_text(), &expected_selections, true) + .replace(" \n", "•\n"); + self.assert_selections(expected_selections, expected_marked_text) } @@ -561,7 +557,8 @@ impl EditorTestContext { ) { let actual_selections = self.editor_selections(); let actual_marked_text = - generate_marked_text(&self.buffer_text(), &actual_selections, true); + generate_marked_text(&self.buffer_text(), &actual_selections, true) + .replace(" \n", "•\n"); if expected_selections != actual_selections { pretty_assertions::assert_eq!( actual_marked_text, diff --git a/crates/eval/Cargo.toml b/crates/eval/Cargo.toml index 1dff8ad7b6..a0214c76a1 100644 --- a/crates/eval/Cargo.toml +++ b/crates/eval/Cargo.toml @@ -20,19 +20,20 @@ path = "src/explorer.rs" [dependencies] agent.workspace = true agent_settings.workspace = true +agent_ui.workspace = true anyhow.workspace = true assistant_tool.workspace = true assistant_tools.workspace = true async-trait.workspace = true -async-watch.workspace = true buffer_diff.workspace = true chrono.workspace = true clap.workspace = true client.workspace = true +cloud_llm_client.workspace = true collections.workspace = true debug_adapter_extension.workspace = true dirs.workspace = true -dotenv.workspace = true +dotenvy.workspace = true env_logger.workspace = true extension.workspace = true fs.workspace = true @@ -66,5 +67,5 @@ toml.workspace = true unindent.workspace = true util.workspace = true uuid.workspace = true +watch.workspace = true workspace-hack.workspace = true -zed_llm_client.workspace = true diff --git a/crates/eval/build.rs b/crates/eval/build.rs new file mode 100644 index 0000000000..9ab40da0fb --- /dev/null +++ b/crates/eval/build.rs @@ -0,0 +1,14 @@ +fn main() { + let cargo_toml = + std::fs::read_to_string("../zed/Cargo.toml").expect("Failed to read crates/zed/Cargo.toml"); + let version = cargo_toml + .lines() + .find(|line| line.starts_with("version = ")) + .expect("Version not found in crates/zed/Cargo.toml") + .split('=') + .nth(1) + .expect("Invalid version format") + .trim() + .trim_matches('"'); + println!("cargo:rustc-env=ZED_PKG_VERSION={}", version); +} diff --git a/crates/eval/src/assertions.rs b/crates/eval/src/assertions.rs index 489e4aa22e..01fac186d3 100644 --- a/crates/eval/src/assertions.rs +++ b/crates/eval/src/assertions.rs @@ -54,7 +54,7 @@ impl AssertionsReport { pub fn passed_count(&self) -> usize { self.ran .iter() - .filter(|a| a.result.as_ref().map_or(false, |result| result.passed)) + .filter(|a| a.result.as_ref().is_ok_and(|result| result.passed)) .count() } diff --git a/crates/eval/src/eval.rs b/crates/eval/src/eval.rs index 41dbe25d96..9e0504abca 100644 --- a/crates/eval/src/eval.rs +++ b/crates/eval/src/eval.rs @@ -8,6 +8,7 @@ mod tool_metrics; use assertions::{AssertionsReport, display_error_row}; use instance::{ExampleInstance, JudgeOutput, RunOutput, run_git}; +use language_extension::LspAccess; pub(crate) use tool_metrics::*; use ::fs::RealFs; @@ -17,7 +18,7 @@ use collections::{HashMap, HashSet}; use extension::ExtensionHostProxy; use futures::future; use gpui::http_client::read_proxy_from_env; -use gpui::{App, AppContext, Application, AsyncApp, Entity, SemanticVersion, UpdateGlobal}; +use gpui::{App, AppContext, Application, AsyncApp, Entity, UpdateGlobal}; use gpui_tokio::Tokio; use language::LanguageRegistry; use language_model::{ConfiguredModel, LanguageModel, LanguageModelRegistry, SelectedModel}; @@ -63,7 +64,7 @@ struct Args { } fn main() { - dotenv::from_filename(CARGO_MANIFEST_DIR.join(".env")).ok(); + dotenvy::from_filename(CARGO_MANIFEST_DIR.join(".env")).ok(); env_logger::init(); @@ -102,7 +103,7 @@ fn main() { let languages: HashSet = args.languages.into_iter().collect(); let http_client = Arc::new(ReqwestClient::new()); - let app = Application::headless().with_http_client(http_client.clone()); + let app = Application::headless().with_http_client(http_client); let all_threads = examples::all(&examples_dir); app.run(move |cx| { @@ -111,7 +112,7 @@ fn main() { let telemetry = app_state.client.telemetry(); telemetry.start(system_id, installation_id, session_id, cx); - let enable_telemetry = env::var("ZED_EVAL_TELEMETRY").map_or(false, |value| value == "1") + let enable_telemetry = env::var("ZED_EVAL_TELEMETRY").is_ok_and(|value| value == "1") && telemetry.has_checksum_seed(); if enable_telemetry { println!("Telemetry enabled"); @@ -166,15 +167,14 @@ fn main() { continue; } - if let Some(language) = meta.language_server { - if !languages.contains(&language.file_extension) { + if let Some(language) = meta.language_server + && !languages.contains(&language.file_extension) { panic!( "Eval for {:?} could not be run because no language server was found for extension {:?}", meta.name, language.file_extension ); } - } // TODO: This creates a worktree per repetition. Ideally these examples should // either be run sequentially on the same worktree, or reuse worktrees when there @@ -336,7 +336,8 @@ pub struct AgentAppState { } pub fn init(cx: &mut App) -> Arc { - release_channel::init(SemanticVersion::default(), cx); + let app_version = AppVersion::load(env!("ZED_PKG_VERSION")); + release_channel::init(app_version, cx); gpui_tokio::init(cx); let mut settings_store = SettingsStore::new(cx); @@ -348,8 +349,8 @@ pub fn init(cx: &mut App) -> Arc { // Set User-Agent so we can download language servers from GitHub let user_agent = format!( - "Zed/{} ({}; {})", - AppVersion::global(cx), + "Zed Agent Eval/{} ({}; {})", + app_version, std::env::consts::OS, std::env::consts::ARCH ); @@ -385,7 +386,7 @@ pub fn init(cx: &mut App) -> Arc { extension::init(cx); - let (tx, rx) = async_watch::channel(None); + let (mut tx, rx) = watch::channel(None); cx.observe_global::(move |cx| { let settings = &ProjectSettings::get_global(cx).node; let options = NodeBinaryOptions { @@ -415,15 +416,15 @@ pub fn init(cx: &mut App) -> Arc { language::init(cx); debug_adapter_extension::init(extension_host_proxy.clone(), cx); - language_extension::init(extension_host_proxy.clone(), languages.clone()); + language_extension::init(LspAccess::Noop, extension_host_proxy, languages.clone()); language_model::init(client.clone(), cx); - language_models::init(user_store.clone(), client.clone(), fs.clone(), cx); + language_models::init(user_store.clone(), client.clone(), cx); languages::init(languages.clone(), node_runtime.clone(), cx); prompt_store::init(cx); terminal_view::init(cx); let stdout_is_a_pty = false; let prompt_builder = PromptBuilder::load(fs.clone(), stdout_is_a_pty, cx); - agent::init( + agent_ui::init( fs.clone(), client.clone(), prompt_builder.clone(), @@ -514,7 +515,7 @@ async fn judge_example( enable_telemetry: bool, cx: &AsyncApp, ) -> JudgeOutput { - let judge_output = example.judge(model.clone(), &run_output, cx).await; + let judge_output = example.judge(model.clone(), run_output, cx).await; if enable_telemetry { telemetry::event!( @@ -525,7 +526,7 @@ async fn judge_example( example_name = example.name.clone(), example_repetition = example.repetition, diff_evaluation = judge_output.diff.clone(), - thread_evaluation = judge_output.thread.clone(), + thread_evaluation = judge_output.thread, tool_metrics = run_output.tool_metrics, response_count = run_output.response_count, token_usage = run_output.token_usage, @@ -705,7 +706,7 @@ fn print_report( println!("Average thread score: {average_thread_score}%"); } - println!(""); + println!(); print_h2("CUMULATIVE TOOL METRICS"); println!("{}", cumulative_tool_metrics); diff --git a/crates/eval/src/example.rs b/crates/eval/src/example.rs index 5615179036..457b62e98c 100644 --- a/crates/eval/src/example.rs +++ b/crates/eval/src/example.rs @@ -15,11 +15,11 @@ use agent_settings::AgentProfileId; use anyhow::{Result, anyhow}; use async_trait::async_trait; use buffer_diff::DiffHunkStatus; +use cloud_llm_client::CompletionIntent; use collections::HashMap; use futures::{FutureExt as _, StreamExt, channel::mpsc, select_biased}; use gpui::{App, AppContext, AsyncApp, Entity}; use language_model::{LanguageModel, Role, StopReason}; -use zed_llm_client::CompletionIntent; pub const THREAD_EVENT_TIMEOUT: Duration = Duration::from_secs(60 * 2); @@ -64,7 +64,7 @@ impl ExampleMetadata { self.url .split('/') .next_back() - .unwrap_or(&"") + .unwrap_or("") .trim_end_matches(".git") .into() } @@ -100,7 +100,7 @@ impl ExampleContext { pub fn new( meta: ExampleMetadata, log_prefix: String, - agent_thread: Entity, + agent_thread: Entity, model: Arc, app: AsyncApp, ) -> Self { @@ -246,6 +246,7 @@ impl ExampleContext { | ThreadEvent::StreamedAssistantThinking(_, _) | ThreadEvent::UsePendingTools { .. } | ThreadEvent::CompletionCanceled => {} + ThreadEvent::ToolUseLimitReached => {} ThreadEvent::ToolFinished { tool_use_id, pending_tool_use, @@ -254,7 +255,7 @@ impl ExampleContext { thread.update(cx, |thread, _cx| { if let Some(tool_use) = pending_tool_use { let mut tool_metrics = tool_metrics.lock().unwrap(); - if let Some(tool_result) = thread.tool_result(&tool_use_id) { + if let Some(tool_result) = thread.tool_result(tool_use_id) { let message = if tool_result.is_error { format!("✖︎ {}", tool_use.name) } else { @@ -293,6 +294,7 @@ impl ExampleContext { | ThreadEvent::MessageDeleted(_) | ThreadEvent::SummaryChanged | ThreadEvent::SummaryGenerated + | ThreadEvent::ProfileChanged | ThreadEvent::ReceivedTextChunk | ThreadEvent::StreamedToolUse { .. } | ThreadEvent::CheckpointChanged @@ -333,7 +335,7 @@ impl ExampleContext { for message in thread.messages().skip(message_count_before) { messages.push(Message { _role: message.role, - text: message.to_string(), + text: message.to_message_content(), tool_use: thread .tool_uses_for_message(message.id, cx) .into_iter() @@ -420,6 +422,13 @@ impl AppContext for ExampleContext { self.app.update_entity(handle, update) } + fn as_mut<'a, T>(&'a mut self, handle: &Entity) -> Self::Result> + where + T: 'static, + { + self.app.as_mut(handle) + } + fn read_entity( &self, handle: &Entity, diff --git a/crates/eval/src/examples/add_arg_to_trait_method.rs b/crates/eval/src/examples/add_arg_to_trait_method.rs index 9c538f9260..084f12bc62 100644 --- a/crates/eval/src/examples/add_arg_to_trait_method.rs +++ b/crates/eval/src/examples/add_arg_to_trait_method.rs @@ -70,10 +70,10 @@ impl Example for AddArgToTraitMethod { let path_str = format!("crates/assistant_tools/src/{}.rs", tool_name); let edits = edits.get(Path::new(&path_str)); - let ignored = edits.map_or(false, |edits| { + let ignored = edits.is_some_and(|edits| { edits.has_added_line(" _window: Option,\n") }); - let uningored = edits.map_or(false, |edits| { + let uningored = edits.is_some_and(|edits| { edits.has_added_line(" window: Option,\n") }); @@ -89,7 +89,7 @@ impl Example for AddArgToTraitMethod { let batch_tool_edits = edits.get(Path::new("crates/assistant_tools/src/batch_tool.rs")); cx.assert( - batch_tool_edits.map_or(false, |edits| { + batch_tool_edits.is_some_and(|edits| { edits.has_added_line(" window: Option,\n") }), "Argument: batch_tool", diff --git a/crates/eval/src/examples/file_change_notification.rs b/crates/eval/src/examples/file_change_notification.rs new file mode 100644 index 0000000000..7879ad6f2e --- /dev/null +++ b/crates/eval/src/examples/file_change_notification.rs @@ -0,0 +1,74 @@ +use agent_settings::AgentProfileId; +use anyhow::Result; +use async_trait::async_trait; + +use crate::example::{Example, ExampleContext, ExampleMetadata, JudgeAssertion}; + +pub struct FileChangeNotificationExample; + +#[async_trait(?Send)] +impl Example for FileChangeNotificationExample { + fn meta(&self) -> ExampleMetadata { + ExampleMetadata { + name: "file_change_notification".to_string(), + url: "https://github.com/octocat/hello-world".to_string(), + revision: "7fd1a60b01f91b314f59955a4e4d4e80d8edf11d".to_string(), + language_server: None, + max_assertions: None, + profile_id: AgentProfileId::default(), + existing_thread_json: None, + max_turns: Some(3), + } + } + + async fn conversation(&self, cx: &mut ExampleContext) -> Result<()> { + // Track README so that the model gets notified of its changes + let project_path = cx.agent_thread().read_with(cx, |thread, cx| { + thread + .project() + .read(cx) + .find_project_path("README", cx) + .expect("README file should exist in this repo") + })?; + + let buffer = { + cx.agent_thread() + .update(cx, |thread, cx| { + thread + .project() + .update(cx, |project, cx| project.open_buffer(project_path, cx)) + })? + .await? + }; + + cx.agent_thread().update(cx, |thread, cx| { + thread.action_log().update(cx, |action_log, cx| { + action_log.buffer_read(buffer.clone(), cx); + }); + })?; + + // Start conversation (specific message is not important) + cx.push_user_message("Find all files in this repo"); + cx.run_turn().await?; + + // Edit the README buffer - the model should get a notification on next turn + buffer.update(cx, |buffer, cx| { + buffer.edit([(0..buffer.len(), "Surprise!")], None, cx); + })?; + + // Run for some more turns. + // The model shouldn't thank us for letting it know about the file change. + cx.run_turns(3).await?; + + Ok(()) + } + + fn thread_assertions(&self) -> Vec { + vec![JudgeAssertion { + id: "change-file-notification".into(), + description: + "Agent should not acknowledge or mention anything about files that have been changed" + .into(), + }] + } +} diff --git a/crates/eval/src/examples/grep_params_escapement.rs b/crates/eval/src/examples/grep_params_escapement.rs new file mode 100644 index 0000000000..0532698ba2 --- /dev/null +++ b/crates/eval/src/examples/grep_params_escapement.rs @@ -0,0 +1,59 @@ +use agent_settings::AgentProfileId; +use anyhow::Result; +use assistant_tools::GrepToolInput; +use async_trait::async_trait; + +use crate::example::{Example, ExampleContext, ExampleMetadata}; + +pub struct GrepParamsEscapementExample; + +/* + +This eval checks that the model doesn't use HTML escapement for characters like `<` and +`>` in tool parameters. + + original +system_prompt change +tool description + claude-opus-4 89% 92% 97%+ + claude-sonnet-4 100% + gpt-4.1-mini 100% + gemini-2.5-pro 98% + +*/ + +#[async_trait(?Send)] +impl Example for GrepParamsEscapementExample { + fn meta(&self) -> ExampleMetadata { + ExampleMetadata { + name: "grep_params_escapement".to_string(), + url: "https://github.com/octocat/hello-world".to_string(), + revision: "7fd1a60b01f91b314f59955a4e4d4e80d8edf11d".to_string(), + language_server: None, + max_assertions: Some(1), + profile_id: AgentProfileId::default(), + existing_thread_json: None, + max_turns: Some(2), + } + } + + async fn conversation(&self, cx: &mut ExampleContext) -> Result<()> { + // cx.push_user_message("How does the precedence/specificity work with Keymap contexts? I am seeing that `MessageEditor > Editor` is lower precendence than `Editor` which is surprising to me, but might be how it works"); + cx.push_user_message("Search for files containing the characters `>` or `<`"); + let response = cx.run_turns(2).await?; + let grep_input = response + .find_tool_call("grep") + .and_then(|tool_use| tool_use.parse_input::().ok()); + + cx.assert_some(grep_input.as_ref(), "`grep` tool should be called")?; + + cx.assert( + !contains_html_entities(&grep_input.unwrap().regex), + "Tool parameters should not be escaped", + ) + } +} + +fn contains_html_entities(pattern: &str) -> bool { + regex::Regex::new(r"&[a-zA-Z]+;|&#[0-9]+;|&#x[0-9a-fA-F]+;") + .unwrap() + .is_match(pattern) +} diff --git a/crates/eval/src/examples/mod.rs b/crates/eval/src/examples/mod.rs index 5968ee2fd0..d74fbdb937 100644 --- a/crates/eval/src/examples/mod.rs +++ b/crates/eval/src/examples/mod.rs @@ -15,7 +15,9 @@ use crate::example::{Example, ExampleContext, ExampleMetadata, JudgeAssertion}; mod add_arg_to_trait_method; mod code_block_citations; mod comment_translation; +mod file_change_notification; mod file_search; +mod grep_params_escapement; mod overwrite_file; mod planets; @@ -27,6 +29,8 @@ pub fn all(examples_dir: &Path) -> Vec> { Rc::new(planets::Planets), Rc::new(comment_translation::CommentTranslation), Rc::new(overwrite_file::FileOverwriteExample), + Rc::new(file_change_notification::FileChangeNotificationExample), + Rc::new(grep_params_escapement::GrepParamsEscapementExample), ]; for example_path in list_declarative_examples(examples_dir).unwrap() { diff --git a/crates/eval/src/explorer.html b/crates/eval/src/explorer.html index fec4597163..04c41090d3 100644 --- a/crates/eval/src/explorer.html +++ b/crates/eval/src/explorer.html @@ -324,20 +324,8 @@

Thread Explorer

- - + + @@ -368,8 +352,7 @@ ← Previous
- Thread 1 of - 1: + Thread 1 of 1: Default Thread