diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 13dcf4fef1..7072a3fe94 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,7 +49,7 @@ jobs: run: cargo build -p collab - name: Build other binaries - run: cargo build --bins --all-features + run: cargo build --workspace --bins --all-features bundle: name: Bundle app diff --git a/.github/workflows/release_actions.yml b/.github/workflows/release_actions.yml index 65866baf7f..3db4f7b467 100644 --- a/.github/workflows/release_actions.yml +++ b/.github/workflows/release_actions.yml @@ -14,10 +14,10 @@ jobs: content: | 📣 Zed ${{ github.event.release.tag_name }} was just released! - Restart your Zed or head to https://zed.dev/releases to grab it. + Restart your Zed or head to https://zed.dev/releases/latest to grab it. ```md - ### Changelog + # Changelog ${{ github.event.release.body }} ``` diff --git a/Cargo.lock b/Cargo.lock index 590835a49b..d1b8a488f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1121,7 +1121,7 @@ dependencies = [ [[package]] name = "collab" -version = "0.2.2" +version = "0.2.5" dependencies = [ "anyhow", "async-tungstenite", @@ -3125,6 +3125,7 @@ dependencies = [ "text", "theme", "tree-sitter", + "tree-sitter-embedded-template", "tree-sitter-html", "tree-sitter-javascript", "tree-sitter-json 0.19.0", @@ -6729,8 +6730,8 @@ dependencies = [ [[package]] name = "tree-sitter" -version = "0.20.8" -source = "git+https://github.com/tree-sitter/tree-sitter?rev=366210ae925d7ea0891bc7a0c738f60c77c04d7b#366210ae925d7ea0891bc7a0c738f60c77c04d7b" +version = "0.20.9" +source = "git+https://github.com/tree-sitter/tree-sitter?rev=36b5b6c89e55ad1a502f8b3234bb3e12ec83a5da#36b5b6c89e55ad1a502f8b3234bb3e12ec83a5da" dependencies = [ "cc", "regex", @@ -6774,6 +6775,16 @@ dependencies = [ "tree-sitter", ] +[[package]] +name = "tree-sitter-embedded-template" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33817ade928c73a32d4f904a602321e09de9fc24b71d106f3b4b3f8ab30dcc38" +dependencies = [ + "cc", + "tree-sitter", +] + [[package]] name = "tree-sitter-go" version = "0.19.1" @@ -7100,6 +7111,7 @@ name = "util" version = "0.1.0" dependencies = [ "anyhow", + "backtrace", "futures 0.3.24", "git2", "lazy_static", @@ -7989,7 +8001,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.65.0" +version = "0.67.0" dependencies = [ "activity_indicator", "anyhow", @@ -8068,6 +8080,7 @@ dependencies = [ "tree-sitter-cpp", "tree-sitter-css", "tree-sitter-elixir", + "tree-sitter-embedded-template", "tree-sitter-go", "tree-sitter-html", "tree-sitter-json 0.20.0", diff --git a/Cargo.toml b/Cargo.toml index 03fcb4cfd9..5069e51cd5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,7 +65,7 @@ serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] } rand = { version = "0.8" } [patch.crates-io] -tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "366210ae925d7ea0891bc7a0c738f60c77c04d7b" } +tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "36b5b6c89e55ad1a502f8b3234bb3e12ec83a5da" } async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" } sqlx = { git = "https://github.com/launchbadge/sqlx", rev = "4b7053807c705df312bcb9b6281e184bf7534eb3" } diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 7c89eacde8..774d8fbc9e 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -472,6 +472,15 @@ "terminal::SendText", "\u0001" ], + // Terminal.app compatability + "alt-left": [ + "terminal::SendText", + "\u001bb" + ], + "alt-right": [ + "terminal::SendText", + "\u001bf" + ], // There are conflicting bindings for these keys in the global context. // these bindings override them, remove at your own risk: "up": [ diff --git a/assets/settings/default.json b/assets/settings/default.json index 3eb82e94c7..0487b11c19 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1,230 +1,233 @@ { - // The name of the Zed theme to use for the UI - "theme": "One Dark", - // The name of a font to use for rendering text in the editor - "buffer_font_family": "Zed Mono", - // The default font size for text in the editor - "buffer_font_size": 15, - // Whether to enable vim modes and key bindings - "vim_mode": false, - // Whether to show the informational hover box when moving the mouse - // over symbols in the editor. - "hover_popover_enabled": true, - // Whether the cursor blinks in the editor. - "cursor_blink": true, - // Whether to pop the completions menu while typing in an editor without - // explicitly requesting it. - "show_completions_on_input": true, - // Whether new projects should start out 'online'. Online projects - // appear in the contacts panel under your name, so that your contacts - // can see which projects you are working on. Regardless of this - // setting, projects keep their last online status when you reopen them. - "projects_online_by_default": true, - // Whether to use language servers to provide code intelligence. - "enable_language_server": true, - // When to automatically save edited buffers. This setting can - // take four values. - // - // 1. Never automatically save: - // "autosave": "off", - // 2. Save when changing focus away from the Zed window: - // "autosave": "on_window_change", - // 3. Save when changing focus away from a specific buffer: - // "autosave": "on_focus_change", - // 4. Save when idle for a certain amount of time: - // "autosave": { "after_delay": {"milliseconds": 500} }, - "autosave": "off", - // Where to place the dock by default. This setting can take three - // values: - // - // 1. Position the dock attached to the bottom of the workspace - // "default_dock_anchor": "bottom" - // 2. Position the dock to the right of the workspace like a side panel - // "default_dock_anchor": "right" - // 3. Position the dock full screen over the entire workspace" - // "default_dock_anchor": "expanded" - "default_dock_anchor": "right", - // Whether or not to perform a buffer format before saving - "format_on_save": "on", - // How to perform a buffer format. This setting can take two values: - // - // 1. Format code using the current language server: - // "format_on_save": "language_server" - // 2. Format code using an external command: - // "format_on_save": { - // "external": { - // "command": "prettier", - // "arguments": ["--stdin-filepath", "{buffer_path}"] - // } - // } - "formatter": "language_server", - // How to soft-wrap long lines of text. This setting can take - // three values: - // - // 1. Do not soft wrap. - // "soft_wrap": "none", - // 2. Soft wrap lines that overflow the editor: - // "soft_wrap": "editor_width", - // 3. Soft wrap lines at the preferred line length - // "soft_wrap": "preferred_line_length", - "soft_wrap": "none", - // The column at which to soft-wrap lines, for buffers where soft-wrap - // is enabled. - "preferred_line_length": 80, - // Whether to indent lines using tab characters, as opposed to multiple - // spaces. - "hard_tabs": false, - // How many columns a tab should occupy. - "tab_size": 4, - // Git gutter behavior configuration. - "git": { - // Control whether the git gutter is shown. May take 2 values: - // 1. Show the gutter - // "git_gutter": "tracked_files" - // 2. Hide the gutter - // "git_gutter": "hide" - "git_gutter": "tracked_files" - }, - // Settings specific to journaling - "journal": { - // The path of the directory where journal entries are stored - "path": "~", - // What format to display the hours in - // May take 2 values: - // 1. hour12 - // 2. hour24 - "hour_format": "hour12" - }, - // Settings specific to the terminal - "terminal": { - // What shell to use when opening a terminal. May take 3 values: - // 1. Use the system's default terminal configuration (e.g. $TERM). - // "shell": "system" - // 2. A program: - // "shell": { - // "program": "sh" - // } - // 3. A program with arguments: - // "shell": { - // "with_arguments": { - // "program": "/bin/bash", - // "arguments": ["--login"] - // } - // } - "shell": "system", - // What working directory to use when launching the terminal. - // May take 4 values: - // 1. Use the current file's project directory. Will Fallback to the - // first project directory strategy if unsuccessful - // "working_directory": "current_project_directory" - // 2. Use the first project in this workspace's directory - // "working_directory": "first_project_directory" - // 3. Always use this platform's home directory (if we can find it) - // "working_directory": "always_home" - // 4. Always use a specific directory. This value will be shell expanded. - // If this path is not a valid directory the terminal will default to - // this platform's home directory (if we can find it) - // "working_directory": { - // "always": { - // "directory": "~/zed/projects/" - // } - // } + // The name of the Zed theme to use for the UI + "theme": "One Dark", + // The name of a font to use for rendering text in the editor + "buffer_font_family": "Zed Mono", + // The default font size for text in the editor + "buffer_font_size": 15, + // The factor to grow the active pane by. Defaults to 1.0 + // which gives the same size as all other panes. + "active_pane_magnification": 1.0, + // Whether to enable vim modes and key bindings + "vim_mode": false, + // Whether to show the informational hover box when moving the mouse + // over symbols in the editor. + "hover_popover_enabled": true, + // Whether the cursor blinks in the editor. + "cursor_blink": true, + // Whether to pop the completions menu while typing in an editor without + // explicitly requesting it. + "show_completions_on_input": true, + // Whether new projects should start out 'online'. Online projects + // appear in the contacts panel under your name, so that your contacts + // can see which projects you are working on. Regardless of this + // setting, projects keep their last online status when you reopen them. + "projects_online_by_default": true, + // Whether to use language servers to provide code intelligence. + "enable_language_server": true, + // When to automatically save edited buffers. This setting can + // take four values. // + // 1. Never automatically save: + // "autosave": "off", + // 2. Save when changing focus away from the Zed window: + // "autosave": "on_window_change", + // 3. Save when changing focus away from a specific buffer: + // "autosave": "on_focus_change", + // 4. Save when idle for a certain amount of time: + // "autosave": { "after_delay": {"milliseconds": 500} }, + "autosave": "off", + // Where to place the dock by default. This setting can take three + // values: // - "working_directory": "current_project_directory", - // Set the cursor blinking behavior in the terminal. - // May take 4 values: - // 1. Never blink the cursor, ignoring the terminal mode - // "blinking": "off", - // 2. Default the cursor blink to off, but allow the terminal to - // set blinking - // "blinking": "terminal_controlled", - // 3. Always blink the cursor, ignoring the terminal mode - // "blinking": "on", - "blinking": "terminal_controlled", - // Set whether Alternate Scroll mode (code: ?1007) is active by default. - // Alternate Scroll mode converts mouse scroll events into up / down key - // presses when in the alternate screen (e.g. when running applications - // like vim or less). The terminal can still set and unset this mode. - // May take 2 values: - // 1. Default alternate scroll mode to on - // "alternate_scroll": "on", - // 2. Default alternate scroll mode to off - // "alternate_scroll": "off", - "alternate_scroll": "off", - // Set whether the option key behaves as the meta key. - // May take 2 values: - // 1. Rely on default platform handling of option key, on macOS - // this means generating certain unicode characters - // "option_to_meta": false, - // 2. Make the option keys behave as a 'meta' key, e.g. for emacs - // "option_to_meta": true, - "option_as_meta": false, - // Whether or not selecting text in the terminal will automatically - // copy to the system clipboard. - "copy_on_select": false, - // Any key-value pairs added to this list will be added to the terminal's - // enviroment. Use `:` to seperate multiple values. - "env": { - // "KEY": "value1:value2" - } - // Set the terminal's font size. If this option is not included, - // the terminal will default to matching the buffer's font size. - // "font_size": "15" - // Set the terminal's font family. If this option is not included, - // the terminal will default to matching the buffer's font family. - // "font_family": "Zed Mono" - }, - // Different settings for specific languages. - "languages": { - "Plain Text": { - "soft_wrap": "preferred_line_length" - }, - "C": { - "tab_size": 2 - }, - "C++": { - "tab_size": 2 - }, - "Elixir": { - "tab_size": 2 - }, - "Go": { - "tab_size": 4, - "hard_tabs": true - }, - "Markdown": { - "soft_wrap": "preferred_line_length" - }, - "Rust": { - "tab_size": 4 - }, - "JavaScript": { - "tab_size": 2 - }, - "TypeScript": { - "tab_size": 2 - }, - "TSX": { - "tab_size": 2 - } - }, - // LSP Specific settings. - "lsp": { - // Specify the LSP name as a key here. - // As of 8/10/22, supported LSPs are: - // pyright - // gopls - // rust-analyzer - // typescript-language-server - // vscode-json-languageserver - // "rust_analyzer": { - // //These initialization options are merged into Zed's defaults - // "initialization_options": { - // "checkOnSave": { - // "command": "clippy" - // } + // 1. Position the dock attached to the bottom of the workspace + // "default_dock_anchor": "bottom" + // 2. Position the dock to the right of the workspace like a side panel + // "default_dock_anchor": "right" + // 3. Position the dock full screen over the entire workspace" + // "default_dock_anchor": "expanded" + "default_dock_anchor": "right", + // Whether or not to perform a buffer format before saving + "format_on_save": "on", + // How to perform a buffer format. This setting can take two values: + // + // 1. Format code using the current language server: + // "format_on_save": "language_server" + // 2. Format code using an external command: + // "format_on_save": { + // "external": { + // "command": "prettier", + // "arguments": ["--stdin-filepath", "{buffer_path}"] + // } // } - // } - } + "formatter": "language_server", + // How to soft-wrap long lines of text. This setting can take + // three values: + // + // 1. Do not soft wrap. + // "soft_wrap": "none", + // 2. Soft wrap lines that overflow the editor: + // "soft_wrap": "editor_width", + // 3. Soft wrap lines at the preferred line length + // "soft_wrap": "preferred_line_length", + "soft_wrap": "none", + // The column at which to soft-wrap lines, for buffers where soft-wrap + // is enabled. + "preferred_line_length": 80, + // Whether to indent lines using tab characters, as opposed to multiple + // spaces. + "hard_tabs": false, + // How many columns a tab should occupy. + "tab_size": 4, + // Git gutter behavior configuration. + "git": { + // Control whether the git gutter is shown. May take 2 values: + // 1. Show the gutter + // "git_gutter": "tracked_files" + // 2. Hide the gutter + // "git_gutter": "hide" + "git_gutter": "tracked_files" + }, + // Settings specific to journaling + "journal": { + // The path of the directory where journal entries are stored + "path": "~", + // What format to display the hours in + // May take 2 values: + // 1. hour12 + // 2. hour24 + "hour_format": "hour12" + }, + // Settings specific to the terminal + "terminal": { + // What shell to use when opening a terminal. May take 3 values: + // 1. Use the system's default terminal configuration (e.g. $TERM). + // "shell": "system" + // 2. A program: + // "shell": { + // "program": "sh" + // } + // 3. A program with arguments: + // "shell": { + // "with_arguments": { + // "program": "/bin/bash", + // "arguments": ["--login"] + // } + // } + "shell": "system", + // What working directory to use when launching the terminal. + // May take 4 values: + // 1. Use the current file's project directory. Will Fallback to the + // first project directory strategy if unsuccessful + // "working_directory": "current_project_directory" + // 2. Use the first project in this workspace's directory + // "working_directory": "first_project_directory" + // 3. Always use this platform's home directory (if we can find it) + // "working_directory": "always_home" + // 4. Always use a specific directory. This value will be shell expanded. + // If this path is not a valid directory the terminal will default to + // this platform's home directory (if we can find it) + // "working_directory": { + // "always": { + // "directory": "~/zed/projects/" + // } + // } + // + // + "working_directory": "current_project_directory", + // Set the cursor blinking behavior in the terminal. + // May take 4 values: + // 1. Never blink the cursor, ignoring the terminal mode + // "blinking": "off", + // 2. Default the cursor blink to off, but allow the terminal to + // set blinking + // "blinking": "terminal_controlled", + // 3. Always blink the cursor, ignoring the terminal mode + // "blinking": "on", + "blinking": "terminal_controlled", + // Set whether Alternate Scroll mode (code: ?1007) is active by default. + // Alternate Scroll mode converts mouse scroll events into up / down key + // presses when in the alternate screen (e.g. when running applications + // like vim or less). The terminal can still set and unset this mode. + // May take 2 values: + // 1. Default alternate scroll mode to on + // "alternate_scroll": "on", + // 2. Default alternate scroll mode to off + // "alternate_scroll": "off", + "alternate_scroll": "off", + // Set whether the option key behaves as the meta key. + // May take 2 values: + // 1. Rely on default platform handling of option key, on macOS + // this means generating certain unicode characters + // "option_to_meta": false, + // 2. Make the option keys behave as a 'meta' key, e.g. for emacs + // "option_to_meta": true, + "option_as_meta": false, + // Whether or not selecting text in the terminal will automatically + // copy to the system clipboard. + "copy_on_select": false, + // Any key-value pairs added to this list will be added to the terminal's + // enviroment. Use `:` to seperate multiple values. + "env": { + // "KEY": "value1:value2" + } + // Set the terminal's font size. If this option is not included, + // the terminal will default to matching the buffer's font size. + // "font_size": "15" + // Set the terminal's font family. If this option is not included, + // the terminal will default to matching the buffer's font family. + // "font_family": "Zed Mono" + }, + // Different settings for specific languages. + "languages": { + "Plain Text": { + "soft_wrap": "preferred_line_length" + }, + "C": { + "tab_size": 2 + }, + "C++": { + "tab_size": 2 + }, + "Elixir": { + "tab_size": 2 + }, + "Go": { + "tab_size": 4, + "hard_tabs": true + }, + "Markdown": { + "soft_wrap": "preferred_line_length" + }, + "Rust": { + "tab_size": 4 + }, + "JavaScript": { + "tab_size": 2 + }, + "TypeScript": { + "tab_size": 2 + }, + "TSX": { + "tab_size": 2 + } + }, + // LSP Specific settings. + "lsp": { + // Specify the LSP name as a key here. + // As of 8/10/22, supported LSPs are: + // pyright + // gopls + // rust-analyzer + // typescript-language-server + // vscode-json-languageserver + // "rust_analyzer": { + // //These initialization options are merged into Zed's defaults + // "initialization_options": { + // "checkOnSave": { + // "command": "clippy" + // } + // } + // } + } } diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index d73523c8bd..bda45053b1 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -70,7 +70,14 @@ pub fn init(db: project::Db, http_client: Arc, cx: &mut MutableA } }); cx.add_global_action(move |_: &ViewReleaseNotes, cx| { - cx.platform().open_url(&format!("{server_url}/releases")); + let latest_release_url = if cx.has_global::() + && *cx.global::() == ReleaseChannel::Preview + { + format!("{server_url}/releases/preview/latest") + } else { + format!("{server_url}/releases/latest") + }; + cx.platform().open_url(&latest_release_url); }); cx.add_action(UpdateNotification::dismiss); } diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 11b9ef6117..4d29669c2f 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -150,7 +150,6 @@ impl UserStore { client.telemetry.set_authenticated_user_info(None, false); } - client.telemetry.report_event("sign in", Default::default()); current_user_tx.send(user).await.ok(); } } diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 66f426839c..2238be2257 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] default-run = "collab" edition = "2021" name = "collab" -version = "0.2.2" +version = "0.2.5" [[bin]] name = "collab" diff --git a/crates/collab/migrations/20221125192125_add_added_to_mailing_list_to_signups.sql b/crates/collab/migrations/20221125192125_add_added_to_mailing_list_to_signups.sql new file mode 100644 index 0000000000..b154396df1 --- /dev/null +++ b/crates/collab/migrations/20221125192125_add_added_to_mailing_list_to_signups.sql @@ -0,0 +1,2 @@ +ALTER TABLE "signups" + ADD "added_to_mailing_list" BOOLEAN NOT NULL DEFAULT FALSE; \ No newline at end of file diff --git a/crates/collab/src/api.rs b/crates/collab/src/api.rs index 4c1c60a04f..921b4189e8 100644 --- a/crates/collab/src/api.rs +++ b/crates/collab/src/api.rs @@ -338,7 +338,7 @@ async fn create_signup( Json(params): Json, Extension(app): Extension>, ) -> Result<()> { - app.db.create_signup(params).await?; + app.db.create_signup(¶ms).await?; Ok(()) } diff --git a/crates/collab/src/bin/seed.rs b/crates/collab/src/bin/seed.rs index 9860b8be84..5ddacf6d64 100644 --- a/crates/collab/src/bin/seed.rs +++ b/crates/collab/src/bin/seed.rs @@ -57,16 +57,14 @@ async fn main() { } } - let mut zed_user_ids = Vec::::new(); for (github_user, admin) in zed_users { - if let Some(user) = db + if db .get_user_by_github_account(&github_user.login, Some(github_user.id)) .await .expect("failed to fetch user") + .is_none() { - zed_user_ids.push(user.id); - } else if let Some(email) = &github_user.email { - zed_user_ids.push( + if let Some(email) = &github_user.email { db.create_user( email, admin, @@ -77,11 +75,8 @@ async fn main() { }, ) .await - .expect("failed to insert user") - .user_id, - ); - } else if admin { - zed_user_ids.push( + .expect("failed to insert user"); + } else if admin { db.create_user( &format!("{}@zed.dev", github_user.login), admin, @@ -92,9 +87,8 @@ async fn main() { }, ) .await - .expect("failed to insert user") - .user_id, - ); + .expect("failed to insert user"); + } } } } diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index ea9757a973..2a8163b9c8 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -660,9 +660,9 @@ impl Database { // signups - pub async fn create_signup(&self, signup: NewSignup) -> Result<()> { + pub async fn create_signup(&self, signup: &NewSignup) -> Result<()> { self.transact(|tx| async { - signup::ActiveModel { + signup::Entity::insert(signup::ActiveModel { email_address: ActiveValue::set(signup.email_address.clone()), email_confirmation_code: ActiveValue::set(random_email_confirmation_code()), email_confirmation_sent: ActiveValue::set(false), @@ -673,9 +673,15 @@ impl Database { editor_features: ActiveValue::set(Some(signup.editor_features.clone())), programming_languages: ActiveValue::set(Some(signup.programming_languages.clone())), device_id: ActiveValue::set(signup.device_id.clone()), + added_to_mailing_list: ActiveValue::set(signup.added_to_mailing_list), ..Default::default() - } - .insert(&tx) + }) + .on_conflict( + OnConflict::column(signup::Column::EmailAddress) + .update_column(signup::Column::EmailAddress) + .to_owned(), + ) + .exec(&tx) .await?; tx.commit().await?; Ok(()) @@ -746,6 +752,7 @@ impl Database { .or(signup::Column::PlatformUnknown.eq(true)), ), ) + .order_by_asc(signup::Column::CreatedAt) .limit(count as u64) .into_model() .all(&tx) @@ -772,32 +779,41 @@ impl Database { Err(anyhow!("email address is already in use"))?; } - let inviter = match user::Entity::find() - .filter(user::Column::InviteCode.eq(code)) + let inviting_user_with_invites = match user::Entity::find() + .filter( + user::Column::InviteCode + .eq(code) + .and(user::Column::InviteCount.gt(0)), + ) .one(&tx) .await? { - Some(inviter) => inviter, + Some(inviting_user) => inviting_user, None => { return Err(Error::Http( - StatusCode::NOT_FOUND, - "invite code not found".to_string(), + StatusCode::UNAUTHORIZED, + "unable to find an invite code with invites remaining".to_string(), ))? } }; - - if inviter.invite_count == 0 { - Err(Error::Http( - StatusCode::UNAUTHORIZED, - "no invites remaining".to_string(), - ))?; - } + user::Entity::update_many() + .filter( + user::Column::Id + .eq(inviting_user_with_invites.id) + .and(user::Column::InviteCount.gt(0)), + ) + .col_expr( + user::Column::InviteCount, + Expr::col(user::Column::InviteCount).sub(1), + ) + .exec(&tx) + .await?; let signup = signup::Entity::insert(signup::ActiveModel { email_address: ActiveValue::set(email_address.into()), email_confirmation_code: ActiveValue::set(random_email_confirmation_code()), email_confirmation_sent: ActiveValue::set(false), - inviting_user_id: ActiveValue::set(Some(inviter.id)), + inviting_user_id: ActiveValue::set(Some(inviting_user_with_invites.id)), platform_linux: ActiveValue::set(false), platform_mac: ActiveValue::set(false), platform_windows: ActiveValue::set(false), @@ -873,26 +889,6 @@ impl Database { let signup = signup.update(&tx).await?; if let Some(inviting_user_id) = signup.inviting_user_id { - let result = user::Entity::update_many() - .filter( - user::Column::Id - .eq(inviting_user_id) - .and(user::Column::InviteCount.gt(0)), - ) - .col_expr( - user::Column::InviteCount, - Expr::col(user::Column::InviteCount).sub(1), - ) - .exec(&tx) - .await?; - - if result.rows_affected == 0 { - Err(Error::Http( - StatusCode::UNAUTHORIZED, - "no invites remaining".to_string(), - ))?; - } - contact::Entity::insert(contact::ActiveModel { user_id_a: ActiveValue::set(inviting_user_id), user_id_b: ActiveValue::set(user.id), diff --git a/crates/collab/src/db/signup.rs b/crates/collab/src/db/signup.rs index 9857018a0c..ca219736a8 100644 --- a/crates/collab/src/db/signup.rs +++ b/crates/collab/src/db/signup.rs @@ -20,6 +20,7 @@ pub struct Model { pub platform_unknown: bool, pub editor_features: Option>, pub programming_languages: Option>, + pub added_to_mailing_list: bool, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] @@ -27,7 +28,7 @@ pub enum Relation {} impl ActiveModelBehavior for ActiveModel {} -#[derive(Debug, PartialEq, Eq, FromQueryResult, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, FromQueryResult, Serialize, Deserialize)] pub struct Invite { pub email_address: String, pub email_confirmation_code: String, @@ -42,6 +43,7 @@ pub struct NewSignup { pub editor_features: Vec, pub programming_languages: Vec, pub device_id: Option, + pub added_to_mailing_list: bool, } #[derive(Clone, Debug, PartialEq, Deserialize, Serialize, FromQueryResult)] diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs index b276bd5057..9e70ae4b05 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/tests.rs @@ -667,19 +667,29 @@ async fn test_signups() { let test_db = TestDb::postgres(build_background_executor()); let db = test_db.db(); - // people sign up on the waitlist - for i in 0..8 { - db.create_signup(NewSignup { - email_address: format!("person-{i}@example.com"), + let usernames = (0..8).map(|i| format!("person-{i}")).collect::>(); + + let all_signups = usernames + .iter() + .enumerate() + .map(|(i, username)| NewSignup { + email_address: format!("{username}@example.com"), platform_mac: true, platform_linux: i % 2 == 0, platform_windows: i % 4 == 0, editor_features: vec!["speed".into()], programming_languages: vec!["rust".into(), "c".into()], device_id: Some(format!("device_id_{i}")), + added_to_mailing_list: i != 0, // One user failed to subscribe }) - .await - .unwrap(); + .collect::>(); + + // people sign up on the waitlist + for signup in &all_signups { + // users can sign up multiple times without issues + for _ in 0..2 { + db.create_signup(&signup).await.unwrap(); + } } assert_eq!( @@ -702,9 +712,9 @@ async fn test_signups() { assert_eq!( addresses, &[ - "person-0@example.com", - "person-1@example.com", - "person-2@example.com" + all_signups[0].email_address.as_str(), + all_signups[1].email_address.as_str(), + all_signups[2].email_address.as_str() ] ); assert_ne!( @@ -728,9 +738,9 @@ async fn test_signups() { assert_eq!( addresses, &[ - "person-3@example.com", - "person-4@example.com", - "person-5@example.com" + all_signups[3].email_address.as_str(), + all_signups[4].email_address.as_str(), + all_signups[5].email_address.as_str() ] ); @@ -756,11 +766,10 @@ async fn test_signups() { } = db .create_user_from_invite( &Invite { - email_address: signups_batch1[0].email_address.clone(), - email_confirmation_code: signups_batch1[0].email_confirmation_code.clone(), + ..signups_batch1[0].clone() }, NewUserParams { - github_login: "person-0".into(), + github_login: usernames[0].clone(), github_user_id: 0, invite_count: 5, }, @@ -770,8 +779,11 @@ async fn test_signups() { .unwrap(); let user = db.get_user_by_id(user_id).await.unwrap().unwrap(); assert!(inviting_user_id.is_none()); - assert_eq!(user.github_login, "person-0"); - assert_eq!(user.email_address.as_deref(), Some("person-0@example.com")); + assert_eq!(user.github_login, usernames[0]); + assert_eq!( + user.email_address, + Some(all_signups[0].email_address.clone()) + ); assert_eq!(user.invite_count, 5); assert_eq!(signup_device_id.unwrap(), "device_id_0"); @@ -799,7 +811,7 @@ async fn test_signups() { email_confirmation_code: "the-wrong-code".to_string(), }, NewUserParams { - github_login: "person-1".into(), + github_login: usernames[1].clone(), github_user_id: 2, invite_count: 5, }, diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 225501c71d..73f450b833 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -5566,6 +5566,13 @@ async fn test_random_collaboration( guest_client.username, id ); + assert_eq!( + guest_snapshot.abs_path(), + host_snapshot.abs_path(), + "{} has different abs path than the host for worktree {}", + guest_client.username, + id + ); assert_eq!( guest_snapshot.entries(false).collect::>(), host_snapshot.entries(false).collect::>(), diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index b4fb6a503c..6ff7490181 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -322,7 +322,7 @@ impl ProjectDiagnosticsEditor { ); let excerpt_id = excerpts .insert_excerpts_after( - &prev_excerpt_id, + prev_excerpt_id, buffer.clone(), [ExcerptRange { context: excerpt_start..excerpt_end, @@ -384,7 +384,7 @@ impl ProjectDiagnosticsEditor { groups_to_add.push(group_state); } else if let Some((group_ix, group_state)) = to_remove { - excerpts.remove_excerpts(group_state.excerpts.iter(), excerpts_cx); + excerpts.remove_excerpts(group_state.excerpts.iter().copied(), excerpts_cx); group_ixs_to_remove.push(group_ix); blocks_to_remove.extend(group_state.blocks.iter().copied()); } else if let Some((_, group)) = to_keep { @@ -457,10 +457,15 @@ impl ProjectDiagnosticsEditor { } // If any selection has lost its position, move it to start of the next primary diagnostic. + let snapshot = editor.snapshot(cx); for selection in &mut selections { if let Some(new_excerpt_id) = new_excerpt_ids_by_selection_id.get(&selection.id) { let group_ix = match groups.binary_search_by(|probe| { - probe.excerpts.last().unwrap().cmp(new_excerpt_id) + probe + .excerpts + .last() + .unwrap() + .cmp(new_excerpt_id, &snapshot.buffer_snapshot) }) { Ok(ix) | Err(ix) => ix, }; @@ -738,7 +743,7 @@ mod tests { DisplayPoint, }; use gpui::TestAppContext; - use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16}; + use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16, Unclipped}; use serde_json::json; use unindent::Unindent as _; use workspace::AppState; @@ -788,7 +793,7 @@ mod tests { None, vec![ DiagnosticEntry { - range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9), + range: Unclipped(PointUtf16::new(1, 8))..Unclipped(PointUtf16::new(1, 9)), diagnostic: Diagnostic { message: "move occurs because `x` has type `Vec`, which does not implement the `Copy` trait" @@ -801,7 +806,7 @@ mod tests { }, }, DiagnosticEntry { - range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9), + range: Unclipped(PointUtf16::new(2, 8))..Unclipped(PointUtf16::new(2, 9)), diagnostic: Diagnostic { message: "move occurs because `y` has type `Vec`, which does not implement the `Copy` trait" @@ -814,7 +819,7 @@ mod tests { }, }, DiagnosticEntry { - range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7), + range: Unclipped(PointUtf16::new(3, 6))..Unclipped(PointUtf16::new(3, 7)), diagnostic: Diagnostic { message: "value moved here".to_string(), severity: DiagnosticSeverity::INFORMATION, @@ -825,7 +830,7 @@ mod tests { }, }, DiagnosticEntry { - range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7), + range: Unclipped(PointUtf16::new(4, 6))..Unclipped(PointUtf16::new(4, 7)), diagnostic: Diagnostic { message: "value moved here".to_string(), severity: DiagnosticSeverity::INFORMATION, @@ -836,7 +841,7 @@ mod tests { }, }, DiagnosticEntry { - range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7), + range: Unclipped(PointUtf16::new(7, 6))..Unclipped(PointUtf16::new(7, 7)), diagnostic: Diagnostic { message: "use of moved value\nvalue used here after move".to_string(), severity: DiagnosticSeverity::ERROR, @@ -847,7 +852,7 @@ mod tests { }, }, DiagnosticEntry { - range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7), + range: Unclipped(PointUtf16::new(8, 6))..Unclipped(PointUtf16::new(8, 7)), diagnostic: Diagnostic { message: "use of moved value\nvalue used here after move".to_string(), severity: DiagnosticSeverity::ERROR, @@ -939,7 +944,7 @@ mod tests { PathBuf::from("/test/consts.rs"), None, vec![DiagnosticEntry { - range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15), + range: Unclipped(PointUtf16::new(0, 15))..Unclipped(PointUtf16::new(0, 15)), diagnostic: Diagnostic { message: "mismatched types\nexpected `usize`, found `char`".to_string(), severity: DiagnosticSeverity::ERROR, @@ -1040,7 +1045,8 @@ mod tests { None, vec![ DiagnosticEntry { - range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15), + range: Unclipped(PointUtf16::new(0, 15)) + ..Unclipped(PointUtf16::new(0, 15)), diagnostic: Diagnostic { message: "mismatched types\nexpected `usize`, found `char`" .to_string(), @@ -1052,7 +1058,8 @@ mod tests { }, }, DiagnosticEntry { - range: PointUtf16::new(1, 15)..PointUtf16::new(1, 15), + range: Unclipped(PointUtf16::new(1, 15)) + ..Unclipped(PointUtf16::new(1, 15)), diagnostic: Diagnostic { message: "unresolved name `c`".to_string(), severity: DiagnosticSeverity::ERROR, diff --git a/crates/drag_and_drop/Cargo.toml b/crates/drag_and_drop/Cargo.toml index 2fd8ce27b8..4ab54ad8e6 100644 --- a/crates/drag_and_drop/Cargo.toml +++ b/crates/drag_and_drop/Cargo.toml @@ -12,4 +12,4 @@ collections = { path = "../collections" } gpui = { path = "../gpui" } [dev-dependencies] -gpui = { path = "../gpui", features = ["test-support"] } \ No newline at end of file +gpui = { path = "../gpui", features = ["test-support"] } diff --git a/crates/drag_and_drop/src/drag_and_drop.rs b/crates/drag_and_drop/src/drag_and_drop.rs index 65f5985edf..6884de7e20 100644 --- a/crates/drag_and_drop/src/drag_and_drop.rs +++ b/crates/drag_and_drop/src/drag_and_drop.rs @@ -4,12 +4,16 @@ use collections::HashSet; use gpui::{ elements::{Empty, MouseEventHandler, Overlay}, geometry::{rect::RectF, vector::Vector2F}, - scene::MouseDrag, + scene::{MouseDown, MouseDrag}, CursorStyle, Element, ElementBox, EventContext, MouseButton, MutableAppContext, RenderContext, View, WeakViewHandle, }; enum State { + Down { + region_offset: Vector2F, + region: RectF, + }, Dragging { window_id: usize, position: Vector2F, @@ -24,6 +28,13 @@ enum State { impl Clone for State { fn clone(&self) -> Self { match self { + &State::Down { + region_offset, + region, + } => State::Down { + region_offset, + region, + }, State::Dragging { window_id, position, @@ -87,6 +98,15 @@ impl DragAndDrop { }) } + pub fn drag_started(event: MouseDown, cx: &mut EventContext) { + cx.update_global(|this: &mut Self, _| { + this.currently_dragged = Some(State::Down { + region_offset: event.region.origin() - event.position, + region: event.region, + }); + }) + } + pub fn dragging( event: MouseDrag, payload: Rc, @@ -94,37 +114,32 @@ impl DragAndDrop { render: Rc) -> ElementBox>, ) { let window_id = cx.window_id(); - cx.update_global::(|this, cx| { + cx.update_global(|this: &mut Self, cx| { this.notify_containers_for_window(window_id, cx); - if matches!(this.currently_dragged, Some(State::Canceled)) { - return; + match this.currently_dragged.as_ref() { + Some(&State::Down { + region_offset, + region, + }) + | Some(&State::Dragging { + region_offset, + region, + .. + }) => { + this.currently_dragged = Some(State::Dragging { + window_id, + region_offset, + region, + position: event.position, + payload, + render: Rc::new(move |payload, cx| { + render(payload.downcast_ref::().unwrap(), cx) + }), + }); + } + _ => {} } - - let (region_offset, region) = if let Some(State::Dragging { - region_offset, - region, - .. - }) = this.currently_dragged.as_ref() - { - (*region_offset, *region) - } else { - ( - event.region.origin() - event.prev_mouse_position, - event.region, - ) - }; - - this.currently_dragged = Some(State::Dragging { - window_id, - region_offset, - region, - position: event.position, - payload, - render: Rc::new(move |payload, cx| { - render(payload.downcast_ref::().unwrap(), cx) - }), - }); }); } @@ -135,6 +150,7 @@ impl DragAndDrop { .clone() .and_then(|state| { match state { + State::Down { .. } => None, State::Dragging { window_id, region_offset, @@ -263,7 +279,11 @@ impl Draggable for MouseEventHandler { { let payload = Rc::new(payload); let render = Rc::new(render); - self.on_drag(MouseButton::Left, move |e, cx| { + self.on_down(MouseButton::Left, move |e, cx| { + cx.propagate_event(); + DragAndDrop::::drag_started(e, cx); + }) + .on_drag(MouseButton::Left, move |e, cx| { let payload = payload.clone(); let render = render.clone(); DragAndDrop::::dragging(e, payload, cx, render) diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index ee07c77d01..797f41c52a 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -2,7 +2,7 @@ use super::{ wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot}, TextHighlights, }; -use crate::{Anchor, ExcerptRange, ToPoint as _}; +use crate::{Anchor, ExcerptId, ExcerptRange, ToPoint as _}; use collections::{Bound, HashMap, HashSet}; use gpui::{ElementBox, RenderContext}; use language::{BufferSnapshot, Chunk, Patch, Point}; @@ -107,7 +107,7 @@ struct Transform { pub enum TransformBlock { Custom(Arc), ExcerptHeader { - key: usize, + id: ExcerptId, buffer: BufferSnapshot, range: ExcerptRange, height: u8, @@ -371,7 +371,7 @@ impl BlockMap { .make_wrap_point(Point::new(excerpt_boundary.row, 0), Bias::Left) .row(), TransformBlock::ExcerptHeader { - key: excerpt_boundary.key, + id: excerpt_boundary.id, buffer: excerpt_boundary.buffer, range: excerpt_boundary.range, height: if excerpt_boundary.starts_new_buffer { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index dd5934f979..5bbeed3fb5 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -73,6 +73,7 @@ use std::{ mem, num::NonZeroU32, ops::{Deref, DerefMut, Range, RangeInclusive}, + path::Path, sync::Arc, time::{Duration, Instant}, }; @@ -1161,7 +1162,7 @@ impl Editor { }); clone.selections.set_state(&self.selections); clone.scroll_position = self.scroll_position; - clone.scroll_top_anchor = self.scroll_top_anchor.clone(); + clone.scroll_top_anchor = self.scroll_top_anchor; clone.searchable = self.searchable; clone } @@ -1304,7 +1305,7 @@ impl Editor { display_snapshot: self.display_map.update(cx, |map, cx| map.snapshot(cx)), ongoing_scroll: self.ongoing_scroll, scroll_position: self.scroll_position, - scroll_top_anchor: self.scroll_top_anchor.clone(), + scroll_top_anchor: self.scroll_top_anchor, placeholder_text: self.placeholder_text.clone(), is_focused: self .handle @@ -1790,17 +1791,15 @@ impl Editor { .pending_anchor() .expect("extend_selection not called with pending selection"); if position >= tail { - pending_selection.start = tail_anchor.clone(); + pending_selection.start = tail_anchor; } else { - pending_selection.end = tail_anchor.clone(); + pending_selection.end = tail_anchor; pending_selection.reversed = true; } let mut pending_mode = self.selections.pending_mode().unwrap(); match &mut pending_mode { - SelectMode::Word(range) | SelectMode::Line(range) => { - *range = tail_anchor.clone()..tail_anchor - } + SelectMode::Word(range) | SelectMode::Line(range) => *range = tail_anchor..tail_anchor, _ => {} } @@ -2144,10 +2143,9 @@ impl Editor { )); if following_text_allows_autoclose && preceding_text_matches_prefix { let anchor = snapshot.anchor_before(selection.end); - new_selections - .push((selection.map(|_| anchor.clone()), text.len())); + new_selections.push((selection.map(|_| anchor), text.len())); new_autoclose_regions.push(( - anchor.clone(), + anchor, text.len(), selection.id, bracket_pair.clone(), @@ -2168,10 +2166,8 @@ impl Editor { && text.as_ref() == region.pair.end.as_str(); if should_skip { let anchor = snapshot.anchor_after(selection.end); - new_selections.push(( - selection.map(|_| anchor.clone()), - region.pair.end.len(), - )); + new_selections + .push((selection.map(|_| anchor), region.pair.end.len())); continue; } } @@ -2203,7 +2199,7 @@ impl Editor { // text with the given input and move the selection to the end of the // newly inserted text. let anchor = snapshot.anchor_after(selection.end); - new_selections.push((selection.map(|_| anchor.clone()), 0)); + new_selections.push((selection.map(|_| anchor), 0)); edits.push((selection.start..selection.end, text.clone())); } @@ -2305,7 +2301,7 @@ impl Editor { } let anchor = buffer.anchor_after(end); - let new_selection = selection.map(|_| anchor.clone()); + let new_selection = selection.map(|_| anchor); ( (start..end, new_text), (insert_extra_newline, new_selection), @@ -2385,7 +2381,7 @@ impl Editor { .iter() .map(|s| { let anchor = snapshot.anchor_after(s.end); - s.map(|_| anchor.clone()) + s.map(|_| anchor) }) .collect::>() }; @@ -3649,7 +3645,7 @@ impl Editor { String::new(), )); let insertion_anchor = buffer.anchor_after(insertion_point); - edits.push((insertion_anchor.clone()..insertion_anchor, text)); + edits.push((insertion_anchor..insertion_anchor, text)); let row_delta = range_to_move.start.row - insertion_point.row + 1; @@ -3754,7 +3750,7 @@ impl Editor { String::new(), )); let insertion_anchor = buffer.anchor_after(insertion_point); - edits.push((insertion_anchor.clone()..insertion_anchor, text)); + edits.push((insertion_anchor..insertion_anchor, text)); let row_delta = insertion_point.row - range_to_move.end.row + 1; @@ -4624,7 +4620,7 @@ impl Editor { cursor_anchor: position, cursor_position: point, scroll_position: self.scroll_position, - scroll_top_anchor: self.scroll_top_anchor.clone(), + scroll_top_anchor: self.scroll_top_anchor, scroll_top_row, }), cx, @@ -6536,15 +6532,13 @@ impl Editor { .as_singleton() .and_then(|b| b.read(cx).file()), ) { - project.read(cx).client().report_event( - name, - json!({ - "File Extension": file - .path() - .extension() - .and_then(|e| e.to_str()) - }), - ); + let extension = Path::new(file.file_name(cx)) + .extension() + .and_then(|e| e.to_str()); + project + .read(cx) + .client() + .report_event(name, json!({ "File Extension": extension })); } } } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 763917a464..8ac1f9a3fc 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -542,7 +542,7 @@ fn test_navigation_history(cx: &mut gpui::MutableAppContext) { // Set scroll position to check later editor.set_scroll_position(Vector2F::new(5.5, 5.5), cx); let original_scroll_position = editor.scroll_position; - let original_scroll_top_anchor = editor.scroll_top_anchor.clone(); + let original_scroll_top_anchor = editor.scroll_top_anchor; // Jump to the end of the document and adjust scroll editor.move_to_end(&MoveToEnd, cx); @@ -556,12 +556,12 @@ fn test_navigation_history(cx: &mut gpui::MutableAppContext) { assert_eq!(editor.scroll_top_anchor, original_scroll_top_anchor); // Ensure we don't panic when navigation data contains invalid anchors *and* points. - let mut invalid_anchor = editor.scroll_top_anchor.clone(); + let mut invalid_anchor = editor.scroll_top_anchor; invalid_anchor.text_anchor.buffer_id = Some(999); let invalid_point = Point::new(9999, 0); editor.navigate( Box::new(NavigationData { - cursor_anchor: invalid_anchor.clone(), + cursor_anchor: invalid_anchor, cursor_position: invalid_point, scroll_top_anchor: invalid_anchor, scroll_top_row: invalid_point.row, @@ -4146,14 +4146,26 @@ async fn test_completion(cx: &mut gpui::TestAppContext) { handle_resolve_completion_request( &mut cx, - Some(( - indoc! {" - one.second_completion - two - threeˇ - "}, - "\nadditional edit", - )), + Some(vec![ + ( + //This overlaps with the primary completion edit which is + //misbehavior from the LSP spec, test that we filter it out + indoc! {" + one.second_ˇcompletion + two + threeˇ + "}, + "overlapping aditional edit", + ), + ( + indoc! {" + one.second_completion + two + threeˇ + "}, + "\nadditional edit", + ), + ]), ) .await; apply_additional_edits.await.unwrap(); @@ -4303,19 +4315,24 @@ async fn test_completion(cx: &mut gpui::TestAppContext) { async fn handle_resolve_completion_request<'a>( cx: &mut EditorLspTestContext<'a>, - edit: Option<(&'static str, &'static str)>, + edits: Option>, ) { - let edit = edit.map(|(marked_string, new_text)| { - let (_, marked_ranges) = marked_text_ranges(marked_string, false); - let replace_range = cx.to_lsp_range(marked_ranges[0].clone()); - vec![lsp::TextEdit::new(replace_range, new_text.to_string())] + let edits = edits.map(|edits| { + edits + .iter() + .map(|(marked_string, new_text)| { + let (_, marked_ranges) = marked_text_ranges(marked_string, false); + let replace_range = cx.to_lsp_range(marked_ranges[0].clone()); + lsp::TextEdit::new(replace_range, new_text.to_string()) + }) + .collect::>() }); cx.handle_request::(move |_, _, _| { - let edit = edit.clone(); + let edits = edits.clone(); async move { Ok(lsp::CompletionItem { - additional_text_edits: edit, + additional_text_edits: edits, ..Default::default() }) } @@ -4701,9 +4718,7 @@ fn test_refresh_selections(cx: &mut gpui::MutableAppContext) { // Refreshing selections is a no-op when excerpts haven't changed. editor.update(cx, |editor, cx| { - editor.change_selections(None, cx, |s| { - s.refresh(); - }); + editor.change_selections(None, cx, |s| s.refresh()); assert_eq!( editor.selections.ranges(cx), [ @@ -4714,7 +4729,7 @@ fn test_refresh_selections(cx: &mut gpui::MutableAppContext) { }); multibuffer.update(cx, |multibuffer, cx| { - multibuffer.remove_excerpts([&excerpt1_id.unwrap()], cx); + multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx); }); editor.update(cx, |editor, cx| { // Removing an excerpt causes the first selection to become degenerate. @@ -4728,9 +4743,7 @@ fn test_refresh_selections(cx: &mut gpui::MutableAppContext) { // Refreshing selections will relocate the first selection to the original buffer // location. - editor.change_selections(None, cx, |s| { - s.refresh(); - }); + editor.change_selections(None, cx, |s| s.refresh()); assert_eq!( editor.selections.ranges(cx), [ @@ -4784,7 +4797,7 @@ fn test_refresh_selections_while_selecting_with_mouse(cx: &mut gpui::MutableAppC }); multibuffer.update(cx, |multibuffer, cx| { - multibuffer.remove_excerpts([&excerpt1_id.unwrap()], cx); + multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx); }); editor.update(cx, |editor, cx| { assert_eq!( @@ -4793,9 +4806,7 @@ fn test_refresh_selections_while_selecting_with_mouse(cx: &mut gpui::MutableAppC ); // Ensure we don't panic when selections are refreshed and that the pending selection is finalized. - editor.change_selections(None, cx, |s| { - s.refresh(); - }); + editor.change_selections(None, cx, |s| s.refresh()); assert_eq!( editor.selections.ranges(cx), [Point::new(0, 3)..Point::new(0, 3)] diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 25100037d7..8409786637 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -192,8 +192,14 @@ impl EditorElement { .on_scroll({ let position_map = position_map.clone(); move |e, cx| { - if !Self::scroll(e.position, e.delta, e.precise, &position_map, bounds, cx) - { + if !Self::scroll( + e.position, + *e.delta.raw(), + e.delta.precise(), + &position_map, + bounds, + cx, + ) { cx.propagate_event() } } @@ -1328,12 +1334,13 @@ impl EditorElement { }) } TransformBlock::ExcerptHeader { - key, + id, buffer, range, starts_new_buffer, .. } => { + let id = *id; let jump_icon = project::File::from_dyn(buffer.file()).map(|file| { let jump_position = range .primary @@ -1350,7 +1357,7 @@ impl EditorElement { enum JumpIcon {} cx.render(&editor, |_, cx| { - MouseEventHandler::::new(*key, cx, |state, _| { + MouseEventHandler::::new(id.into(), cx, |state, _| { let style = style.jump_icon.style_for(state, false); Svg::new("icons/arrow_up_right_8.svg") .with_color(style.color) @@ -1369,7 +1376,7 @@ impl EditorElement { cx.dispatch_action(jump_action.clone()) }) .with_tooltip::( - *key, + id.into(), "Jump to Buffer".to_string(), Some(Box::new(crate::OpenExcerpts)), tooltip_style.clone(), @@ -1600,16 +1607,13 @@ impl Element for EditorElement { highlighted_rows = view.highlighted_rows(); let theme = cx.global::().theme.as_ref(); - highlighted_ranges = view.background_highlights_in_range( - start_anchor.clone()..end_anchor.clone(), - &display_map, - theme, - ); + highlighted_ranges = + view.background_highlights_in_range(start_anchor..end_anchor, &display_map, theme); let mut remote_selections = HashMap::default(); for (replica_id, line_mode, cursor_shape, selection) in display_map .buffer_snapshot - .remote_selections_in_range(&(start_anchor.clone()..end_anchor.clone())) + .remote_selections_in_range(&(start_anchor..end_anchor)) { // The local selections match the leader's selections. if Some(replica_id) == view.leader_replica_id { diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index e4b4da68d3..7369b0a6f4 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -221,7 +221,7 @@ fn show_hover( start..end } else { - anchor.clone()..anchor.clone() + anchor..anchor }; Some(InfoPopover { diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index efcd8d67db..0cc8575e99 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -819,11 +819,20 @@ impl StatusItemView for CursorPosition { fn path_for_buffer<'a>( buffer: &ModelHandle, - mut height: usize, + height: usize, include_filename: bool, cx: &'a AppContext, ) -> Option> { let file = buffer.read(cx).as_singleton()?.read(cx).file()?; + path_for_file(file, height, include_filename, cx) +} + +fn path_for_file<'a>( + file: &'a dyn language::File, + mut height: usize, + include_filename: bool, + cx: &'a AppContext, +) -> Option> { // Ensure we always render at least the filename. height += 1; @@ -845,13 +854,82 @@ fn path_for_buffer<'a>( if include_filename { Some(full_path.into()) } else { - Some(full_path.parent().unwrap().to_path_buf().into()) + Some(full_path.parent()?.to_path_buf().into()) } } else { - let mut path = file.path().strip_prefix(prefix).unwrap(); + let mut path = file.path().strip_prefix(prefix).ok()?; if !include_filename { - path = path.parent().unwrap(); + path = path.parent()?; } Some(path.into()) } } + +#[cfg(test)] +mod tests { + use super::*; + use gpui::MutableAppContext; + use std::{ + path::{Path, PathBuf}, + sync::Arc, + }; + + #[gpui::test] + fn test_path_for_file(cx: &mut MutableAppContext) { + let file = TestFile { + path: Path::new("").into(), + full_path: PathBuf::from(""), + }; + assert_eq!(path_for_file(&file, 0, false, cx), None); + } + + struct TestFile { + path: Arc, + full_path: PathBuf, + } + + impl language::File for TestFile { + fn path(&self) -> &Arc { + &self.path + } + + fn full_path(&self, _: &gpui::AppContext) -> PathBuf { + self.full_path.clone() + } + + fn as_local(&self) -> Option<&dyn language::LocalFile> { + todo!() + } + + fn mtime(&self) -> std::time::SystemTime { + todo!() + } + + fn file_name<'a>(&'a self, _: &'a gpui::AppContext) -> &'a std::ffi::OsStr { + todo!() + } + + fn is_deleted(&self) -> bool { + todo!() + } + + fn save( + &self, + _: u64, + _: language::Rope, + _: clock::Global, + _: project::LineEnding, + _: &mut MutableAppContext, + ) -> gpui::Task> { + todo!() + } + + fn as_any(&self) -> &dyn std::any::Any { + todo!() + } + + fn to_proto(&self) -> rpc::proto::File { + todo!() + } + } +} diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 38475daf28..d758792e6c 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -11,7 +11,7 @@ use language::{ char_kind, AutoindentMode, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, CursorShape, DiagnosticEntry, Event, File, IndentSize, Language, OffsetRangeExt, OffsetUtf16, Outline, OutlineItem, Point, PointUtf16, Selection, TextDimension, ToOffset as _, ToOffsetUtf16 as _, - ToPoint as _, ToPointUtf16 as _, TransactionId, + ToPoint as _, ToPointUtf16 as _, TransactionId, Unclipped, }; use smallvec::SmallVec; use std::{ @@ -36,13 +36,13 @@ use util::post_inc; const NEWLINES: &[u8] = &[b'\n'; u8::MAX as usize]; -pub type ExcerptId = Locator; +#[derive(Debug, Default, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub struct ExcerptId(usize); pub struct MultiBuffer { snapshot: RefCell, buffers: RefCell>, - used_excerpt_ids: SumTree, - next_excerpt_key: usize, + next_excerpt_id: usize, subscriptions: Topic, singleton: bool, replica_id: ReplicaId, @@ -92,7 +92,7 @@ struct BufferState { last_diagnostics_update_count: usize, last_file_update_count: usize, last_git_diff_update_count: usize, - excerpts: Vec, + excerpts: Vec, _subscriptions: [gpui::Subscription; 2], } @@ -100,6 +100,7 @@ struct BufferState { pub struct MultiBufferSnapshot { singleton: bool, excerpts: SumTree, + excerpt_ids: SumTree, parse_count: usize, diagnostics_update_count: usize, trailing_excerpt_update_count: usize, @@ -111,7 +112,6 @@ pub struct MultiBufferSnapshot { pub struct ExcerptBoundary { pub id: ExcerptId, - pub key: usize, pub row: u32, pub buffer: BufferSnapshot, pub range: ExcerptRange, @@ -121,7 +121,7 @@ pub struct ExcerptBoundary { #[derive(Clone)] struct Excerpt { id: ExcerptId, - key: usize, + locator: Locator, buffer_id: usize, buffer: BufferSnapshot, range: ExcerptRange, @@ -130,6 +130,12 @@ struct Excerpt { has_trailing_newline: bool, } +#[derive(Clone, Debug)] +struct ExcerptIdMapping { + id: ExcerptId, + locator: Locator, +} + #[derive(Clone, Debug, Eq, PartialEq)] pub struct ExcerptRange { pub context: Range, @@ -139,6 +145,7 @@ pub struct ExcerptRange { #[derive(Clone, Debug, Default)] struct ExcerptSummary { excerpt_id: ExcerptId, + excerpt_locator: Locator, max_buffer_row: u32, text: TextSummary, } @@ -178,8 +185,7 @@ impl MultiBuffer { Self { snapshot: Default::default(), buffers: Default::default(), - used_excerpt_ids: Default::default(), - next_excerpt_key: Default::default(), + next_excerpt_id: 1, subscriptions: Default::default(), singleton: false, replica_id, @@ -218,8 +224,7 @@ impl MultiBuffer { Self { snapshot: RefCell::new(self.snapshot.borrow().clone()), buffers: RefCell::new(buffers), - used_excerpt_ids: self.used_excerpt_ids.clone(), - next_excerpt_key: self.next_excerpt_key, + next_excerpt_id: 1, subscriptions: Default::default(), singleton: self.singleton, replica_id: self.replica_id, @@ -610,11 +615,14 @@ impl MultiBuffer { let mut selections_by_buffer: HashMap>> = Default::default(); let snapshot = self.read(cx); - let mut cursor = snapshot.excerpts.cursor::>(); + let mut cursor = snapshot.excerpts.cursor::>(); for selection in selections { - cursor.seek(&Some(&selection.start.excerpt_id), Bias::Left, &()); + let start_locator = snapshot.excerpt_locator_for_id(selection.start.excerpt_id); + let end_locator = snapshot.excerpt_locator_for_id(selection.end.excerpt_id); + + cursor.seek(&Some(start_locator), Bias::Left, &()); while let Some(excerpt) = cursor.item() { - if excerpt.id > selection.end.excerpt_id { + if excerpt.locator > *end_locator { break; } @@ -745,7 +753,7 @@ impl MultiBuffer { where O: text::ToOffset, { - self.insert_excerpts_after(&ExcerptId::max(), buffer, ranges, cx) + self.insert_excerpts_after(ExcerptId::max(), buffer, ranges, cx) } pub fn push_excerpts_with_context_lines( @@ -818,7 +826,7 @@ impl MultiBuffer { pub fn insert_excerpts_after( &mut self, - prev_excerpt_id: &ExcerptId, + prev_excerpt_id: ExcerptId, buffer: ModelHandle, ranges: impl IntoIterator>, cx: &mut ModelContext, @@ -854,8 +862,12 @@ impl MultiBuffer { }); let mut snapshot = self.snapshot.borrow_mut(); - let mut cursor = snapshot.excerpts.cursor::>(); - let mut new_excerpts = cursor.slice(&Some(prev_excerpt_id), Bias::Right, &()); + + let mut prev_locator = snapshot.excerpt_locator_for_id(prev_excerpt_id).clone(); + let mut new_excerpt_ids = mem::take(&mut snapshot.excerpt_ids); + let mut cursor = snapshot.excerpts.cursor::>(); + let mut new_excerpts = cursor.slice(&prev_locator, Bias::Right, &()); + prev_locator = cursor.start().unwrap_or(Locator::min_ref()).clone(); let edit_start = new_excerpts.summary().text.len; new_excerpts.update_last( @@ -865,25 +877,17 @@ impl MultiBuffer { &(), ); - let mut used_cursor = self.used_excerpt_ids.cursor::(); - used_cursor.seek(prev_excerpt_id, Bias::Right, &()); - let mut prev_id = if let Some(excerpt_id) = used_cursor.prev_item() { - excerpt_id.clone() + let next_locator = if let Some(excerpt) = cursor.item() { + excerpt.locator.clone() } else { - ExcerptId::min() + Locator::max() }; - let next_id = if let Some(excerpt_id) = used_cursor.item() { - excerpt_id.clone() - } else { - ExcerptId::max() - }; - drop(used_cursor); let mut ids = Vec::new(); while let Some(range) = ranges.next() { - let id = ExcerptId::between(&prev_id, &next_id); - if let Err(ix) = buffer_state.excerpts.binary_search(&id) { - buffer_state.excerpts.insert(ix, id.clone()); + let locator = Locator::between(&prev_locator, &next_locator); + if let Err(ix) = buffer_state.excerpts.binary_search(&locator) { + buffer_state.excerpts.insert(ix, locator.clone()); } let range = ExcerptRange { context: buffer_snapshot.anchor_before(&range.context.start) @@ -893,22 +897,20 @@ impl MultiBuffer { ..buffer_snapshot.anchor_after(&primary.end) }), }; + let id = ExcerptId(post_inc(&mut self.next_excerpt_id)); let excerpt = Excerpt::new( - id.clone(), - post_inc(&mut self.next_excerpt_key), + id, + locator.clone(), buffer_id, buffer_snapshot.clone(), range, ranges.peek().is_some() || cursor.item().is_some(), ); new_excerpts.push(excerpt, &()); - prev_id = id.clone(); + prev_locator = locator.clone(); + new_excerpt_ids.push(ExcerptIdMapping { id, locator }, &()); ids.push(id); } - self.used_excerpt_ids.edit( - ids.iter().cloned().map(sum_tree::Edit::Insert).collect(), - &(), - ); let edit_end = new_excerpts.summary().text.len; @@ -917,6 +919,7 @@ impl MultiBuffer { new_excerpts.push_tree(suffix, &()); drop(cursor); snapshot.excerpts = new_excerpts; + snapshot.excerpt_ids = new_excerpt_ids; if changed_trailing_excerpt { snapshot.trailing_excerpt_update_count += 1; } @@ -956,16 +959,16 @@ impl MultiBuffer { let mut excerpts = Vec::new(); let snapshot = self.read(cx); let buffers = self.buffers.borrow(); - let mut cursor = snapshot.excerpts.cursor::>(); - for excerpt_id in buffers + let mut cursor = snapshot.excerpts.cursor::>(); + for locator in buffers .get(&buffer.id()) .map(|state| &state.excerpts) .into_iter() .flatten() { - cursor.seek_forward(&Some(excerpt_id), Bias::Left, &()); + cursor.seek_forward(&Some(locator), Bias::Left, &()); if let Some(excerpt) = cursor.item() { - if excerpt.id == *excerpt_id { + if excerpt.locator == *locator { excerpts.push((excerpt.id.clone(), excerpt.range.clone())); } } @@ -975,10 +978,11 @@ impl MultiBuffer { } pub fn excerpt_ids(&self) -> Vec { - self.buffers + self.snapshot .borrow() - .values() - .flat_map(|state| state.excerpts.iter().cloned()) + .excerpts + .iter() + .map(|entry| entry.id) .collect() } @@ -1061,32 +1065,34 @@ impl MultiBuffer { result } - pub fn remove_excerpts<'a>( + pub fn remove_excerpts( &mut self, - excerpt_ids: impl IntoIterator, + excerpt_ids: impl IntoIterator, cx: &mut ModelContext, ) { self.sync(cx); let mut buffers = self.buffers.borrow_mut(); let mut snapshot = self.snapshot.borrow_mut(); let mut new_excerpts = SumTree::new(); - let mut cursor = snapshot.excerpts.cursor::<(Option<&ExcerptId>, usize)>(); + let mut cursor = snapshot.excerpts.cursor::<(Option<&Locator>, usize)>(); let mut edits = Vec::new(); let mut excerpt_ids = excerpt_ids.into_iter().peekable(); - while let Some(mut excerpt_id) = excerpt_ids.next() { + while let Some(excerpt_id) = excerpt_ids.next() { // Seek to the next excerpt to remove, preserving any preceding excerpts. - new_excerpts.push_tree(cursor.slice(&Some(excerpt_id), Bias::Left, &()), &()); + let locator = snapshot.excerpt_locator_for_id(excerpt_id); + new_excerpts.push_tree(cursor.slice(&Some(locator), Bias::Left, &()), &()); + if let Some(mut excerpt) = cursor.item() { - if excerpt.id != *excerpt_id { + if excerpt.id != excerpt_id { continue; } let mut old_start = cursor.start().1; // Skip over the removed excerpt. - loop { + 'remove_excerpts: loop { if let Some(buffer_state) = buffers.get_mut(&excerpt.buffer_id) { - buffer_state.excerpts.retain(|id| id != excerpt_id); + buffer_state.excerpts.retain(|l| l != &excerpt.locator); if buffer_state.excerpts.is_empty() { buffers.remove(&excerpt.buffer_id); } @@ -1094,14 +1100,16 @@ impl MultiBuffer { cursor.next(&()); // Skip over any subsequent excerpts that are also removed. - if let Some(&next_excerpt_id) = excerpt_ids.peek() { + while let Some(&next_excerpt_id) = excerpt_ids.peek() { + let next_locator = snapshot.excerpt_locator_for_id(next_excerpt_id); if let Some(next_excerpt) = cursor.item() { - if next_excerpt.id == *next_excerpt_id { + if next_excerpt.locator == *next_locator { + excerpt_ids.next(); excerpt = next_excerpt; - excerpt_id = excerpt_ids.next().unwrap(); - continue; + continue 'remove_excerpts; } } + break; } break; @@ -1128,6 +1136,7 @@ impl MultiBuffer { new_excerpts.push_tree(suffix, &()); drop(cursor); snapshot.excerpts = new_excerpts; + if changed_trailing_excerpt { snapshot.trailing_excerpt_update_count += 1; } @@ -1307,7 +1316,7 @@ impl MultiBuffer { buffer_state .excerpts .iter() - .map(|excerpt_id| (excerpt_id, buffer_state.buffer.clone(), buffer_edited)), + .map(|locator| (locator, buffer_state.buffer.clone(), buffer_edited)), ); } @@ -1333,14 +1342,14 @@ impl MultiBuffer { snapshot.is_dirty = is_dirty; snapshot.has_conflict = has_conflict; - excerpts_to_edit.sort_unstable_by_key(|(excerpt_id, _, _)| *excerpt_id); + excerpts_to_edit.sort_unstable_by_key(|(locator, _, _)| *locator); let mut edits = Vec::new(); let mut new_excerpts = SumTree::new(); - let mut cursor = snapshot.excerpts.cursor::<(Option<&ExcerptId>, usize)>(); + let mut cursor = snapshot.excerpts.cursor::<(Option<&Locator>, usize)>(); - for (id, buffer, buffer_edited) in excerpts_to_edit { - new_excerpts.push_tree(cursor.slice(&Some(id), Bias::Left, &()), &()); + for (locator, buffer, buffer_edited) in excerpts_to_edit { + new_excerpts.push_tree(cursor.slice(&Some(locator), Bias::Left, &()), &()); let old_excerpt = cursor.item().unwrap(); let buffer_id = buffer.id(); let buffer = buffer.read(cx); @@ -1365,8 +1374,8 @@ impl MultiBuffer { ); new_excerpt = Excerpt::new( - id.clone(), - old_excerpt.key, + old_excerpt.id, + locator.clone(), buffer_id, buffer.snapshot(), old_excerpt.range.clone(), @@ -1467,13 +1476,7 @@ impl MultiBuffer { continue; } - let excerpt_ids = self - .buffers - .borrow() - .values() - .flat_map(|b| &b.excerpts) - .cloned() - .collect::>(); + let excerpt_ids = self.excerpt_ids(); if excerpt_ids.is_empty() || (rng.gen() && excerpt_ids.len() < max_excerpts) { let buffer_handle = if rng.gen() || self.buffers.borrow().is_empty() { let text = RandomCharIter::new(&mut *rng).take(10).collect::(); @@ -1511,24 +1514,26 @@ impl MultiBuffer { log::info!( "Inserting excerpts from buffer {} and ranges {:?}: {:?}", buffer_handle.id(), - ranges, + ranges.iter().map(|r| &r.context).collect::>(), ranges .iter() - .map(|range| &buffer_text[range.context.clone()]) + .map(|r| &buffer_text[r.context.clone()]) .collect::>() ); let excerpt_id = self.push_excerpts(buffer_handle.clone(), ranges, cx); - log::info!("Inserted with id: {:?}", excerpt_id); + log::info!("Inserted with ids: {:?}", excerpt_id); } else { let remove_count = rng.gen_range(1..=excerpt_ids.len()); let mut excerpts_to_remove = excerpt_ids .choose_multiple(rng, remove_count) .cloned() .collect::>(); - excerpts_to_remove.sort(); + let snapshot = self.snapshot.borrow(); + excerpts_to_remove.sort_unstable_by(|a, b| a.cmp(b, &*snapshot)); + drop(snapshot); log::info!("Removing excerpts {:?}", excerpts_to_remove); - self.remove_excerpts(&excerpts_to_remove, cx); + self.remove_excerpts(excerpts_to_remove, cx); } } } @@ -1563,6 +1568,38 @@ impl MultiBuffer { } else { self.randomly_edit_excerpts(rng, mutation_count, cx); } + + self.check_invariants(cx); + } + + fn check_invariants(&self, cx: &mut ModelContext) { + let snapshot = self.read(cx); + let excerpts = snapshot.excerpts.items(&()); + let excerpt_ids = snapshot.excerpt_ids.items(&()); + + for (ix, excerpt) in excerpts.iter().enumerate() { + if ix == 0 { + if excerpt.locator <= Locator::min() { + panic!("invalid first excerpt locator {:?}", excerpt.locator); + } + } else { + if excerpt.locator <= excerpts[ix - 1].locator { + panic!("excerpts are out-of-order: {:?}", excerpts); + } + } + } + + for (ix, entry) in excerpt_ids.iter().enumerate() { + if ix == 0 { + if entry.id.cmp(&ExcerptId::min(), &*snapshot).is_le() { + panic!("invalid first excerpt id {:?}", entry.id); + } + } else { + if entry.id <= excerpt_ids[ix - 1].id { + panic!("excerpt ids are out-of-order: {:?}", excerpt_ids); + } + } + } } } @@ -1749,20 +1786,20 @@ impl MultiBufferSnapshot { *cursor.start() + overshoot } - pub fn clip_point_utf16(&self, point: PointUtf16, bias: Bias) -> PointUtf16 { + pub fn clip_point_utf16(&self, point: Unclipped, bias: Bias) -> PointUtf16 { if let Some((_, _, buffer)) = self.as_singleton() { return buffer.clip_point_utf16(point, bias); } let mut cursor = self.excerpts.cursor::(); - cursor.seek(&point, Bias::Right, &()); + cursor.seek(&point.0, Bias::Right, &()); let overshoot = if let Some(excerpt) = cursor.item() { let excerpt_start = excerpt .buffer .offset_to_point_utf16(excerpt.range.context.start.to_offset(&excerpt.buffer)); let buffer_point = excerpt .buffer - .clip_point_utf16(excerpt_start + (point - cursor.start()), bias); + .clip_point_utf16(Unclipped(excerpt_start + (point.0 - cursor.start())), bias); buffer_point.saturating_sub(excerpt_start) } else { PointUtf16::zero() @@ -2151,7 +2188,9 @@ impl MultiBufferSnapshot { D: TextDimension + Ord + Sub, { let mut cursor = self.excerpts.cursor::(); - cursor.seek(&Some(&anchor.excerpt_id), Bias::Left, &()); + let locator = self.excerpt_locator_for_id(anchor.excerpt_id); + + cursor.seek(locator, Bias::Left, &()); if cursor.item().is_none() { cursor.next(&()); } @@ -2189,24 +2228,25 @@ impl MultiBufferSnapshot { let mut cursor = self.excerpts.cursor::(); let mut summaries = Vec::new(); while let Some(anchor) = anchors.peek() { - let excerpt_id = &anchor.excerpt_id; + let excerpt_id = anchor.excerpt_id; let excerpt_anchors = iter::from_fn(|| { let anchor = anchors.peek()?; - if anchor.excerpt_id == *excerpt_id { + if anchor.excerpt_id == excerpt_id { Some(&anchors.next().unwrap().text_anchor) } else { None } }); - cursor.seek_forward(&Some(excerpt_id), Bias::Left, &()); + let locator = self.excerpt_locator_for_id(excerpt_id); + cursor.seek_forward(locator, Bias::Left, &()); if cursor.item().is_none() { cursor.next(&()); } let position = D::from_text_summary(&cursor.start().text); if let Some(excerpt) = cursor.item() { - if excerpt.id == *excerpt_id { + if excerpt.id == excerpt_id { let excerpt_buffer_start = excerpt.range.context.start.summary::(&excerpt.buffer); let excerpt_buffer_end = @@ -2240,13 +2280,18 @@ impl MultiBufferSnapshot { I: 'a + IntoIterator, { let mut anchors = anchors.into_iter().enumerate().peekable(); - let mut cursor = self.excerpts.cursor::>(); + let mut cursor = self.excerpts.cursor::>(); + cursor.next(&()); + let mut result = Vec::new(); + while let Some((_, anchor)) = anchors.peek() { - let old_excerpt_id = &anchor.excerpt_id; + let old_excerpt_id = anchor.excerpt_id; // Find the location where this anchor's excerpt should be. - cursor.seek_forward(&Some(old_excerpt_id), Bias::Left, &()); + let old_locator = self.excerpt_locator_for_id(old_excerpt_id); + cursor.seek_forward(&Some(old_locator), Bias::Left, &()); + if cursor.item().is_none() { cursor.next(&()); } @@ -2256,27 +2301,22 @@ impl MultiBufferSnapshot { // Process all of the anchors for this excerpt. while let Some((_, anchor)) = anchors.peek() { - if anchor.excerpt_id != *old_excerpt_id { + if anchor.excerpt_id != old_excerpt_id { break; } - let mut kept_position = false; let (anchor_ix, anchor) = anchors.next().unwrap(); - let mut anchor = anchor.clone(); - - let id_invalid = - *old_excerpt_id == ExcerptId::max() || *old_excerpt_id == ExcerptId::min(); - let still_exists = next_excerpt.map_or(false, |excerpt| { - excerpt.id == *old_excerpt_id && excerpt.contains(&anchor) - }); + let mut anchor = *anchor; // Leave min and max anchors unchanged if invalid or // if the old excerpt still exists at this location - if id_invalid || still_exists { - kept_position = true; - } + let mut kept_position = next_excerpt + .map_or(false, |e| e.id == old_excerpt_id && e.contains(&anchor)) + || old_excerpt_id == ExcerptId::max() + || old_excerpt_id == ExcerptId::min(); + // If the old excerpt no longer exists at this location, then attempt to // find an equivalent position for this anchor in an adjacent excerpt. - else { + if !kept_position { for excerpt in [next_excerpt, prev_excerpt].iter().filter_map(|e| *e) { if excerpt.contains(&anchor) { anchor.excerpt_id = excerpt.id.clone(); @@ -2285,6 +2325,7 @@ impl MultiBufferSnapshot { } } } + // If there's no adjacent excerpt that contains the anchor's position, // then report that the anchor has lost its position. if !kept_position { @@ -2354,7 +2395,7 @@ impl MultiBufferSnapshot { }; } - let mut cursor = self.excerpts.cursor::<(usize, Option<&ExcerptId>)>(); + let mut cursor = self.excerpts.cursor::<(usize, Option)>(); cursor.seek(&offset, Bias::Right, &()); if cursor.item().is_none() && offset == cursor.start().0 && bias == Bias::Left { cursor.prev(&()); @@ -2382,8 +2423,9 @@ impl MultiBufferSnapshot { } pub fn anchor_in_excerpt(&self, excerpt_id: ExcerptId, text_anchor: text::Anchor) -> Anchor { - let mut cursor = self.excerpts.cursor::>(); - cursor.seek(&Some(&excerpt_id), Bias::Left, &()); + let locator = self.excerpt_locator_for_id(excerpt_id); + let mut cursor = self.excerpts.cursor::>(); + cursor.seek(locator, Bias::Left, &()); if let Some(excerpt) = cursor.item() { if excerpt.id == excerpt_id { let text_anchor = excerpt.clip_anchor(text_anchor); @@ -2401,7 +2443,7 @@ impl MultiBufferSnapshot { pub fn can_resolve(&self, anchor: &Anchor) -> bool { if anchor.excerpt_id == ExcerptId::min() || anchor.excerpt_id == ExcerptId::max() { true - } else if let Some(excerpt) = self.excerpt(&anchor.excerpt_id) { + } else if let Some(excerpt) = self.excerpt(anchor.excerpt_id) { excerpt.buffer.can_resolve(&anchor.text_anchor) } else { false @@ -2456,7 +2498,6 @@ impl MultiBufferSnapshot { let starts_new_buffer = Some(excerpt.buffer_id) != prev_buffer_id; let boundary = ExcerptBoundary { id: excerpt.id.clone(), - key: excerpt.key, row: cursor.start().1.row, buffer: excerpt.buffer.clone(), range: excerpt.range.clone(), @@ -2678,8 +2719,8 @@ impl MultiBufferSnapshot { .flatten() .map(|item| OutlineItem { depth: item.depth, - range: self.anchor_in_excerpt(excerpt_id.clone(), item.range.start) - ..self.anchor_in_excerpt(excerpt_id.clone(), item.range.end), + range: self.anchor_in_excerpt(excerpt_id, item.range.start) + ..self.anchor_in_excerpt(excerpt_id, item.range.end), text: item.text, highlight_ranges: item.highlight_ranges, name_ranges: item.name_ranges, @@ -2688,11 +2729,29 @@ impl MultiBufferSnapshot { )) } - fn excerpt<'a>(&'a self, excerpt_id: &'a ExcerptId) -> Option<&'a Excerpt> { - let mut cursor = self.excerpts.cursor::>(); - cursor.seek(&Some(excerpt_id), Bias::Left, &()); + fn excerpt_locator_for_id<'a>(&'a self, id: ExcerptId) -> &'a Locator { + if id == ExcerptId::min() { + Locator::min_ref() + } else if id == ExcerptId::max() { + Locator::max_ref() + } else { + let mut cursor = self.excerpt_ids.cursor::(); + cursor.seek(&id, Bias::Left, &()); + if let Some(entry) = cursor.item() { + if entry.id == id { + return &entry.locator; + } + } + panic!("invalid excerpt id {:?}", id) + } + } + + fn excerpt<'a>(&'a self, excerpt_id: ExcerptId) -> Option<&'a Excerpt> { + let mut cursor = self.excerpts.cursor::>(); + let locator = self.excerpt_locator_for_id(excerpt_id); + cursor.seek(&Some(locator), Bias::Left, &()); if let Some(excerpt) = cursor.item() { - if excerpt.id == *excerpt_id { + if excerpt.id == excerpt_id { return Some(excerpt); } } @@ -2703,10 +2762,12 @@ impl MultiBufferSnapshot { &'a self, range: &'a Range, ) -> impl 'a + Iterator)> { - let mut cursor = self.excerpts.cursor::>(); - cursor.seek(&Some(&range.start.excerpt_id), Bias::Left, &()); + let mut cursor = self.excerpts.cursor::(); + let start_locator = self.excerpt_locator_for_id(range.start.excerpt_id); + let end_locator = self.excerpt_locator_for_id(range.end.excerpt_id); + cursor.seek(start_locator, Bias::Left, &()); cursor - .take_while(move |excerpt| excerpt.id <= range.end.excerpt_id) + .take_while(move |excerpt| excerpt.locator <= *end_locator) .flat_map(move |excerpt| { let mut query_range = excerpt.range.context.start..excerpt.range.context.end; if excerpt.id == range.start.excerpt_id { @@ -2916,7 +2977,7 @@ impl History { impl Excerpt { fn new( id: ExcerptId, - key: usize, + locator: Locator, buffer_id: usize, buffer: BufferSnapshot, range: ExcerptRange, @@ -2924,7 +2985,7 @@ impl Excerpt { ) -> Self { Excerpt { id, - key, + locator, max_buffer_row: range.context.end.to_point(&buffer).row, text_summary: buffer .text_summary_for_range::(range.context.to_offset(&buffer)), @@ -3010,10 +3071,33 @@ impl Excerpt { } } +impl ExcerptId { + pub fn min() -> Self { + Self(0) + } + + pub fn max() -> Self { + Self(usize::MAX) + } + + pub fn cmp(&self, other: &Self, snapshot: &MultiBufferSnapshot) -> cmp::Ordering { + let a = snapshot.excerpt_locator_for_id(*self); + let b = snapshot.excerpt_locator_for_id(*other); + a.cmp(&b).then_with(|| self.0.cmp(&other.0)) + } +} + +impl Into for ExcerptId { + fn into(self) -> usize { + self.0 + } +} + impl fmt::Debug for Excerpt { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Excerpt") .field("id", &self.id) + .field("locator", &self.locator) .field("buffer_id", &self.buffer_id) .field("range", &self.range) .field("text_summary", &self.text_summary) @@ -3031,19 +3115,44 @@ impl sum_tree::Item for Excerpt { text += TextSummary::from("\n"); } ExcerptSummary { - excerpt_id: self.id.clone(), + excerpt_id: self.id, + excerpt_locator: self.locator.clone(), max_buffer_row: self.max_buffer_row, text, } } } +impl sum_tree::Item for ExcerptIdMapping { + type Summary = ExcerptId; + + fn summary(&self) -> Self::Summary { + self.id + } +} + +impl sum_tree::KeyedItem for ExcerptIdMapping { + type Key = ExcerptId; + + fn key(&self) -> Self::Key { + self.id + } +} + +impl sum_tree::Summary for ExcerptId { + type Context = (); + + fn add_summary(&mut self, other: &Self, _: &()) { + *self = *other; + } +} + impl sum_tree::Summary for ExcerptSummary { type Context = (); fn add_summary(&mut self, summary: &Self, _: &()) { - debug_assert!(summary.excerpt_id > self.excerpt_id); - self.excerpt_id = summary.excerpt_id.clone(); + debug_assert!(summary.excerpt_locator > self.excerpt_locator); + self.excerpt_locator = summary.excerpt_locator.clone(); self.text.add_summary(&summary.text, &()); self.max_buffer_row = cmp::max(self.max_buffer_row, summary.max_buffer_row); } @@ -3067,9 +3176,15 @@ impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, ExcerptSummary> for usize { } } -impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, ExcerptSummary> for Option<&'a ExcerptId> { +impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, Option<&'a Locator>> for Locator { + fn cmp(&self, cursor_location: &Option<&'a Locator>, _: &()) -> cmp::Ordering { + Ord::cmp(&Some(self), cursor_location) + } +} + +impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, ExcerptSummary> for Locator { fn cmp(&self, cursor_location: &ExcerptSummary, _: &()) -> cmp::Ordering { - Ord::cmp(self, &Some(&cursor_location.excerpt_id)) + Ord::cmp(self, &cursor_location.excerpt_locator) } } @@ -3091,9 +3206,15 @@ impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for PointUtf16 { } } -impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for Option<&'a ExcerptId> { +impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for Option<&'a Locator> { fn add_summary(&mut self, summary: &'a ExcerptSummary, _: &()) { - *self = Some(&summary.excerpt_id); + *self = Some(&summary.excerpt_locator); + } +} + +impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for Option { + fn add_summary(&mut self, summary: &'a ExcerptSummary, _: &()) { + *self = Some(summary.excerpt_id); } } @@ -3274,12 +3395,6 @@ impl ToOffset for Point { } } -impl ToOffset for PointUtf16 { - fn to_offset<'a>(&self, snapshot: &MultiBufferSnapshot) -> usize { - snapshot.point_utf16_to_offset(*self) - } -} - impl ToOffset for usize { fn to_offset<'a>(&self, snapshot: &MultiBufferSnapshot) -> usize { assert!(*self <= snapshot.len(), "offset is out of range"); @@ -3293,6 +3408,12 @@ impl ToOffset for OffsetUtf16 { } } +impl ToOffset for PointUtf16 { + fn to_offset<'a>(&self, snapshot: &MultiBufferSnapshot) -> usize { + snapshot.point_utf16_to_offset(*self) + } +} + impl ToOffsetUtf16 for OffsetUtf16 { fn to_offset_utf16(&self, _snapshot: &MultiBufferSnapshot) -> OffsetUtf16 { *self @@ -3591,7 +3712,7 @@ mod tests { let snapshot = multibuffer.update(cx, |multibuffer, cx| { let (buffer_2_excerpt_id, _) = multibuffer.excerpts_for_buffer(&buffer_2, cx)[0].clone(); - multibuffer.remove_excerpts(&[buffer_2_excerpt_id], cx); + multibuffer.remove_excerpts([buffer_2_excerpt_id], cx); multibuffer.snapshot(cx) }); @@ -3780,7 +3901,7 @@ mod tests { // Replace the buffer 1 excerpt with new excerpts from buffer 2. let (excerpt_id_2, excerpt_id_3) = multibuffer.update(cx, |multibuffer, cx| { - multibuffer.remove_excerpts([&excerpt_id_1], cx); + multibuffer.remove_excerpts([excerpt_id_1], cx); let mut ids = multibuffer .push_excerpts( buffer_2.clone(), @@ -3810,9 +3931,8 @@ mod tests { assert_ne!(excerpt_id_2, excerpt_id_1); // Resolve some anchors from the previous snapshot in the new snapshot. - // Although there is still an excerpt with the same id, it is for - // a different buffer, so we don't attempt to resolve the old text - // anchor in the new buffer. + // The current excerpts are from a different buffer, so we don't attempt to + // resolve the old text anchor in the new buffer. assert_eq!( snapshot_2.summary_for_anchor::(&snapshot_1.anchor_before(2)), 0 @@ -3824,6 +3944,9 @@ mod tests { ]), vec![0, 0] ); + + // Refresh anchors from the old snapshot. The return value indicates that both + // anchors lost their original excerpt. let refresh = snapshot_2.refresh_anchors(&[snapshot_1.anchor_before(2), snapshot_1.anchor_after(3)]); assert_eq!( @@ -3837,10 +3960,10 @@ mod tests { // Replace the middle excerpt with a smaller excerpt in buffer 2, // that intersects the old excerpt. let excerpt_id_5 = multibuffer.update(cx, |multibuffer, cx| { - multibuffer.remove_excerpts([&excerpt_id_3], cx); + multibuffer.remove_excerpts([excerpt_id_3], cx); multibuffer .insert_excerpts_after( - &excerpt_id_3, + excerpt_id_2, buffer_2.clone(), [ExcerptRange { context: 5..8, @@ -3857,8 +3980,8 @@ mod tests { assert_ne!(excerpt_id_5, excerpt_id_3); // Resolve some anchors from the previous snapshot in the new snapshot. - // The anchor in the middle excerpt snaps to the beginning of the - // excerpt, since it is not + // The third anchor can't be resolved, since its excerpt has been removed, + // so it resolves to the same position as its predecessor. let anchors = [ snapshot_2.anchor_before(0), snapshot_2.anchor_after(2), @@ -3867,7 +3990,7 @@ mod tests { ]; assert_eq!( snapshot_3.summaries_for_anchors::(&anchors), - &[0, 2, 5, 13] + &[0, 2, 9, 13] ); let new_anchors = snapshot_3.refresh_anchors(&anchors); @@ -3889,7 +4012,7 @@ mod tests { let mut buffers: Vec> = Vec::new(); let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); - let mut excerpt_ids = Vec::new(); + let mut excerpt_ids = Vec::::new(); let mut expected_excerpts = Vec::<(ModelHandle, Range)>::new(); let mut anchors = Vec::new(); let mut old_versions = Vec::new(); @@ -3919,9 +4042,11 @@ mod tests { .collect::(), ); } - ids_to_remove.sort_unstable(); + let snapshot = multibuffer.read(cx).read(cx); + ids_to_remove.sort_unstable_by(|a, b| a.cmp(&b, &snapshot)); + drop(snapshot); multibuffer.update(cx, |multibuffer, cx| { - multibuffer.remove_excerpts(&ids_to_remove, cx) + multibuffer.remove_excerpts(ids_to_remove, cx) }); } 30..=39 if !expected_excerpts.is_empty() => { @@ -3945,7 +4070,6 @@ mod tests { // Ensure the newly-refreshed anchors point to a valid excerpt and don't // overshoot its boundaries. assert_eq!(anchors.len(), prev_len); - let mut cursor = multibuffer.excerpts.cursor::>(); for anchor in &anchors { if anchor.excerpt_id == ExcerptId::min() || anchor.excerpt_id == ExcerptId::max() @@ -3953,8 +4077,7 @@ mod tests { continue; } - cursor.seek_forward(&Some(&anchor.excerpt_id), Bias::Left, &()); - let excerpt = cursor.item().unwrap(); + let excerpt = multibuffer.excerpt(anchor.excerpt_id).unwrap(); assert_eq!(excerpt.id, anchor.excerpt_id); assert!(excerpt.contains(anchor)); } @@ -3994,7 +4117,7 @@ mod tests { let excerpt_id = multibuffer.update(cx, |multibuffer, cx| { multibuffer .insert_excerpts_after( - &prev_excerpt_id, + prev_excerpt_id, buffer_handle.clone(), [ExcerptRange { context: start_ix..end_ix, @@ -4158,12 +4281,14 @@ mod tests { } for _ in 0..ch.len_utf16() { - let left_point_utf16 = snapshot.clip_point_utf16(point_utf16, Bias::Left); - let right_point_utf16 = snapshot.clip_point_utf16(point_utf16, Bias::Right); + let left_point_utf16 = + snapshot.clip_point_utf16(Unclipped(point_utf16), Bias::Left); + let right_point_utf16 = + snapshot.clip_point_utf16(Unclipped(point_utf16), Bias::Right); let buffer_left_point_utf16 = - buffer.clip_point_utf16(buffer_point_utf16, Bias::Left); + buffer.clip_point_utf16(Unclipped(buffer_point_utf16), Bias::Left); let buffer_right_point_utf16 = - buffer.clip_point_utf16(buffer_point_utf16, Bias::Right); + buffer.clip_point_utf16(Unclipped(buffer_point_utf16), Bias::Right); assert_eq!( left_point_utf16, excerpt_start.lines_utf16() diff --git a/crates/editor/src/multi_buffer/anchor.rs b/crates/editor/src/multi_buffer/anchor.rs index 43723b95fc..4ecab76c48 100644 --- a/crates/editor/src/multi_buffer/anchor.rs +++ b/crates/editor/src/multi_buffer/anchor.rs @@ -6,7 +6,7 @@ use std::{ }; use sum_tree::Bias; -#[derive(Clone, Eq, PartialEq, Debug, Hash)] +#[derive(Clone, Copy, Eq, PartialEq, Debug, Hash)] pub struct Anchor { pub(crate) buffer_id: Option, pub(crate) excerpt_id: ExcerptId, @@ -30,16 +30,16 @@ impl Anchor { } } - pub fn excerpt_id(&self) -> &ExcerptId { - &self.excerpt_id + pub fn excerpt_id(&self) -> ExcerptId { + self.excerpt_id } pub fn cmp(&self, other: &Anchor, snapshot: &MultiBufferSnapshot) -> Ordering { - let excerpt_id_cmp = self.excerpt_id.cmp(&other.excerpt_id); + let excerpt_id_cmp = self.excerpt_id.cmp(&other.excerpt_id, snapshot); if excerpt_id_cmp.is_eq() { if self.excerpt_id == ExcerptId::min() || self.excerpt_id == ExcerptId::max() { Ordering::Equal - } else if let Some(excerpt) = snapshot.excerpt(&self.excerpt_id) { + } else if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) { self.text_anchor.cmp(&other.text_anchor, &excerpt.buffer) } else { Ordering::Equal @@ -51,7 +51,7 @@ impl Anchor { pub fn bias_left(&self, snapshot: &MultiBufferSnapshot) -> Anchor { if self.text_anchor.bias != Bias::Left { - if let Some(excerpt) = snapshot.excerpt(&self.excerpt_id) { + if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) { return Self { buffer_id: self.buffer_id, excerpt_id: self.excerpt_id.clone(), @@ -64,7 +64,7 @@ impl Anchor { pub fn bias_right(&self, snapshot: &MultiBufferSnapshot) -> Anchor { if self.text_anchor.bias != Bias::Right { - if let Some(excerpt) = snapshot.excerpt(&self.excerpt_id) { + if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) { return Self { buffer_id: self.buffer_id, excerpt_id: self.excerpt_id.clone(), diff --git a/crates/editor/src/selections_collection.rs b/crates/editor/src/selections_collection.rs index 256405f20e..facc1b0491 100644 --- a/crates/editor/src/selections_collection.rs +++ b/crates/editor/src/selections_collection.rs @@ -544,11 +544,21 @@ impl<'a> MutableSelectionsCollection<'a> { T: ToOffset, { let buffer = self.buffer.read(self.cx).snapshot(self.cx); + let ranges = ranges + .into_iter() + .map(|range| range.start.to_offset(&buffer)..range.end.to_offset(&buffer)); + self.select_offset_ranges(ranges); + } + + fn select_offset_ranges(&mut self, ranges: I) + where + I: IntoIterator>, + { let selections = ranges .into_iter() .map(|range| { - let mut start = range.start.to_offset(&buffer); - let mut end = range.end.to_offset(&buffer); + let mut start = range.start; + let mut end = range.end; let reversed = if start > end { mem::swap(&mut start, &mut end); true diff --git a/crates/gpui/src/elements/flex.rs b/crates/gpui/src/elements/flex.rs index 129d36dadd..f6a1a5d8e6 100644 --- a/crates/gpui/src/elements/flex.rs +++ b/crates/gpui/src/elements/flex.rs @@ -257,17 +257,19 @@ impl Element for Flex { let axis = self.axis; move |e, cx| { if remaining_space < 0. { + let scroll_delta = e.delta.raw(); + let mut delta = match axis { Axis::Horizontal => { - if e.delta.x().abs() >= e.delta.y().abs() { - e.delta.x() + if scroll_delta.x().abs() >= scroll_delta.y().abs() { + scroll_delta.x() } else { - e.delta.y() + scroll_delta.y() } } - Axis::Vertical => e.delta.y(), + Axis::Vertical => scroll_delta.y(), }; - if !e.precise { + if !e.delta.precise() { delta *= 20.; } diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index f1b747d647..53a0b70b35 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -258,8 +258,8 @@ impl Element for List { state.0.borrow_mut().scroll( &scroll_top, height, - e.platform_event.delta, - e.platform_event.precise, + *e.platform_event.delta.raw(), + e.platform_event.delta.precise(), cx, ) } diff --git a/crates/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index 0eab7f0bc9..79836b70e8 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/crates/gpui/src/elements/uniform_list.rs @@ -295,15 +295,19 @@ impl Element for UniformList { move |MouseScrollWheel { platform_event: ScrollWheelEvent { - position, - delta, - precise, - .. + position, delta, .. }, .. }, cx| { - if !Self::scroll(state.clone(), position, delta, precise, scroll_max, cx) { + if !Self::scroll( + state.clone(), + position, + *delta.raw(), + delta.precise(), + scroll_max, + cx, + ) { cx.propagate_event(); } } diff --git a/crates/gpui/src/platform/event.rs b/crates/gpui/src/platform/event.rs index a309f5123f..862807a74d 100644 --- a/crates/gpui/src/platform/event.rs +++ b/crates/gpui/src/platform/event.rs @@ -1,5 +1,7 @@ use std::ops::Deref; +use pathfinder_geometry::vector::vec2f; + use crate::{geometry::vector::Vector2F, keymap::Keystroke}; #[derive(Clone, Debug)] @@ -44,11 +46,45 @@ pub enum TouchPhase { Ended, } +#[derive(Clone, Copy, Debug)] +pub enum ScrollDelta { + Pixels(Vector2F), + Lines(Vector2F), +} + +impl Default for ScrollDelta { + fn default() -> Self { + Self::Lines(Default::default()) + } +} + +impl ScrollDelta { + pub fn raw(&self) -> &Vector2F { + match self { + ScrollDelta::Pixels(v) => v, + ScrollDelta::Lines(v) => v, + } + } + + pub fn precise(&self) -> bool { + match self { + ScrollDelta::Pixels(_) => true, + ScrollDelta::Lines(_) => false, + } + } + + pub fn pixel_delta(&self, line_height: f32) -> Vector2F { + match self { + ScrollDelta::Pixels(delta) => *delta, + ScrollDelta::Lines(delta) => vec2f(delta.x() * line_height, delta.y() * line_height), + } + } +} + #[derive(Clone, Copy, Debug, Default)] pub struct ScrollWheelEvent { pub position: Vector2F, - pub delta: Vector2F, - pub precise: bool, + pub delta: ScrollDelta, pub modifiers: Modifiers, /// If the platform supports returning the phase of a scroll wheel event, it will be stored here pub phase: Option, diff --git a/crates/gpui/src/platform/mac/event.rs b/crates/gpui/src/platform/mac/event.rs index 77d1e67cae..36dab73149 100644 --- a/crates/gpui/src/platform/mac/event.rs +++ b/crates/gpui/src/platform/mac/event.rs @@ -3,7 +3,7 @@ use crate::{ keymap::Keystroke, platform::{Event, NavigationDirection}, KeyDownEvent, KeyUpEvent, Modifiers, ModifiersChangedEvent, MouseButton, MouseButtonEvent, - MouseMovedEvent, ScrollWheelEvent, TouchPhase, + MouseMovedEvent, ScrollDelta, ScrollWheelEvent, TouchPhase, }; use cocoa::{ appkit::{NSEvent, NSEventModifierFlags, NSEventPhase, NSEventType}, @@ -164,17 +164,24 @@ impl Event { _ => Some(TouchPhase::Moved), }; + let raw_data = vec2f( + native_event.scrollingDeltaX() as f32, + native_event.scrollingDeltaY() as f32, + ); + + let delta = if native_event.hasPreciseScrollingDeltas() == YES { + ScrollDelta::Pixels(raw_data) + } else { + ScrollDelta::Lines(raw_data) + }; + Self::ScrollWheel(ScrollWheelEvent { position: vec2f( native_event.locationInWindow().x as f32, window_height - native_event.locationInWindow().y as f32, ), - delta: vec2f( - native_event.scrollingDeltaX() as f32, - native_event.scrollingDeltaY() as f32, - ), + delta, phase, - precise: native_event.hasPreciseScrollingDeltas() == YES, modifiers: read_modifiers(native_event), }) }), diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index e43e428fe6..d15051ef12 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -475,27 +475,35 @@ impl Presenter { if let MouseEvent::Down(e) = &mouse_event { if valid_region .handlers - .contains_handler(MouseEvent::click_disc(), Some(e.button)) + .contains(MouseEvent::click_disc(), Some(e.button)) || valid_region .handlers - .contains_handler(MouseEvent::drag_disc(), Some(e.button)) + .contains(MouseEvent::drag_disc(), Some(e.button)) { event_cx.handled = true; } } - if let Some(callback) = valid_region.handlers.get(&mouse_event.handler_key()) { - event_cx.handled = true; - event_cx.with_current_view(valid_region.id().view_id(), { - let region_event = mouse_event.clone(); - |cx| callback(region_event, cx) - }); + // `event_consumed` should only be true if there are any handlers for this event. + let mut event_consumed = event_cx.handled; + if let Some(callbacks) = valid_region.handlers.get(&mouse_event.handler_key()) { + event_consumed = true; + for callback in callbacks { + event_cx.handled = true; + event_cx.with_current_view(valid_region.id().view_id(), { + let region_event = mouse_event.clone(); + |cx| callback(region_event, cx) + }); + event_consumed &= event_cx.handled; + any_event_handled |= event_cx.handled; + } } - any_event_handled = any_event_handled || event_cx.handled; - // For bubbling events, if the event was handled, don't continue dispatching - // This only makes sense for local events. - if event_cx.handled && mouse_event.is_capturable() { + any_event_handled |= event_cx.handled; + + // For bubbling events, if the event was handled, don't continue dispatching. + // This only makes sense for local events which return false from is_capturable. + if event_consumed && mouse_event.is_capturable() { break; } } diff --git a/crates/gpui/src/scene/mouse_event.rs b/crates/gpui/src/scene/mouse_event.rs index d7370ac75f..00d1ddbf8b 100644 --- a/crates/gpui/src/scene/mouse_event.rs +++ b/crates/gpui/src/scene/mouse_event.rs @@ -5,7 +5,7 @@ use std::{ use pathfinder_geometry::{rect::RectF, vector::Vector2F}; -use crate::{MouseButton, MouseButtonEvent, MouseMovedEvent, ScrollWheelEvent}; +use crate::{scene::mouse_region::HandlerKey, MouseButtonEvent, MouseMovedEvent, ScrollWheelEvent}; #[derive(Debug, Default, Clone)] pub struct MouseMove { @@ -217,17 +217,17 @@ impl MouseEvent { discriminant(&MouseEvent::ScrollWheel(Default::default())) } - pub fn handler_key(&self) -> (Discriminant, Option) { + pub fn handler_key(&self) -> HandlerKey { match self { - MouseEvent::Move(_) => (Self::move_disc(), None), - MouseEvent::Drag(e) => (Self::drag_disc(), e.pressed_button), - MouseEvent::Hover(_) => (Self::hover_disc(), None), - MouseEvent::Down(e) => (Self::down_disc(), Some(e.button)), - MouseEvent::Up(e) => (Self::up_disc(), Some(e.button)), - MouseEvent::Click(e) => (Self::click_disc(), Some(e.button)), - MouseEvent::UpOut(e) => (Self::up_out_disc(), Some(e.button)), - MouseEvent::DownOut(e) => (Self::down_out_disc(), Some(e.button)), - MouseEvent::ScrollWheel(_) => (Self::scroll_wheel_disc(), None), + MouseEvent::Move(_) => HandlerKey::new(Self::move_disc(), None), + MouseEvent::Drag(e) => HandlerKey::new(Self::drag_disc(), e.pressed_button), + MouseEvent::Hover(_) => HandlerKey::new(Self::hover_disc(), None), + MouseEvent::Down(e) => HandlerKey::new(Self::down_disc(), Some(e.button)), + MouseEvent::Up(e) => HandlerKey::new(Self::up_disc(), Some(e.button)), + MouseEvent::Click(e) => HandlerKey::new(Self::click_disc(), Some(e.button)), + MouseEvent::UpOut(e) => HandlerKey::new(Self::up_out_disc(), Some(e.button)), + MouseEvent::DownOut(e) => HandlerKey::new(Self::down_out_disc(), Some(e.button)), + MouseEvent::ScrollWheel(_) => HandlerKey::new(Self::scroll_wheel_disc(), None), } } } diff --git a/crates/gpui/src/scene/mouse_region.rs b/crates/gpui/src/scene/mouse_region.rs index 4b5217cc2d..0fdc76ebbf 100644 --- a/crates/gpui/src/scene/mouse_region.rs +++ b/crates/gpui/src/scene/mouse_region.rs @@ -3,6 +3,7 @@ use std::{any::TypeId, fmt::Debug, mem::Discriminant, rc::Rc}; use collections::HashMap; use pathfinder_geometry::rect::RectF; +use smallvec::SmallVec; use crate::{EventContext, MouseButton}; @@ -177,61 +178,105 @@ impl MouseRegionId { } } +pub type HandlerCallback = Rc; + +#[derive(Clone, PartialEq, Eq, Hash)] +pub struct HandlerKey { + event_kind: Discriminant, + button: Option, +} + +impl HandlerKey { + pub fn new(event_kind: Discriminant, button: Option) -> HandlerKey { + HandlerKey { event_kind, button } + } +} + #[derive(Clone, Default)] pub struct HandlerSet { - #[allow(clippy::type_complexity)] - pub set: HashMap< - (Discriminant, Option), - Rc, - >, + set: HashMap>, } impl HandlerSet { pub fn capture_all() -> Self { - #[allow(clippy::type_complexity)] - let mut set: HashMap< - (Discriminant, Option), - Rc, - > = Default::default(); + let mut set: HashMap> = HashMap::default(); - set.insert((MouseEvent::move_disc(), None), Rc::new(|_, _| {})); - set.insert((MouseEvent::hover_disc(), None), Rc::new(|_, _| {})); + set.insert( + HandlerKey::new(MouseEvent::move_disc(), None), + SmallVec::from_buf([Rc::new(|_, _| {})]), + ); + set.insert( + HandlerKey::new(MouseEvent::hover_disc(), None), + SmallVec::from_buf([Rc::new(|_, _| {})]), + ); for button in MouseButton::all() { - set.insert((MouseEvent::drag_disc(), Some(button)), Rc::new(|_, _| {})); - set.insert((MouseEvent::down_disc(), Some(button)), Rc::new(|_, _| {})); - set.insert((MouseEvent::up_disc(), Some(button)), Rc::new(|_, _| {})); - set.insert((MouseEvent::click_disc(), Some(button)), Rc::new(|_, _| {})); set.insert( - (MouseEvent::down_out_disc(), Some(button)), - Rc::new(|_, _| {}), + HandlerKey::new(MouseEvent::drag_disc(), Some(button)), + SmallVec::from_buf([Rc::new(|_, _| {})]), ); set.insert( - (MouseEvent::up_out_disc(), Some(button)), - Rc::new(|_, _| {}), + HandlerKey::new(MouseEvent::down_disc(), Some(button)), + SmallVec::from_buf([Rc::new(|_, _| {})]), + ); + set.insert( + HandlerKey::new(MouseEvent::up_disc(), Some(button)), + SmallVec::from_buf([Rc::new(|_, _| {})]), + ); + set.insert( + HandlerKey::new(MouseEvent::click_disc(), Some(button)), + SmallVec::from_buf([Rc::new(|_, _| {})]), + ); + set.insert( + HandlerKey::new(MouseEvent::down_out_disc(), Some(button)), + SmallVec::from_buf([Rc::new(|_, _| {})]), + ); + set.insert( + HandlerKey::new(MouseEvent::up_out_disc(), Some(button)), + SmallVec::from_buf([Rc::new(|_, _| {})]), ); } - set.insert((MouseEvent::scroll_wheel_disc(), None), Rc::new(|_, _| {})); + set.insert( + HandlerKey::new(MouseEvent::scroll_wheel_disc(), None), + SmallVec::from_buf([Rc::new(|_, _| {})]), + ); HandlerSet { set } } - pub fn get( - &self, - key: &(Discriminant, Option), - ) -> Option> { - self.set.get(key).cloned() + pub fn get(&self, key: &HandlerKey) -> Option<&[HandlerCallback]> { + self.set.get(key).map(|vec| vec.as_slice()) } - pub fn contains_handler( + pub fn contains( &self, - event: Discriminant, + discriminant: Discriminant, button: Option, ) -> bool { - self.set.contains_key(&(event, button)) + self.set + .contains_key(&HandlerKey::new(discriminant, button)) + } + + fn insert( + &mut self, + event_kind: Discriminant, + button: Option, + callback: HandlerCallback, + ) { + use std::collections::hash_map::Entry; + + match self.set.entry(HandlerKey::new(event_kind, button)) { + Entry::Occupied(mut vec) => { + vec.get_mut().push(callback); + } + + Entry::Vacant(entry) => { + entry.insert(SmallVec::from_buf([callback])); + } + } } pub fn on_move(mut self, handler: impl Fn(MouseMove, &mut EventContext) + 'static) -> Self { - self.set.insert((MouseEvent::move_disc(), None), + self.insert(MouseEvent::move_disc(), None, Rc::new(move |region_event, cx| { if let MouseEvent::Move(e) = region_event { handler(e, cx); @@ -249,7 +294,7 @@ impl HandlerSet { button: MouseButton, handler: impl Fn(MouseDown, &mut EventContext) + 'static, ) -> Self { - self.set.insert((MouseEvent::down_disc(), Some(button)), + self.insert(MouseEvent::down_disc(), Some(button), Rc::new(move |region_event, cx| { if let MouseEvent::Down(e) = region_event { handler(e, cx); @@ -267,7 +312,7 @@ impl HandlerSet { button: MouseButton, handler: impl Fn(MouseUp, &mut EventContext) + 'static, ) -> Self { - self.set.insert((MouseEvent::up_disc(), Some(button)), + self.insert(MouseEvent::up_disc(), Some(button), Rc::new(move |region_event, cx| { if let MouseEvent::Up(e) = region_event { handler(e, cx); @@ -285,7 +330,7 @@ impl HandlerSet { button: MouseButton, handler: impl Fn(MouseClick, &mut EventContext) + 'static, ) -> Self { - self.set.insert((MouseEvent::click_disc(), Some(button)), + self.insert(MouseEvent::click_disc(), Some(button), Rc::new(move |region_event, cx| { if let MouseEvent::Click(e) = region_event { handler(e, cx); @@ -303,7 +348,7 @@ impl HandlerSet { button: MouseButton, handler: impl Fn(MouseDownOut, &mut EventContext) + 'static, ) -> Self { - self.set.insert((MouseEvent::down_out_disc(), Some(button)), + self.insert(MouseEvent::down_out_disc(), Some(button), Rc::new(move |region_event, cx| { if let MouseEvent::DownOut(e) = region_event { handler(e, cx); @@ -321,7 +366,7 @@ impl HandlerSet { button: MouseButton, handler: impl Fn(MouseUpOut, &mut EventContext) + 'static, ) -> Self { - self.set.insert((MouseEvent::up_out_disc(), Some(button)), + self.insert(MouseEvent::up_out_disc(), Some(button), Rc::new(move |region_event, cx| { if let MouseEvent::UpOut(e) = region_event { handler(e, cx); @@ -339,7 +384,7 @@ impl HandlerSet { button: MouseButton, handler: impl Fn(MouseDrag, &mut EventContext) + 'static, ) -> Self { - self.set.insert((MouseEvent::drag_disc(), Some(button)), + self.insert(MouseEvent::drag_disc(), Some(button), Rc::new(move |region_event, cx| { if let MouseEvent::Drag(e) = region_event { handler(e, cx); @@ -353,7 +398,7 @@ impl HandlerSet { } pub fn on_hover(mut self, handler: impl Fn(MouseHover, &mut EventContext) + 'static) -> Self { - self.set.insert((MouseEvent::hover_disc(), None), + self.insert(MouseEvent::hover_disc(), None, Rc::new(move |region_event, cx| { if let MouseEvent::Hover(e) = region_event { handler(e, cx); @@ -370,7 +415,7 @@ impl HandlerSet { mut self, handler: impl Fn(MouseScrollWheel, &mut EventContext) + 'static, ) -> Self { - self.set.insert((MouseEvent::scroll_wheel_disc(), None), + self.insert(MouseEvent::scroll_wheel_disc(), None, Rc::new(move |region_event, cx| { if let MouseEvent::ScrollWheel(e) = region_event { handler(e, cx); diff --git a/crates/journal/Cargo.toml b/crates/journal/Cargo.toml index 8c900d9f4a..9622049a9c 100644 --- a/crates/journal/Cargo.toml +++ b/crates/journal/Cargo.toml @@ -16,4 +16,4 @@ chrono = "0.4" dirs = "4.0" log = { version = "0.4.16", features = ["kv_unstable_serde"] } settings = { path = "../settings" } -shellexpand = "2.1.0" \ No newline at end of file +shellexpand = "2.1.0" diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index 419c7a79a5..6c074a2d75 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -72,4 +72,5 @@ tree-sitter-rust = "*" tree-sitter-python = "*" tree-sitter-typescript = "*" tree-sitter-ruby = "*" +tree-sitter-embedded-template = "*" unindent = "0.1.7" diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 2a0b158a7a..e8bc2bf314 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -2225,11 +2225,12 @@ impl BufferSnapshot { range: Range, ) -> Option<(Range, Range)> { // Find bracket pairs that *inclusively* contain the given range. - let range = range.start.to_offset(self).saturating_sub(1) - ..self.len().min(range.end.to_offset(self) + 1); - let mut matches = self.syntax.matches(range, &self.text, |grammar| { - grammar.brackets_config.as_ref().map(|c| &c.query) - }); + let range = range.start.to_offset(self)..range.end.to_offset(self); + let mut matches = self.syntax.matches( + range.start.saturating_sub(1)..self.len().min(range.end + 1), + &self.text, + |grammar| grammar.brackets_config.as_ref().map(|c| &c.query), + ); let configs = matches .grammars() .iter() @@ -2252,18 +2253,20 @@ impl BufferSnapshot { matches.advance(); - if let Some((open, close)) = open.zip(close) { - let len = close.end - open.start; - - if let Some((existing_open, existing_close)) = &result { - let existing_len = existing_close.end - existing_open.start; - if len > existing_len { - continue; - } - } - - result = Some((open, close)); + let Some((open, close)) = open.zip(close) else { continue }; + if open.start > range.start || close.end < range.end { + continue; } + let len = close.end - open.start; + + if let Some((existing_open, existing_close)) = &result { + let existing_len = existing_close.end - existing_open.start; + if len > existing_len { + continue; + } + } + + result = Some((open, close)); } result diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index 6043127dd5..5f2fdf6e8e 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -573,14 +573,72 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) { )) ); - // Regression test: avoid crash when querying at the end of the buffer. assert_eq!( - buffer.enclosing_bracket_point_ranges(buffer.len() - 1..buffer.len()), + buffer.enclosing_bracket_point_ranges(Point::new(4, 1)..Point::new(4, 1)), Some(( Point::new(0, 6)..Point::new(0, 7), Point::new(4, 0)..Point::new(4, 1) )) ); + + // Regression test: avoid crash when querying at the end of the buffer. + assert_eq!( + buffer.enclosing_bracket_point_ranges(Point::new(4, 1)..Point::new(5, 0)), + None + ); +} + +#[gpui::test] +fn test_enclosing_bracket_ranges_where_brackets_are_not_outermost_children( + cx: &mut MutableAppContext, +) { + let javascript_language = Arc::new( + Language::new( + LanguageConfig { + name: "JavaScript".into(), + ..Default::default() + }, + Some(tree_sitter_javascript::language()), + ) + .with_brackets_query( + r#" + ("{" @open "}" @close) + ("(" @open ")" @close) + "#, + ) + .unwrap(), + ); + + cx.set_global(Settings::test(cx)); + let buffer = cx.add_model(|cx| { + let text = " + for (const a in b) { + // a comment that's longer than the for-loop header + } + " + .unindent(); + Buffer::new(0, text, cx).with_language(javascript_language, cx) + }); + + let buffer = buffer.read(cx); + assert_eq!( + buffer.enclosing_bracket_point_ranges(Point::new(0, 18)..Point::new(0, 18)), + Some(( + Point::new(0, 4)..Point::new(0, 5), + Point::new(0, 17)..Point::new(0, 18) + )) + ); + + // Regression test: even though the parent node of the parentheses (the for loop) does + // intersect the given range, the parentheses themselves do not contain the range, so + // they should not be returned. Only the curly braces contain the range. + assert_eq!( + buffer.enclosing_bracket_point_ranges(Point::new(0, 20)..Point::new(0, 20)), + Some(( + Point::new(0, 19)..Point::new(0, 20), + Point::new(2, 0)..Point::new(2, 1) + )) + ); } #[gpui::test] @@ -1337,6 +1395,7 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) { (0..entry_count).map(|_| { let range = buffer.random_byte_range(0, &mut rng); let range = range.to_point_utf16(buffer); + let range = range.start..range.end; DiagnosticEntry { range, diagnostic: Diagnostic { diff --git a/crates/language/src/diagnostic_set.rs b/crates/language/src/diagnostic_set.rs index b52327cac0..cde5a6fb2b 100644 --- a/crates/language/src/diagnostic_set.rs +++ b/crates/language/src/diagnostic_set.rs @@ -71,7 +71,7 @@ impl DiagnosticSet { diagnostics: SumTree::from_iter( entries.into_iter().map(|entry| DiagnosticEntry { range: buffer.anchor_before(entry.range.start) - ..buffer.anchor_after(entry.range.end), + ..buffer.anchor_before(entry.range.end), diagnostic: entry.diagnostic, }), buffer, diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 5abc89321c..c3f2c3716b 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -28,6 +28,7 @@ use std::{ any::Any, cell::RefCell, fmt::Debug, + hash::Hash, mem, ops::Range, path::{Path, PathBuf}, @@ -134,6 +135,10 @@ impl CachedLspAdapter { self.adapter.process_diagnostics(params).await } + pub async fn process_completion(&self, completion_item: &mut lsp::CompletionItem) { + self.adapter.process_completion(completion_item).await + } + pub async fn label_for_completion( &self, completion_item: &lsp::CompletionItem, @@ -174,6 +179,8 @@ pub trait LspAdapter: 'static + Send + Sync { async fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {} + async fn process_completion(&self, _: &mut lsp::CompletionItem) {} + async fn label_for_completion( &self, _: &lsp::CompletionItem, @@ -326,7 +333,13 @@ struct InjectionConfig { query: Query, content_capture_ix: u32, language_capture_ix: Option, - languages_by_pattern_ix: Vec>>, + patterns: Vec, +} + +#[derive(Default, Clone)] +struct InjectionPatternConfig { + language: Option>, + combined: bool, } struct BracketConfig { @@ -637,6 +650,10 @@ impl Language { self.adapter.clone() } + pub fn id(&self) -> Option { + self.grammar.as_ref().map(|g| g.id) + } + pub fn with_highlights_query(mut self, source: &str) -> Result { let grammar = self.grammar_mut(); grammar.highlights_query = Some(Query::new(grammar.ts_language, source)?); @@ -730,15 +747,21 @@ impl Language { ("content", &mut content_capture_ix), ], ); - let languages_by_pattern_ix = (0..query.pattern_count()) + let patterns = (0..query.pattern_count()) .map(|ix| { - query.property_settings(ix).iter().find_map(|setting| { - if setting.key.as_ref() == "language" { - return setting.value.clone(); - } else { - None + let mut config = InjectionPatternConfig::default(); + for setting in query.property_settings(ix) { + match setting.key.as_ref() { + "language" => { + config.language = setting.value.clone(); + } + "combined" => { + config.combined = true; + } + _ => {} } - }) + } + config }) .collect(); if let Some(content_capture_ix) = content_capture_ix { @@ -746,7 +769,7 @@ impl Language { query, language_capture_ix, content_capture_ix, - languages_by_pattern_ix, + patterns, }); } Ok(self) @@ -809,6 +832,12 @@ impl Language { } } + pub async fn process_completion(self: &Arc, completion: &mut lsp::CompletionItem) { + if let Some(adapter) = self.adapter.as_ref() { + adapter.process_completion(completion).await; + } + } + pub async fn label_for_completion( self: &Arc, completion: &lsp::CompletionItem, @@ -883,6 +912,20 @@ impl Language { } } +impl Hash for Language { + fn hash(&self, state: &mut H) { + self.id().hash(state) + } +} + +impl PartialEq for Language { + fn eq(&self, other: &Self) -> bool { + self.id().eq(&other.id()) + } +} + +impl Eq for Language {} + impl Debug for Language { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Language") @@ -1010,8 +1053,8 @@ pub fn point_to_lsp(point: PointUtf16) -> lsp::Position { lsp::Position::new(point.row, point.column) } -pub fn point_from_lsp(point: lsp::Position) -> PointUtf16 { - PointUtf16::new(point.line, point.character) +pub fn point_from_lsp(point: lsp::Position) -> Unclipped { + Unclipped(PointUtf16::new(point.line, point.character)) } pub fn range_to_lsp(range: Range) -> lsp::Range { @@ -1021,7 +1064,7 @@ pub fn range_to_lsp(range: Range) -> lsp::Range { } } -pub fn range_from_lsp(range: lsp::Range) -> Range { +pub fn range_from_lsp(range: lsp::Range) -> Range> { let mut start = point_from_lsp(range.start); let mut end = point_from_lsp(range.end); if start > end { diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index f93d99f76b..674ce4f50e 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -426,10 +426,11 @@ pub async fn deserialize_completion( .and_then(deserialize_anchor) .ok_or_else(|| anyhow!("invalid old end"))?; let lsp_completion = serde_json::from_slice(&completion.lsp_completion)?; - let label = match language { - Some(l) => l.label_for_completion(&lsp_completion).await, - None => None, - }; + + let mut label = None; + if let Some(language) = language { + label = language.label_for_completion(&lsp_completion).await; + } Ok(Completion { old_range: old_start..old_end, diff --git a/crates/language/src/syntax_map.rs b/crates/language/src/syntax_map.rs index 5dd9c483af..65d01e9493 100644 --- a/crates/language/src/syntax_map.rs +++ b/crates/language/src/syntax_map.rs @@ -1,4 +1,5 @@ use crate::{Grammar, InjectionConfig, Language, LanguageRegistry}; +use collections::HashMap; use lazy_static::lazy_static; use parking_lot::Mutex; use std::{ @@ -105,22 +106,42 @@ struct SyntaxLayerSummary { max_depth: usize, range: Range, last_layer_range: Range, + last_layer_language: Option, } #[derive(Clone, Debug)] -struct DepthAndRange(usize, Range); +struct SyntaxLayerPosition { + depth: usize, + range: Range, + language: Option, +} #[derive(Clone, Debug)] -struct DepthAndMaxPosition(usize, Anchor); +struct ChangeStartPosition { + depth: usize, + position: Anchor, +} #[derive(Clone, Debug)] -struct DepthAndRangeOrMaxPosition(DepthAndRange, DepthAndMaxPosition); +struct SyntaxLayerPositionBeforeChange { + position: SyntaxLayerPosition, + change: ChangeStartPosition, +} -struct ReparseStep { +struct ParseStep { depth: usize, language: Arc, - ranges: Vec, range: Range, + included_ranges: Vec, + mode: ParseMode, +} + +enum ParseMode { + Single, + Combined { + parent_layer_range: Range, + parent_layer_changed_ranges: Vec>, + }, } #[derive(Debug, PartialEq, Eq)] @@ -215,7 +236,10 @@ impl SyntaxSnapshot { // Preserve any layers at this depth that precede the first edit. if let Some((_, edit_range)) = edits.get(first_edit_ix_for_depth) { - let target = DepthAndMaxPosition(depth, edit_range.start); + let target = ChangeStartPosition { + depth, + position: edit_range.start, + }; if target.cmp(&cursor.start(), text).is_gt() { let slice = cursor.slice(&target, Bias::Left, text); layers.push_tree(slice, text); @@ -225,7 +249,11 @@ impl SyntaxSnapshot { // subsequent layers at this same depth. else if cursor.item().is_some() { let slice = cursor.slice( - &DepthAndRange(depth + 1, Anchor::MIN..Anchor::MAX), + &SyntaxLayerPosition { + depth: depth + 1, + range: Anchor::MIN..Anchor::MAX, + language: None, + }, Bias::Left, text, ); @@ -233,24 +261,17 @@ impl SyntaxSnapshot { continue; }; - let layer = if let Some(layer) = cursor.item() { - layer - } else { - break; - }; + let Some(layer) = cursor.item() else { break }; let (start_byte, start_point) = layer.range.start.summary::<(usize, Point)>(text); // Ignore edits that end before the start of this layer, and don't consider them // for any subsequent layers at this same depth. loop { - if let Some((_, edit_range)) = edits.get(first_edit_ix_for_depth) { - if edit_range.end.cmp(&layer.range.start, text).is_le() { - first_edit_ix_for_depth += 1; - } else { - break; - } + let Some((_, edit_range)) = edits.get(first_edit_ix_for_depth) else { continue 'outer }; + if edit_range.end.cmp(&layer.range.start, text).is_le() { + first_edit_ix_for_depth += 1; } else { - continue 'outer; + break; } } @@ -310,7 +331,7 @@ impl SyntaxSnapshot { from_version: &clock::Global, text: &BufferSnapshot, registry: Option>, - language: Arc, + root_language: Arc, ) { let edits = text.edits_since::(from_version).collect::>(); let max_depth = self.layers.summary().max_depth; @@ -320,30 +341,46 @@ impl SyntaxSnapshot { let mut changed_regions = ChangeRegionSet::default(); let mut queue = BinaryHeap::new(); - queue.push(ReparseStep { + let mut combined_injection_ranges = HashMap::default(); + queue.push(ParseStep { depth: 0, - language: language.clone(), - ranges: Vec::new(), + language: root_language.clone(), + included_ranges: vec![tree_sitter::Range { + start_byte: 0, + end_byte: text.len(), + start_point: Point::zero().to_ts_point(), + end_point: text.max_point().to_ts_point(), + }], range: Anchor::MIN..Anchor::MAX, + mode: ParseMode::Single, }); loop { let step = queue.pop(); - let (depth, range) = if let Some(step) = &step { - (step.depth, step.range.clone()) + let position = if let Some(step) = &step { + SyntaxLayerPosition { + depth: step.depth, + range: step.range.clone(), + language: step.language.id(), + } } else { - (max_depth + 1, Anchor::MAX..Anchor::MAX) + SyntaxLayerPosition { + depth: max_depth + 1, + range: Anchor::MAX..Anchor::MAX, + language: None, + } }; - let target = DepthAndRange(depth, range.clone()); let mut done = cursor.item().is_none(); - while !done && target.cmp(&cursor.end(text), &text).is_gt() { + while !done && position.cmp(&cursor.end(text), &text).is_gt() { done = true; - let bounded_target = - DepthAndRangeOrMaxPosition(target.clone(), changed_regions.start_position()); - if bounded_target.cmp(&cursor.start(), &text).is_gt() { - let slice = cursor.slice(&bounded_target, Bias::Left, text); + let bounded_position = SyntaxLayerPositionBeforeChange { + position: position.clone(), + change: changed_regions.start_position(), + }; + if bounded_position.cmp(&cursor.start(), &text).is_gt() { + let slice = cursor.slice(&bounded_position, Bias::Left, text); if !slice.is_empty() { layers.push_tree(slice, &text); if changed_regions.prune(cursor.end(text), text) { @@ -352,12 +389,8 @@ impl SyntaxSnapshot { } } - while target.cmp(&cursor.end(text), text).is_gt() { - let layer = if let Some(layer) = cursor.item() { - layer - } else { - break; - }; + while position.cmp(&cursor.end(text), text).is_gt() { + let Some(layer) = cursor.item() else { break }; if changed_regions.intersects(&layer, text) { changed_regions.insert( @@ -378,70 +411,74 @@ impl SyntaxSnapshot { } } - let (ranges, language) = if let Some(step) = step { - (step.ranges, step.language) - } else { - break; - }; - - let start_point; - let start_byte; - let end_byte; - if let Some((first, last)) = ranges.first().zip(ranges.last()) { - start_point = first.start_point; - start_byte = first.start_byte; - end_byte = last.end_byte; - } else { - start_point = Point::zero().to_ts_point(); - start_byte = 0; - end_byte = text.len(); - }; + let Some(step) = step else { break }; + let (step_start_byte, step_start_point) = + step.range.start.summary::<(usize, Point)>(text); + let step_end_byte = step.range.end.to_offset(text); + let Some(grammar) = step.language.grammar.as_deref() else { continue }; let mut old_layer = cursor.item(); if let Some(layer) = old_layer { - if layer.range.to_offset(text) == (start_byte..end_byte) { + if layer.range.to_offset(text) == (step_start_byte..step_end_byte) + && layer.language.id() == step.language.id() + { cursor.next(&text); } else { old_layer = None; } } - let grammar = if let Some(grammar) = language.grammar.as_deref() { - grammar - } else { - continue; - }; - let tree; let changed_ranges; + let mut included_ranges = step.included_ranges; if let Some(old_layer) = old_layer { + if let ParseMode::Combined { + parent_layer_changed_ranges, + .. + } = step.mode + { + included_ranges = splice_included_ranges( + old_layer.tree.included_ranges(), + &parent_layer_changed_ranges, + &included_ranges, + ); + } + tree = parse_text( grammar, text.as_rope(), + step_start_byte, + step_start_point, + included_ranges, Some(old_layer.tree.clone()), - ranges, ); changed_ranges = join_ranges( - edits - .iter() - .map(|e| e.new.clone()) - .filter(|range| range.start < end_byte && range.end > start_byte), + edits.iter().map(|e| e.new.clone()).filter(|range| { + range.start <= step_end_byte && range.end >= step_start_byte + }), old_layer .tree .changed_ranges(&tree) - .map(|r| start_byte + r.start_byte..start_byte + r.end_byte), + .map(|r| step_start_byte + r.start_byte..step_start_byte + r.end_byte), ); } else { - tree = parse_text(grammar, text.as_rope(), None, ranges); - changed_ranges = vec![start_byte..end_byte]; + tree = parse_text( + grammar, + text.as_rope(), + step_start_byte, + step_start_point, + included_ranges, + None, + ); + changed_ranges = vec![step_start_byte..step_end_byte]; } layers.push( SyntaxLayer { - depth, - range, + depth: step.depth, + range: step.range, tree: tree.clone(), - language: language.clone(), + language: step.language.clone(), }, &text, ); @@ -450,11 +487,10 @@ impl SyntaxSnapshot { grammar.injection_config.as_ref().zip(registry.as_ref()), changed_ranges.is_empty(), ) { - let depth = depth + 1; for range in &changed_ranges { changed_regions.insert( ChangedRegion { - depth, + depth: step.depth + 1, range: text.anchor_before(range.start)..text.anchor_after(range.end), }, text, @@ -463,10 +499,11 @@ impl SyntaxSnapshot { get_injections( config, text, - tree.root_node_with_offset(start_byte, start_point), + tree.root_node_with_offset(step_start_byte, step_start_point.to_ts_point()), registry, - depth, + step.depth + 1, &changed_ranges, + &mut combined_injection_ranges, &mut queue, ); } @@ -547,7 +584,6 @@ impl SyntaxSnapshot { } }); - // let mut result = Vec::new(); cursor.next(buffer); std::iter::from_fn(move || { if let Some(layer) = cursor.item() { @@ -565,8 +601,6 @@ impl SyntaxSnapshot { None } }) - - // result } } @@ -892,14 +926,11 @@ fn join_ranges( fn parse_text( grammar: &Grammar, text: &Rope, - old_tree: Option, + start_byte: usize, + start_point: Point, mut ranges: Vec, + old_tree: Option, ) -> Tree { - let (start_byte, start_point) = ranges - .first() - .map(|range| (range.start_byte, Point::from_ts_point(range.start_point))) - .unwrap_or_default(); - for range in &mut ranges { range.start_byte -= start_byte; range.end_byte -= start_byte; @@ -934,14 +965,25 @@ fn get_injections( node: Node, language_registry: &LanguageRegistry, depth: usize, - query_ranges: &[Range], - queue: &mut BinaryHeap, + changed_ranges: &[Range], + combined_injection_ranges: &mut HashMap, Vec>, + queue: &mut BinaryHeap, ) -> bool { let mut result = false; let mut query_cursor = QueryCursorHandle::new(); let mut prev_match = None; - for query_range in query_ranges { - query_cursor.set_byte_range(query_range.start.saturating_sub(1)..query_range.end); + + combined_injection_ranges.clear(); + for pattern in &config.patterns { + if let (Some(language_name), true) = (pattern.language.as_ref(), pattern.combined) { + if let Some(language) = language_registry.get_language(language_name) { + combined_injection_ranges.insert(language, Vec::new()); + } + } + } + + for query_range in changed_ranges { + query_cursor.set_byte_range(query_range.start.saturating_sub(1)..query_range.end + 1); for mat in query_cursor.matches(&config.query, node, TextProvider(text.as_rope())) { let content_ranges = mat .nodes_for_capture_index(config.content_capture_ix) @@ -961,7 +1003,9 @@ fn get_injections( } prev_match = Some((mat.pattern_index, content_range.clone())); - let language_name = config.languages_by_pattern_ix[mat.pattern_index] + let combined = config.patterns[mat.pattern_index].combined; + let language_name = config.patterns[mat.pattern_index] + .language .as_ref() .map(|s| Cow::Borrowed(s.as_ref())) .or_else(|| { @@ -975,19 +1019,114 @@ fn get_injections( result = true; let range = text.anchor_before(content_range.start) ..text.anchor_after(content_range.end); - queue.push(ReparseStep { - depth, - language, - ranges: content_ranges, - range, - }) + if combined { + combined_injection_ranges + .get_mut(&language.clone()) + .unwrap() + .extend(content_ranges); + } else { + queue.push(ParseStep { + depth, + language, + included_ranges: content_ranges, + range, + mode: ParseMode::Single, + }); + } } } } } + + for (language, mut included_ranges) in combined_injection_ranges.drain() { + included_ranges.sort_unstable(); + let range = text.anchor_before(node.start_byte())..text.anchor_after(node.end_byte()); + queue.push(ParseStep { + depth, + language, + range, + included_ranges, + mode: ParseMode::Combined { + parent_layer_range: node.start_byte()..node.end_byte(), + parent_layer_changed_ranges: changed_ranges.to_vec(), + }, + }) + } + result } +fn splice_included_ranges( + mut ranges: Vec, + changed_ranges: &[Range], + new_ranges: &[tree_sitter::Range], +) -> Vec { + let mut changed_ranges = changed_ranges.into_iter().peekable(); + let mut new_ranges = new_ranges.into_iter().peekable(); + let mut ranges_ix = 0; + loop { + let new_range = new_ranges.peek(); + let mut changed_range = changed_ranges.peek(); + + // Remove ranges that have changed before inserting any new ranges + // into those ranges. + if let Some((changed, new)) = changed_range.zip(new_range) { + if new.end_byte < changed.start { + changed_range = None; + } + } + + if let Some(changed) = changed_range { + let mut start_ix = ranges_ix + + match ranges[ranges_ix..].binary_search_by_key(&changed.start, |r| r.end_byte) { + Ok(ix) | Err(ix) => ix, + }; + let mut end_ix = ranges_ix + + match ranges[ranges_ix..].binary_search_by_key(&changed.end, |r| r.start_byte) { + Ok(ix) => ix + 1, + Err(ix) => ix, + }; + + // If there are empty ranges, then there may be multiple ranges with the same + // start or end. Expand the splice to include any adjacent ranges that touch + // the changed range. + while start_ix > 0 { + if ranges[start_ix - 1].end_byte == changed.start { + start_ix -= 1; + } else { + break; + } + } + while let Some(range) = ranges.get(end_ix) { + if range.start_byte == changed.end { + end_ix += 1; + } else { + break; + } + } + + if end_ix > start_ix { + ranges.splice(start_ix..end_ix, []); + } + changed_ranges.next(); + ranges_ix = start_ix; + } else if let Some(new_range) = new_range { + let ix = ranges_ix + + match ranges[ranges_ix..] + .binary_search_by_key(&new_range.start_byte, |r| r.start_byte) + { + Ok(ix) | Err(ix) => ix, + }; + ranges.insert(ix, **new_range); + new_ranges.next(); + ranges_ix = ix + 1; + } else { + break; + } + } + ranges +} + impl std::ops::Deref for SyntaxMap { type Target = SyntaxSnapshot; @@ -996,35 +1135,43 @@ impl std::ops::Deref for SyntaxMap { } } -impl PartialEq for ReparseStep { +impl PartialEq for ParseStep { fn eq(&self, _: &Self) -> bool { false } } -impl Eq for ReparseStep {} +impl Eq for ParseStep {} -impl PartialOrd for ReparseStep { +impl PartialOrd for ParseStep { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(&other)) } } -impl Ord for ReparseStep { +impl Ord for ParseStep { fn cmp(&self, other: &Self) -> Ordering { let range_a = self.range(); let range_b = other.range(); Ord::cmp(&other.depth, &self.depth) .then_with(|| Ord::cmp(&range_b.start, &range_a.start)) .then_with(|| Ord::cmp(&range_a.end, &range_b.end)) + .then_with(|| self.language.id().cmp(&other.language.id())) } } -impl ReparseStep { +impl ParseStep { fn range(&self) -> Range { - let start = self.ranges.first().map_or(0, |r| r.start_byte); - let end = self.ranges.last().map_or(0, |r| r.end_byte); - start..end + if let ParseMode::Combined { + parent_layer_range, .. + } = &self.mode + { + parent_layer_range.clone() + } else { + let start = self.included_ranges.first().map_or(0, |r| r.start_byte); + let end = self.included_ranges.last().map_or(0, |r| r.end_byte); + start..end + } } } @@ -1039,12 +1186,17 @@ impl ChangedRegion { } impl ChangeRegionSet { - fn start_position(&self) -> DepthAndMaxPosition { - self.0 - .first() - .map_or(DepthAndMaxPosition(usize::MAX, Anchor::MAX), |region| { - DepthAndMaxPosition(region.depth, region.range.start) - }) + fn start_position(&self) -> ChangeStartPosition { + self.0.first().map_or( + ChangeStartPosition { + depth: usize::MAX, + position: Anchor::MAX, + }, + |region| ChangeStartPosition { + depth: region.depth, + position: region.range.start, + }, + ) } fn intersects(&self, layer: &SyntaxLayer, text: &BufferSnapshot) -> bool { @@ -1094,6 +1246,7 @@ impl Default for SyntaxLayerSummary { min_depth: 0, range: Anchor::MAX..Anchor::MIN, last_layer_range: Anchor::MIN..Anchor::MAX, + last_layer_language: None, } } } @@ -1114,14 +1267,15 @@ impl sum_tree::Summary for SyntaxLayerSummary { } } self.last_layer_range = other.last_layer_range.clone(); + self.last_layer_language = other.last_layer_language; } } -impl<'a> SeekTarget<'a, SyntaxLayerSummary, SyntaxLayerSummary> for DepthAndRange { +impl<'a> SeekTarget<'a, SyntaxLayerSummary, SyntaxLayerSummary> for SyntaxLayerPosition { fn cmp(&self, cursor_location: &SyntaxLayerSummary, buffer: &BufferSnapshot) -> Ordering { - Ord::cmp(&self.0, &cursor_location.max_depth) + Ord::cmp(&self.depth, &cursor_location.max_depth) .then_with(|| { - self.1 + self.range .start .cmp(&cursor_location.last_layer_range.start, buffer) }) @@ -1129,24 +1283,27 @@ impl<'a> SeekTarget<'a, SyntaxLayerSummary, SyntaxLayerSummary> for DepthAndRang cursor_location .last_layer_range .end - .cmp(&self.1.end, buffer) + .cmp(&self.range.end, buffer) }) + .then_with(|| self.language.cmp(&cursor_location.last_layer_language)) } } -impl<'a> SeekTarget<'a, SyntaxLayerSummary, SyntaxLayerSummary> for DepthAndMaxPosition { +impl<'a> SeekTarget<'a, SyntaxLayerSummary, SyntaxLayerSummary> for ChangeStartPosition { fn cmp(&self, cursor_location: &SyntaxLayerSummary, text: &BufferSnapshot) -> Ordering { - Ord::cmp(&self.0, &cursor_location.max_depth) - .then_with(|| self.1.cmp(&cursor_location.range.end, text)) + Ord::cmp(&self.depth, &cursor_location.max_depth) + .then_with(|| self.position.cmp(&cursor_location.range.end, text)) } } -impl<'a> SeekTarget<'a, SyntaxLayerSummary, SyntaxLayerSummary> for DepthAndRangeOrMaxPosition { +impl<'a> SeekTarget<'a, SyntaxLayerSummary, SyntaxLayerSummary> + for SyntaxLayerPositionBeforeChange +{ fn cmp(&self, cursor_location: &SyntaxLayerSummary, buffer: &BufferSnapshot) -> Ordering { - if self.1.cmp(cursor_location, buffer).is_le() { + if self.change.cmp(cursor_location, buffer).is_le() { return Ordering::Less; } else { - self.0.cmp(cursor_location, buffer) + self.position.cmp(cursor_location, buffer) } } } @@ -1160,6 +1317,7 @@ impl sum_tree::Item for SyntaxLayer { max_depth: self.depth, range: self.range.clone(), last_layer_range: self.range.clone(), + last_layer_language: self.language.id(), } } } @@ -1246,6 +1404,63 @@ mod tests { use unindent::Unindent as _; use util::test::marked_text_ranges; + #[test] + fn test_splice_included_ranges() { + let ranges = vec![ts_range(20..30), ts_range(50..60), ts_range(80..90)]; + + let new_ranges = splice_included_ranges( + ranges.clone(), + &[54..56, 58..68], + &[ts_range(50..54), ts_range(59..67)], + ); + assert_eq!( + new_ranges, + &[ + ts_range(20..30), + ts_range(50..54), + ts_range(59..67), + ts_range(80..90), + ] + ); + + let new_ranges = splice_included_ranges(ranges.clone(), &[70..71, 91..100], &[]); + assert_eq!( + new_ranges, + &[ts_range(20..30), ts_range(50..60), ts_range(80..90)] + ); + + let new_ranges = + splice_included_ranges(ranges.clone(), &[], &[ts_range(0..2), ts_range(70..75)]); + assert_eq!( + new_ranges, + &[ + ts_range(0..2), + ts_range(20..30), + ts_range(50..60), + ts_range(70..75), + ts_range(80..90) + ] + ); + + let new_ranges = splice_included_ranges(ranges.clone(), &[30..50], &[ts_range(25..55)]); + assert_eq!(new_ranges, &[ts_range(25..55), ts_range(80..90)]); + + fn ts_range(range: Range) -> tree_sitter::Range { + tree_sitter::Range { + start_byte: range.start, + start_point: tree_sitter::Point { + row: 0, + column: range.start, + }, + end_byte: range.end, + end_point: tree_sitter::Point { + row: 0, + column: range.end, + }, + } + } + } + #[gpui::test] fn test_syntax_map_layers_for_range() { let registry = Arc::new(LanguageRegistry::test()); @@ -1345,21 +1560,24 @@ mod tests { #[gpui::test] fn test_typing_multiple_new_injections() { - let (buffer, syntax_map) = test_edit_sequence(&[ - "fn a() { dbg }", - "fn a() { dbg«!» }", - "fn a() { dbg!«()» }", - "fn a() { dbg!(«b») }", - "fn a() { dbg!(b«.») }", - "fn a() { dbg!(b.«c») }", - "fn a() { dbg!(b.c«()») }", - "fn a() { dbg!(b.c(«vec»)) }", - "fn a() { dbg!(b.c(vec«!»)) }", - "fn a() { dbg!(b.c(vec!«[]»)) }", - "fn a() { dbg!(b.c(vec![«d»])) }", - "fn a() { dbg!(b.c(vec![d«.»])) }", - "fn a() { dbg!(b.c(vec![d.«e»])) }", - ]); + let (buffer, syntax_map) = test_edit_sequence( + "Rust", + &[ + "fn a() { dbg }", + "fn a() { dbg«!» }", + "fn a() { dbg!«()» }", + "fn a() { dbg!(«b») }", + "fn a() { dbg!(b«.») }", + "fn a() { dbg!(b.«c») }", + "fn a() { dbg!(b.c«()») }", + "fn a() { dbg!(b.c(«vec»)) }", + "fn a() { dbg!(b.c(vec«!»)) }", + "fn a() { dbg!(b.c(vec!«[]»)) }", + "fn a() { dbg!(b.c(vec![«d»])) }", + "fn a() { dbg!(b.c(vec![d«.»])) }", + "fn a() { dbg!(b.c(vec![d.«e»])) }", + ], + ); assert_capture_ranges( &syntax_map, @@ -1371,29 +1589,32 @@ mod tests { #[gpui::test] fn test_pasting_new_injection_line_between_others() { - let (buffer, syntax_map) = test_edit_sequence(&[ - " - fn a() { - b!(B {}); - c!(C {}); - d!(D {}); - e!(E {}); - f!(F {}); - g!(G {}); - } - ", - " - fn a() { - b!(B {}); - c!(C {}); - d!(D {}); - « h!(H {}); - » e!(E {}); - f!(F {}); - g!(G {}); - } - ", - ]); + let (buffer, syntax_map) = test_edit_sequence( + "Rust", + &[ + " + fn a() { + b!(B {}); + c!(C {}); + d!(D {}); + e!(E {}); + f!(F {}); + g!(G {}); + } + ", + " + fn a() { + b!(B {}); + c!(C {}); + d!(D {}); + « h!(H {}); + » e!(E {}); + f!(F {}); + g!(G {}); + } + ", + ], + ); assert_capture_ranges( &syntax_map, @@ -1415,28 +1636,31 @@ mod tests { #[gpui::test] fn test_joining_injections_with_child_injections() { - let (buffer, syntax_map) = test_edit_sequence(&[ - " - fn a() { - b!( - c![one.two.three], - d![four.five.six], - ); - e!( - f![seven.eight], - ); - } - ", - " - fn a() { - b!( - c![one.two.three], - d![four.five.six], - ˇ f![seven.eight], - ); - } - ", - ]); + let (buffer, syntax_map) = test_edit_sequence( + "Rust", + &[ + " + fn a() { + b!( + c![one.two.three], + d![four.five.six], + ); + e!( + f![seven.eight], + ); + } + ", + " + fn a() { + b!( + c![one.two.three], + d![four.five.six], + ˇ f![seven.eight], + ); + } + ", + ], + ); assert_capture_ranges( &syntax_map, @@ -1456,131 +1680,270 @@ mod tests { #[gpui::test] fn test_editing_edges_of_injection() { - test_edit_sequence(&[ - " - fn a() { - b!(c!()) - } + test_edit_sequence( + "Rust", + &[ + " + fn a() { + b!(c!()) + } + ", + " + fn a() { + «d»!(c!()) + } + ", + " + fn a() { + «e»d!(c!()) + } + ", + " + fn a() { + ed!«[»c!()«]» + } ", - " - fn a() { - «d»!(c!()) - } - ", - " - fn a() { - «e»d!(c!()) - } - ", - " - fn a() { - ed!«[»c!()«]» - } - ", - ]); + ], + ); } #[gpui::test] fn test_edits_preceding_and_intersecting_injection() { - test_edit_sequence(&[ - // - "const aaaaaaaaaaaa: B = c!(d(e.f));", - "const aˇa: B = c!(d(eˇ));", - ]); + test_edit_sequence( + "Rust", + &[ + // + "const aaaaaaaaaaaa: B = c!(d(e.f));", + "const aˇa: B = c!(d(eˇ));", + ], + ); } #[gpui::test] fn test_non_local_changes_create_injections() { - test_edit_sequence(&[ - " - // a! { - static B: C = d; - // } - ", - " - ˇa! { - static B: C = d; - ˇ} - ", - ]); + test_edit_sequence( + "Rust", + &[ + " + // a! { + static B: C = d; + // } + ", + " + ˇa! { + static B: C = d; + ˇ} + ", + ], + ); } #[gpui::test] fn test_creating_many_injections_in_one_edit() { - test_edit_sequence(&[ - " - fn a() { - one(Two::three(3)); - four(Five::six(6)); - seven(Eight::nine(9)); - } - ", - " - fn a() { - one«!»(Two::three(3)); - four«!»(Five::six(6)); - seven«!»(Eight::nine(9)); - } - ", - " - fn a() { - one!(Two::three«!»(3)); - four!(Five::six«!»(6)); - seven!(Eight::nine«!»(9)); - } - ", - ]); + test_edit_sequence( + "Rust", + &[ + " + fn a() { + one(Two::three(3)); + four(Five::six(6)); + seven(Eight::nine(9)); + } + ", + " + fn a() { + one«!»(Two::three(3)); + four«!»(Five::six(6)); + seven«!»(Eight::nine(9)); + } + ", + " + fn a() { + one!(Two::three«!»(3)); + four!(Five::six«!»(6)); + seven!(Eight::nine«!»(9)); + } + ", + ], + ); } #[gpui::test] fn test_editing_across_injection_boundary() { - test_edit_sequence(&[ - " - fn one() { - two(); - three!( - three.four, - five.six, - ); - } - ", - " - fn one() { - two(); - th«irty_five![» - three.four, - five.six, - « seven.eight, - ];» - } - ", - ]); + test_edit_sequence( + "Rust", + &[ + " + fn one() { + two(); + three!( + three.four, + five.six, + ); + } + ", + " + fn one() { + two(); + th«irty_five![» + three.four, + five.six, + « seven.eight, + ];» + } + ", + ], + ); } #[gpui::test] fn test_removing_injection_by_replacing_across_boundary() { - test_edit_sequence(&[ - " - fn one() { - two!( - three.four, - ); - } - ", - " - fn one() { - t«en - .eleven( - twelve, - » - three.four, - ); - } - ", - ]); + test_edit_sequence( + "Rust", + &[ + " + fn one() { + two!( + three.four, + ); + } + ", + " + fn one() { + t«en + .eleven( + twelve, + » + three.four, + ); + } + ", + ], + ); } - #[gpui::test(iterations = 100)] + #[gpui::test] + fn test_combined_injections() { + let (buffer, syntax_map) = test_edit_sequence( + "ERB", + &[ + " + + <% if @one %> +
+ <% else %> +
+ <% end %> +
+ + ", + " + + <% if @one %> +
+ ˇ else ˇ +
+ <% end %> +
+ + ", + " + + <% if @one «;» end %> +
+ + ", + ], + ); + + assert_capture_ranges( + &syntax_map, + &buffer, + &["tag", "ivar"], + " + <«body»> + <% if «@one» ; end %> + + + ", + ); + } + + #[gpui::test] + fn test_combined_injections_empty_ranges() { + test_edit_sequence( + "ERB", + &[ + " + <% if @one %> + <% else %> + <% end %> + ", + " + <% if @one %> + ˇ<% end %> + ", + ], + ); + } + + #[gpui::test] + fn test_combined_injections_edit_edges_of_ranges() { + let (buffer, syntax_map) = test_edit_sequence( + "ERB", + &[ + " + <%= one @two %> + <%= three @four %> + ", + " + <%= one @two %ˇ + <%= three @four %> + ", + " + <%= one @two %«>» + <%= three @four %> + ", + ], + ); + + assert_capture_ranges( + &syntax_map, + &buffer, + &["tag", "ivar"], + " + <%= one «@two» %> + <%= three «@four» %> + ", + ); + } + + #[gpui::test] + fn test_combined_injections_splitting_some_injections() { + let (_buffer, _syntax_map) = test_edit_sequence( + "ERB", + &[ + r#" + <%A if b(:c) %> + d + <% end %> + eee + <% f %> + "#, + r#" + <%« AAAAAAA %> + hhhhhhh + <%=» if b(:c) %> + d + <% end %> + eee + <% f %> + "#, + ], + ); + } + + #[gpui::test(iterations = 50)] fn test_random_syntax_map_edits(mut rng: StdRng) { let operations = env::var("OPERATIONS") .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) @@ -1660,6 +2023,89 @@ mod tests { } } + #[gpui::test(iterations = 50)] + fn test_random_syntax_map_edits_with_combined_injections(mut rng: StdRng) { + let operations = env::var("OPERATIONS") + .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) + .unwrap_or(10); + + let text = r#" +
+ <% if one?(:two) %> +

+ <%= yield :five %> +

+ <% elsif Six.seven(8) %> +

+ <%= yield :five %> +

+ <% else %> + Ok + <% end %> +
+ "# + .unindent() + .repeat(8); + + let registry = Arc::new(LanguageRegistry::test()); + let language = Arc::new(erb_lang()); + registry.add(language.clone()); + registry.add(Arc::new(ruby_lang())); + registry.add(Arc::new(html_lang())); + let mut buffer = Buffer::new(0, 0, text); + + let mut syntax_map = SyntaxMap::new(); + syntax_map.set_language_registry(registry.clone()); + syntax_map.reparse(language.clone(), &buffer); + + let mut reference_syntax_map = SyntaxMap::new(); + reference_syntax_map.set_language_registry(registry.clone()); + + log::info!("initial text:\n{}", buffer.text()); + + for _ in 0..operations { + let prev_buffer = buffer.snapshot(); + let prev_syntax_map = syntax_map.snapshot(); + + buffer.randomly_edit(&mut rng, 3); + log::info!("text:\n{}", buffer.text()); + + syntax_map.interpolate(&buffer); + check_interpolation(&prev_syntax_map, &syntax_map, &prev_buffer, &buffer); + + syntax_map.reparse(language.clone(), &buffer); + + reference_syntax_map.clear(); + reference_syntax_map.reparse(language.clone(), &buffer); + } + + for i in 0..operations { + let i = operations - i - 1; + buffer.undo(); + log::info!("undoing operation {}", i); + log::info!("text:\n{}", buffer.text()); + + syntax_map.interpolate(&buffer); + syntax_map.reparse(language.clone(), &buffer); + + reference_syntax_map.clear(); + reference_syntax_map.reparse(language.clone(), &buffer); + assert_eq!( + syntax_map.layers(&buffer).len(), + reference_syntax_map.layers(&buffer).len(), + "wrong number of layers after undoing edit {i}" + ); + } + + let layers = syntax_map.layers(&buffer); + let reference_layers = reference_syntax_map.layers(&buffer); + for (edited_layer, reference_layer) in layers.into_iter().zip(reference_layers.into_iter()) + { + assert_eq!(edited_layer.node.to_sexp(), reference_layer.node.to_sexp()); + assert_eq!(edited_layer.node.range(), reference_layer.node.range()); + } + } + fn check_interpolation( old_syntax_map: &SyntaxSnapshot, new_syntax_map: &SyntaxSnapshot, @@ -1767,10 +2213,13 @@ mod tests { } } - fn test_edit_sequence(steps: &[&str]) -> (Buffer, SyntaxMap) { + fn test_edit_sequence(language_name: &str, steps: &[&str]) -> (Buffer, SyntaxMap) { let registry = Arc::new(LanguageRegistry::test()); - let language = Arc::new(rust_lang()); - registry.add(language.clone()); + registry.add(Arc::new(rust_lang())); + registry.add(Arc::new(ruby_lang())); + registry.add(Arc::new(html_lang())); + registry.add(Arc::new(erb_lang())); + let language = registry.get_language(language_name).unwrap(); let mut buffer = Buffer::new(0, 0, Default::default()); let mut mutated_syntax_map = SyntaxMap::new(); @@ -1816,6 +2265,72 @@ mod tests { (buffer, mutated_syntax_map) } + fn html_lang() -> Language { + Language::new( + LanguageConfig { + name: "HTML".into(), + path_suffixes: vec!["html".to_string()], + ..Default::default() + }, + Some(tree_sitter_html::language()), + ) + .with_highlights_query( + r#" + (tag_name) @tag + (erroneous_end_tag_name) @tag + (attribute_name) @property + "#, + ) + .unwrap() + } + + fn ruby_lang() -> Language { + Language::new( + LanguageConfig { + name: "Ruby".into(), + path_suffixes: vec!["rb".to_string()], + ..Default::default() + }, + Some(tree_sitter_ruby::language()), + ) + .with_highlights_query( + r#" + ["if" "do" "else" "end"] @keyword + (instance_variable) @ivar + "#, + ) + .unwrap() + } + + fn erb_lang() -> Language { + Language::new( + LanguageConfig { + name: "ERB".into(), + path_suffixes: vec!["erb".to_string()], + ..Default::default() + }, + Some(tree_sitter_embedded_template::language()), + ) + .with_highlights_query( + r#" + ["<%" "%>"] @keyword + "#, + ) + .unwrap() + .with_injection_query( + r#" + ((code) @content + (#set! "language" "ruby") + (#set! "combined")) + + ((content) @content + (#set! "language" "html") + (#set! "combined")) + "#, + ) + .unwrap() + } + fn rust_lang() -> Language { Language::new( LanguageConfig { diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 37f6e76340..3ea1261735 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -128,8 +128,8 @@ impl LspCommand for PrepareRename { ) = message { let Range { start, end } = range_from_lsp(range); - if buffer.clip_point_utf16(start, Bias::Left) == start - && buffer.clip_point_utf16(end, Bias::Left) == end + if buffer.clip_point_utf16(start, Bias::Left) == start.0 + && buffer.clip_point_utf16(end, Bias::Left) == end.0 { return Ok(Some(buffer.anchor_after(start)..buffer.anchor_before(end))); } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index a3439430fd..c9f674b8c9 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -30,6 +30,7 @@ use language::{ CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Event as BufferEvent, File as _, Language, LanguageRegistry, LanguageServerName, LocalFile, OffsetRangeExt, Operation, Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, + Unclipped, }; use lsp::{ DiagnosticSeverity, DiagnosticTag, DocumentHighlightKind, LanguageServer, LanguageString, @@ -251,7 +252,7 @@ pub struct Symbol { pub label: CodeLabel, pub name: String, pub kind: lsp::SymbolKind, - pub range: Range, + pub range: Range>, pub signature: [u8; 32], } @@ -2582,7 +2583,7 @@ impl Project { language_server_id: usize, abs_path: PathBuf, version: Option, - diagnostics: Vec>, + diagnostics: Vec>>, cx: &mut ModelContext, ) -> Result<(), anyhow::Error> { let (worktree, relative_path) = self @@ -2620,7 +2621,7 @@ impl Project { fn update_buffer_diagnostics( &mut self, buffer: &ModelHandle, - mut diagnostics: Vec>, + mut diagnostics: Vec>>, version: Option, cx: &mut ModelContext, ) -> Result<()> { @@ -2644,7 +2645,7 @@ impl Project { let mut sanitized_diagnostics = Vec::new(); let edits_since_save = Patch::new( snapshot - .edits_since::(buffer.read(cx).saved_version()) + .edits_since::>(buffer.read(cx).saved_version()) .collect(), ); for entry in diagnostics { @@ -2664,13 +2665,14 @@ impl Project { let mut range = snapshot.clip_point_utf16(start, Bias::Left) ..snapshot.clip_point_utf16(end, Bias::Right); - // Expand empty ranges by one character + // Expand empty ranges by one codepoint if range.start == range.end { + // This will be go to the next boundary when being clipped range.end.column += 1; - range.end = snapshot.clip_point_utf16(range.end, Bias::Right); + range.end = snapshot.clip_point_utf16(Unclipped(range.end), Bias::Right); if range.start == range.end && range.end.column > 0 { range.start.column -= 1; - range.start = snapshot.clip_point_utf16(range.start, Bias::Left); + range.end = snapshot.clip_point_utf16(Unclipped(range.end), Bias::Left); } } @@ -3273,7 +3275,7 @@ impl Project { return Task::ready(Ok(Default::default())); }; - let position = position.to_point_utf16(source_buffer); + let position = Unclipped(position.to_point_utf16(source_buffer)); let anchor = source_buffer.anchor_after(position); if worktree.read(cx).as_local().is_some() { @@ -3292,7 +3294,7 @@ impl Project { lsp::TextDocumentIdentifier::new( lsp::Url::from_file_path(buffer_abs_path).unwrap(), ), - point_to_lsp(position), + point_to_lsp(position.0), ), context: Default::default(), work_done_progress_params: Default::default(), @@ -3314,88 +3316,91 @@ impl Project { let snapshot = this.snapshot(); let clipped_position = this.clip_point_utf16(position, Bias::Left); let mut range_for_token = None; - completions.into_iter().filter_map(move |lsp_completion| { - // For now, we can only handle additional edits if they are returned - // when resolving the completion, not if they are present initially. - if lsp_completion - .additional_text_edits - .as_ref() - .map_or(false, |edits| !edits.is_empty()) - { - return None; - } - - let (old_range, mut new_text) = match lsp_completion.text_edit.as_ref() { - // If the language server provides a range to overwrite, then - // check that the range is valid. - Some(lsp::CompletionTextEdit::Edit(edit)) => { - let range = range_from_lsp(edit.range); - let start = snapshot.clip_point_utf16(range.start, Bias::Left); - let end = snapshot.clip_point_utf16(range.end, Bias::Left); - if start != range.start || end != range.end { - log::info!("completion out of expected range"); - return None; - } - ( - snapshot.anchor_before(start)..snapshot.anchor_after(end), - edit.new_text.clone(), - ) - } - // If the language server does not provide a range, then infer - // the range based on the syntax tree. - None => { - if position != clipped_position { - log::info!("completion out of expected range"); - return None; - } - let Range { start, end } = range_for_token - .get_or_insert_with(|| { - let offset = position.to_offset(&snapshot); - let (range, kind) = snapshot.surrounding_word(offset); - if kind == Some(CharKind::Word) { - range - } else { - offset..offset - } - }) - .clone(); - let text = lsp_completion - .insert_text - .as_ref() - .unwrap_or(&lsp_completion.label) - .clone(); - ( - snapshot.anchor_before(start)..snapshot.anchor_after(end), - text, - ) - } - Some(lsp::CompletionTextEdit::InsertAndReplace(_)) => { - log::info!("unsupported insert/replace completion"); + completions + .into_iter() + .filter_map(move |mut lsp_completion| { + // For now, we can only handle additional edits if they are returned + // when resolving the completion, not if they are present initially. + if lsp_completion + .additional_text_edits + .as_ref() + .map_or(false, |edits| !edits.is_empty()) + { return None; } - }; - LineEnding::normalize(&mut new_text); - let language = language.clone(); - Some(async move { - let label = if let Some(language) = language { - language.label_for_completion(&lsp_completion).await - } else { - None - }; - Completion { - old_range, - new_text, - label: label.unwrap_or_else(|| { - CodeLabel::plain( - lsp_completion.label.clone(), - lsp_completion.filter_text.as_deref(), + let (old_range, mut new_text) = match lsp_completion.text_edit.as_ref() + { + // If the language server provides a range to overwrite, then + // check that the range is valid. + Some(lsp::CompletionTextEdit::Edit(edit)) => { + let range = range_from_lsp(edit.range); + let start = snapshot.clip_point_utf16(range.start, Bias::Left); + let end = snapshot.clip_point_utf16(range.end, Bias::Left); + if start != range.start.0 || end != range.end.0 { + log::info!("completion out of expected range"); + return None; + } + ( + snapshot.anchor_before(start)..snapshot.anchor_after(end), + edit.new_text.clone(), ) - }), - lsp_completion, - } + } + // If the language server does not provide a range, then infer + // the range based on the syntax tree. + None => { + if position.0 != clipped_position { + log::info!("completion out of expected range"); + return None; + } + let Range { start, end } = range_for_token + .get_or_insert_with(|| { + let offset = position.to_offset(&snapshot); + let (range, kind) = snapshot.surrounding_word(offset); + if kind == Some(CharKind::Word) { + range + } else { + offset..offset + } + }) + .clone(); + let text = lsp_completion + .insert_text + .as_ref() + .unwrap_or(&lsp_completion.label) + .clone(); + ( + snapshot.anchor_before(start)..snapshot.anchor_after(end), + text, + ) + } + Some(lsp::CompletionTextEdit::InsertAndReplace(_)) => { + log::info!("unsupported insert/replace completion"); + return None; + } + }; + + LineEnding::normalize(&mut new_text); + let language = language.clone(); + Some(async move { + let mut label = None; + if let Some(language) = language { + language.process_completion(&mut lsp_completion).await; + label = language.label_for_completion(&lsp_completion).await; + } + Completion { + old_range, + new_text, + label: label.unwrap_or_else(|| { + CodeLabel::plain( + lsp_completion.label.clone(), + lsp_completion.filter_text.as_deref(), + ) + }), + lsp_completion, + } + }) }) - }) }); Ok(futures::future::join_all(completions).await) @@ -3448,29 +3453,41 @@ impl Project { let buffer_id = buffer.remote_id(); if self.is_local() { - let lang_server = if let Some((_, server)) = self.language_server_for_buffer(buffer, cx) - { - server.clone() - } else { - return Task::ready(Ok(Default::default())); + let lang_server = match self.language_server_for_buffer(buffer, cx) { + Some((_, server)) => server.clone(), + _ => return Task::ready(Ok(Default::default())), }; cx.spawn(|this, mut cx| async move { let resolved_completion = lang_server .request::(completion.lsp_completion) .await?; + if let Some(edits) = resolved_completion.additional_text_edits { let edits = this .update(&mut cx, |this, cx| { this.edits_from_lsp(&buffer_handle, edits, None, cx) }) .await?; + buffer_handle.update(&mut cx, |buffer, cx| { buffer.finalize_last_transaction(); buffer.start_transaction(); + for (range, text) in edits { - buffer.edit([(range, text)], None, cx); + let primary = &completion.old_range; + let start_within = primary.start.cmp(&range.start, buffer).is_le() + && primary.end.cmp(&range.start, buffer).is_ge(); + let end_within = range.start.cmp(&primary.end, buffer).is_le() + && range.end.cmp(&primary.end, buffer).is_ge(); + + //Skip addtional edits which overlap with the primary completion edit + //https://github.com/zed-industries/zed/pull/1871 + if !start_within && !end_within { + buffer.edit([(range, text)], None, cx); + } } + let transaction = if buffer.end_transaction(cx).is_some() { let transaction = buffer.finalize_last_transaction().unwrap().clone(); if !push_to_history { @@ -3568,7 +3585,13 @@ impl Project { partial_result_params: Default::default(), context: lsp::CodeActionContext { diagnostics: relevant_diagnostics, - only: None, + only: Some(vec![ + lsp::CodeActionKind::EMPTY, + lsp::CodeActionKind::QUICKFIX, + lsp::CodeActionKind::REFACTOR, + lsp::CodeActionKind::REFACTOR_EXTRACT, + lsp::CodeActionKind::SOURCE, + ]), }, }) .await? @@ -5111,22 +5134,30 @@ impl Project { _: Arc, mut cx: AsyncAppContext, ) -> Result { - let position = envelope - .payload - .position - .and_then(language::proto::deserialize_anchor) - .ok_or_else(|| anyhow!("invalid position"))?; - let version = deserialize_version(envelope.payload.version); let buffer = this.read_with(&cx, |this, cx| { this.opened_buffers .get(&envelope.payload.buffer_id) .and_then(|buffer| buffer.upgrade(cx)) .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id)) })?; + + let position = envelope + .payload + .position + .and_then(language::proto::deserialize_anchor) + .map(|p| { + buffer.read_with(&cx, |buffer, _| { + buffer.clip_point_utf16(Unclipped(p.to_point_utf16(buffer)), Bias::Left) + }) + }) + .ok_or_else(|| anyhow!("invalid position"))?; + + let version = deserialize_version(envelope.payload.version); buffer .update(&mut cx, |buffer, _| buffer.wait_for_version(version)) .await; let version = buffer.read_with(&cx, |buffer, _| buffer.version()); + let completions = this .update(&mut cx, |this, cx| this.completions(&buffer, position, cx)) .await?; @@ -5613,8 +5644,8 @@ impl Project { }, name: serialized_symbol.name, - range: PointUtf16::new(start.row, start.column) - ..PointUtf16::new(end.row, end.column), + range: Unclipped(PointUtf16::new(start.row, start.column)) + ..Unclipped(PointUtf16::new(end.row, end.column)), kind, signature: serialized_symbol .signature @@ -5700,10 +5731,10 @@ impl Project { let mut lsp_edits = lsp_edits.into_iter().peekable(); let mut edits = Vec::new(); - while let Some((mut range, mut new_text)) = lsp_edits.next() { + while let Some((range, mut new_text)) = lsp_edits.next() { // Clip invalid ranges provided by the language server. - range.start = snapshot.clip_point_utf16(range.start, Bias::Left); - range.end = snapshot.clip_point_utf16(range.end, Bias::Left); + let mut range = snapshot.clip_point_utf16(range.start, Bias::Left) + ..snapshot.clip_point_utf16(range.end, Bias::Left); // Combine any LSP edits that are adjacent. // @@ -5715,11 +5746,11 @@ impl Project { // In order for the diffing logic below to work properly, any edits that // cancel each other out must be combined into one. while let Some((next_range, next_text)) = lsp_edits.peek() { - if next_range.start > range.end { - if next_range.start.row > range.end.row + 1 - || next_range.start.column > 0 + if next_range.start.0 > range.end { + if next_range.start.0.row > range.end.row + 1 + || next_range.start.0.column > 0 || snapshot.clip_point_utf16( - PointUtf16::new(range.end.row, u32::MAX), + Unclipped(PointUtf16::new(range.end.row, u32::MAX)), Bias::Left, ) > range.end { @@ -5727,7 +5758,7 @@ impl Project { } new_text.push('\n'); } - range.end = next_range.end; + range.end = snapshot.clip_point_utf16(next_range.end, Bias::Left); new_text.push_str(next_text); lsp_edits.next(); } @@ -6000,13 +6031,13 @@ fn serialize_symbol(symbol: &Symbol) -> proto::Symbol { path: symbol.path.path.to_string_lossy().to_string(), name: symbol.name.clone(), kind: unsafe { mem::transmute(symbol.kind) }, - start: Some(proto::Point { - row: symbol.range.start.row, - column: symbol.range.start.column, + start: Some(proto::PointUtf16 { + row: symbol.range.start.0.row, + column: symbol.range.start.0.column, }), - end: Some(proto::Point { - row: symbol.range.end.row, - column: symbol.range.end.column, + end: Some(proto::PointUtf16 { + row: symbol.range.end.0.row, + column: symbol.range.end.0.column, }), signature: symbol.signature.to_vec(), } diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 77d2a610d5..a36831857f 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -1239,7 +1239,7 @@ async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) { &buffer, vec![ DiagnosticEntry { - range: PointUtf16::new(0, 10)..PointUtf16::new(0, 10), + range: Unclipped(PointUtf16::new(0, 10))..Unclipped(PointUtf16::new(0, 10)), diagnostic: Diagnostic { severity: DiagnosticSeverity::ERROR, message: "syntax error 1".to_string(), @@ -1247,7 +1247,7 @@ async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) { }, }, DiagnosticEntry { - range: PointUtf16::new(1, 10)..PointUtf16::new(1, 10), + range: Unclipped(PointUtf16::new(1, 10))..Unclipped(PointUtf16::new(1, 10)), diagnostic: Diagnostic { severity: DiagnosticSeverity::ERROR, message: "syntax error 2".to_string(), diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 791cd1d622..409f65f786 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -20,6 +20,7 @@ use gpui::{ executor, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task, }; +use language::Unclipped; use language::{ proto::{deserialize_version, serialize_line_ending, serialize_version}, Buffer, DiagnosticEntry, PointUtf16, Rope, @@ -64,7 +65,7 @@ pub struct LocalWorktree { _background_scanner_task: Option>, poll_task: Option>, share: Option, - diagnostics: HashMap, Vec>>, + diagnostics: HashMap, Vec>>>, diagnostic_summaries: TreeMap, client: Arc, fs: Arc, @@ -502,7 +503,10 @@ impl LocalWorktree { }) } - pub fn diagnostics_for_path(&self, path: &Path) -> Option>> { + pub fn diagnostics_for_path( + &self, + path: &Path, + ) -> Option>>> { self.diagnostics.get(path).cloned() } @@ -510,7 +514,7 @@ impl LocalWorktree { &mut self, language_server_id: usize, worktree_path: Arc, - diagnostics: Vec>, + diagnostics: Vec>>, _: &mut ModelContext, ) -> Result { self.diagnostics.remove(&worktree_path); @@ -1168,6 +1172,10 @@ impl Snapshot { self.id } + pub fn abs_path(&self) -> &Arc { + &self.abs_path + } + pub fn contains_entry(&self, entry_id: ProjectEntryId) -> bool { self.entries_by_id.get(&entry_id, &()).is_some() } @@ -1359,10 +1367,6 @@ impl Snapshot { } impl LocalSnapshot { - pub fn abs_path(&self) -> &Arc { - &self.abs_path - } - pub fn extension_counts(&self) -> &HashMap { &self.extension_counts } diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 3eb5d68516..b6787c930c 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -43,6 +43,7 @@ pub struct ProjectPanel { filename_editor: ViewHandle, clipboard_entry: Option, context_menu: ViewHandle, + dragged_entry_destination: Option>, } #[derive(Copy, Clone)] @@ -95,6 +96,13 @@ pub struct Open { pub change_focus: bool, } +#[derive(Clone, PartialEq)] +pub struct MoveProjectEntry { + pub entry_to_move: ProjectEntryId, + pub destination: ProjectEntryId, + pub destination_is_file: bool, +} + #[derive(Clone, PartialEq)] pub struct DeployContextMenu { pub position: Vector2F, @@ -117,7 +125,10 @@ actions!( ToggleFocus ] ); -impl_internal_actions!(project_panel, [Open, ToggleExpanded, DeployContextMenu]); +impl_internal_actions!( + project_panel, + [Open, ToggleExpanded, DeployContextMenu, MoveProjectEntry] +); pub fn init(cx: &mut MutableAppContext) { cx.add_action(ProjectPanel::deploy_context_menu); @@ -141,6 +152,7 @@ pub fn init(cx: &mut MutableAppContext) { this.paste(action, cx); }, ); + cx.add_action(ProjectPanel::move_entry); } pub enum Event { @@ -219,6 +231,7 @@ impl ProjectPanel { filename_editor, clipboard_entry: None, context_menu: cx.add_view(ContextMenu::new), + dragged_entry_destination: None, }; this.update_visible_entries(None, cx); this @@ -774,6 +787,39 @@ impl ProjectPanel { } } + fn move_entry( + &mut self, + &MoveProjectEntry { + entry_to_move, + destination, + destination_is_file, + }: &MoveProjectEntry, + cx: &mut ViewContext, + ) { + let destination_worktree = self.project.update(cx, |project, cx| { + let entry_path = project.path_for_entry(entry_to_move, cx)?; + let destination_entry_path = project.path_for_entry(destination, cx)?.path.clone(); + + let mut destination_path = destination_entry_path.as_ref(); + if destination_is_file { + destination_path = destination_path.parent()?; + } + + let mut new_path = destination_path.to_path_buf(); + new_path.push(entry_path.path.file_name()?); + if new_path != entry_path.path.as_ref() { + let task = project.rename_entry(entry_to_move, new_path, cx)?; + cx.foreground().spawn(task).detach_and_log_err(cx); + } + + Some(project.worktree_id_for_entry(destination, cx)?) + }); + + if let Some(destination_worktree) = destination_worktree { + self.expand_entry(destination_worktree, destination, cx); + } + } + fn index_for_selection(&self, selection: Selection) -> Option<(usize, usize, usize)> { let mut entry_index = 0; let mut visible_entries_index = 0; @@ -1079,10 +1125,13 @@ impl ProjectPanel { entry_id: ProjectEntryId, details: EntryDetails, editor: &ViewHandle, + dragged_entry_destination: &mut Option>, theme: &theme::ProjectPanel, cx: &mut RenderContext, ) -> ElementBox { + let this = cx.handle(); let kind = details.kind; + let path = details.path.clone(); let padding = theme.container.padding.left + details.depth as f32 * theme.indent_width; let entry_style = if details.is_cut { @@ -1096,7 +1145,20 @@ impl ProjectPanel { let show_editor = details.is_editing && !details.is_processing; MouseEventHandler::::new(entry_id.to_usize(), cx, |state, cx| { - let style = entry_style.style_for(state, details.is_selected).clone(); + let mut style = entry_style.style_for(state, details.is_selected).clone(); + + if cx + .global::>() + .currently_dragged::(cx.window_id()) + .is_some() + && dragged_entry_destination + .as_ref() + .filter(|destination| details.path.starts_with(destination)) + .is_some() + { + style = entry_style.active.clone().unwrap(); + } + let row_container_style = if show_editor { theme.filename_editor.container } else { @@ -1128,6 +1190,35 @@ impl ProjectPanel { position: e.position, }) }) + .on_up(MouseButton::Left, move |_, cx| { + if let Some((_, dragged_entry)) = cx + .global::>() + .currently_dragged::(cx.window_id()) + { + cx.dispatch_action(MoveProjectEntry { + entry_to_move: *dragged_entry, + destination: entry_id, + destination_is_file: matches!(details.kind, EntryKind::File(_)), + }); + } + }) + .on_move(move |_, cx| { + if cx + .global::>() + .currently_dragged::(cx.window_id()) + .is_some() + { + if let Some(this) = this.upgrade(cx.app) { + this.update(cx.app, |this, _| { + this.dragged_entry_destination = if matches!(kind, EntryKind::File(_)) { + path.parent().map(|parent| Arc::from(parent)) + } else { + Some(path.clone()) + }; + }) + } + } + }) .as_draggable(entry_id, { let row_container_style = theme.dragged_entry.container; @@ -1154,14 +1245,15 @@ impl View for ProjectPanel { } fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox { - enum Tag {} + enum ProjectPanel {} let theme = &cx.global::().theme.project_panel; let mut container_style = theme.container; let padding = std::mem::take(&mut container_style.padding); let last_worktree_root_id = self.last_worktree_root_id; + Stack::new() .with_child( - MouseEventHandler::::new(0, cx, |_, cx| { + MouseEventHandler::::new(0, cx, |_, cx| { UniformList::new( self.list.clone(), self.visible_entries @@ -1171,15 +1263,19 @@ impl View for ProjectPanel { cx, move |this, range, items, cx| { let theme = cx.global::().theme.clone(); + let mut dragged_entry_destination = + this.dragged_entry_destination.clone(); this.for_each_visible_entry(range, cx, |id, details, cx| { items.push(Self::render_entry( id, details, &this.filename_editor, + &mut dragged_entry_destination, &theme.project_panel, cx, )); }); + this.dragged_entry_destination = dragged_entry_destination; }, ) .with_padding_top(padding.top) diff --git a/crates/project_symbols/Cargo.toml b/crates/project_symbols/Cargo.toml index cb1f186bde..a426e2e0d4 100644 --- a/crates/project_symbols/Cargo.toml +++ b/crates/project_symbols/Cargo.toml @@ -28,4 +28,4 @@ settings = { path = "../settings", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } language = { path = "../language", features = ["test-support"] } lsp = { path = "../lsp", features = ["test-support"] } -project = { path = "../project", features = ["test-support"] } \ No newline at end of file +project = { path = "../project", features = ["test-support"] } diff --git a/crates/rope/Cargo.toml b/crates/rope/Cargo.toml index 0f754c1fb3..bd1dc690db 100644 --- a/crates/rope/Cargo.toml +++ b/crates/rope/Cargo.toml @@ -12,7 +12,7 @@ smallvec = { version = "1.6", features = ["union"] } sum_tree = { path = "../sum_tree" } arrayvec = "0.7.1" log = { version = "0.4.16", features = ["kv_unstable_serde"] } - +util = { path = "../util" } [dev-dependencies] rand = "0.8.3" diff --git a/crates/rope/src/rope.rs b/crates/rope/src/rope.rs index 8c357801e3..53713e3f7a 100644 --- a/crates/rope/src/rope.rs +++ b/crates/rope/src/rope.rs @@ -1,16 +1,23 @@ mod offset_utf16; mod point; mod point_utf16; +mod unclipped; use arrayvec::ArrayString; use bromberg_sl2::{DigestString, HashMatrix}; use smallvec::SmallVec; -use std::{cmp, fmt, io, mem, ops::Range, str}; +use std::{ + cmp, fmt, io, mem, + ops::{AddAssign, Range}, + str, +}; use sum_tree::{Bias, Dimension, SumTree}; +use util::debug_panic; pub use offset_utf16::OffsetUtf16; pub use point::Point; pub use point_utf16::PointUtf16; +pub use unclipped::Unclipped; #[cfg(test)] const CHUNK_BASE: usize = 6; @@ -260,6 +267,14 @@ impl Rope { } pub fn point_utf16_to_offset(&self, point: PointUtf16) -> usize { + self.point_utf16_to_offset_impl(point, false) + } + + pub fn unclipped_point_utf16_to_offset(&self, point: Unclipped) -> usize { + self.point_utf16_to_offset_impl(point.0, true) + } + + fn point_utf16_to_offset_impl(&self, point: PointUtf16, clip: bool) -> usize { if point >= self.summary().lines_utf16() { return self.summary().len; } @@ -269,20 +284,20 @@ impl Rope { cursor.start().1 + cursor .item() - .map_or(0, |chunk| chunk.point_utf16_to_offset(overshoot)) + .map_or(0, |chunk| chunk.point_utf16_to_offset(overshoot, clip)) } - pub fn point_utf16_to_point(&self, point: PointUtf16) -> Point { - if point >= self.summary().lines_utf16() { + pub fn unclipped_point_utf16_to_point(&self, point: Unclipped) -> Point { + if point.0 >= self.summary().lines_utf16() { return self.summary().lines; } let mut cursor = self.chunks.cursor::<(PointUtf16, Point)>(); - cursor.seek(&point, Bias::Left, &()); - let overshoot = point - cursor.start().0; + cursor.seek(&point.0, Bias::Left, &()); + let overshoot = Unclipped(point.0 - cursor.start().0); cursor.start().1 - + cursor - .item() - .map_or(Point::zero(), |chunk| chunk.point_utf16_to_point(overshoot)) + + cursor.item().map_or(Point::zero(), |chunk| { + chunk.unclipped_point_utf16_to_point(overshoot) + }) } pub fn clip_offset(&self, mut offset: usize, bias: Bias) -> usize { @@ -330,11 +345,11 @@ impl Rope { } } - pub fn clip_point_utf16(&self, point: PointUtf16, bias: Bias) -> PointUtf16 { + pub fn clip_point_utf16(&self, point: Unclipped, bias: Bias) -> PointUtf16 { let mut cursor = self.chunks.cursor::(); - cursor.seek(&point, Bias::Right, &()); + cursor.seek(&point.0, Bias::Right, &()); if let Some(chunk) = cursor.item() { - let overshoot = point - cursor.start(); + let overshoot = Unclipped(point.0 - cursor.start()); *cursor.start() + chunk.clip_point_utf16(overshoot, bias) } else { self.summary().lines_utf16() @@ -665,28 +680,33 @@ impl Chunk { fn point_to_offset(&self, target: Point) -> usize { let mut offset = 0; let mut point = Point::new(0, 0); + for ch in self.0.chars() { if point >= target { if point > target { - panic!("point {:?} is inside of character {:?}", target, ch); + debug_panic!("point {target:?} is inside of character {ch:?}"); } break; } if ch == '\n' { point.row += 1; - if point.row > target.row { - panic!( - "point {:?} is beyond the end of a line with length {}", - target, point.column - ); - } point.column = 0; + + if point.row > target.row { + debug_panic!( + "point {target:?} is beyond the end of a line with length {}", + point.column + ); + break; + } } else { point.column += ch.len_utf8() as u32; } + offset += ch.len_utf8(); } + offset } @@ -711,45 +731,62 @@ impl Chunk { point_utf16 } - fn point_utf16_to_offset(&self, target: PointUtf16) -> usize { + fn point_utf16_to_offset(&self, target: PointUtf16, clip: bool) -> usize { let mut offset = 0; let mut point = PointUtf16::new(0, 0); + for ch in self.0.chars() { - if point >= target { - if point > target { - panic!("point {:?} is inside of character {:?}", target, ch); - } + if point == target { break; } if ch == '\n' { point.row += 1; - if point.row > target.row { - panic!( - "point {:?} is beyond the end of a line with length {}", - target, point.column - ); - } point.column = 0; + + if point.row > target.row { + if !clip { + debug_panic!( + "point {target:?} is beyond the end of a line with length {}", + point.column + ); + } + // Return the offset of the newline + return offset; + } } else { point.column += ch.len_utf16() as u32; } + + if point > target { + if !clip { + debug_panic!("point {target:?} is inside of codepoint {ch:?}"); + } + // Return the offset of the codepoint which we have landed within, bias left + return offset; + } + offset += ch.len_utf8(); } + offset } - fn point_utf16_to_point(&self, target: PointUtf16) -> Point { + fn unclipped_point_utf16_to_point(&self, target: Unclipped) -> Point { let mut point = Point::zero(); let mut point_utf16 = PointUtf16::zero(); + for ch in self.0.chars() { - if point_utf16 >= target { - if point_utf16 > target { - panic!("point {:?} is inside of character {:?}", target, ch); - } + if point_utf16 == target.0 { break; } + if point_utf16 > target.0 { + // If the point is past the end of a line or inside of a code point, + // return the last valid point before the target. + return point; + } + if ch == '\n' { point_utf16 += PointUtf16::new(1, 0); point += Point::new(1, 0); @@ -758,6 +795,7 @@ impl Chunk { point += Point::new(0, ch.len_utf8() as u32); } } + point } @@ -777,11 +815,11 @@ impl Chunk { unreachable!() } - fn clip_point_utf16(&self, target: PointUtf16, bias: Bias) -> PointUtf16 { + fn clip_point_utf16(&self, target: Unclipped, bias: Bias) -> PointUtf16 { for (row, line) in self.0.split('\n').enumerate() { - if row == target.row as usize { + if row == target.0.row as usize { let mut code_units = line.encode_utf16(); - let mut column = code_units.by_ref().take(target.column as usize).count(); + let mut column = code_units.by_ref().take(target.0.column as usize).count(); if char::decode_utf16(code_units).next().transpose().is_err() { match bias { Bias::Left => column -= 1, @@ -917,7 +955,7 @@ impl std::ops::Add for TextSummary { type Output = Self; fn add(mut self, rhs: Self) -> Self::Output { - self.add_assign(&rhs); + AddAssign::add_assign(&mut self, &rhs); self } } @@ -1114,15 +1152,15 @@ mod tests { ); assert_eq!( - rope.clip_point_utf16(PointUtf16::new(0, 1), Bias::Left), + rope.clip_point_utf16(Unclipped(PointUtf16::new(0, 1)), Bias::Left), PointUtf16::new(0, 0) ); assert_eq!( - rope.clip_point_utf16(PointUtf16::new(0, 1), Bias::Right), + rope.clip_point_utf16(Unclipped(PointUtf16::new(0, 1)), Bias::Right), PointUtf16::new(0, 2) ); assert_eq!( - rope.clip_point_utf16(PointUtf16::new(0, 3), Bias::Right), + rope.clip_point_utf16(Unclipped(PointUtf16::new(0, 3)), Bias::Right), PointUtf16::new(0, 2) ); @@ -1238,7 +1276,7 @@ mod tests { } let mut offset_utf16 = OffsetUtf16(0); - let mut point_utf16 = PointUtf16::zero(); + let mut point_utf16 = Unclipped(PointUtf16::zero()); for unit in expected.encode_utf16() { let left_offset = actual.clip_offset_utf16(offset_utf16, Bias::Left); let right_offset = actual.clip_offset_utf16(offset_utf16, Bias::Right); @@ -1250,15 +1288,15 @@ mod tests { let left_point = actual.clip_point_utf16(point_utf16, Bias::Left); let right_point = actual.clip_point_utf16(point_utf16, Bias::Right); assert!(right_point >= left_point); - // Ensure translating UTF-16 points to offsets doesn't panic. + // Ensure translating valid UTF-16 points to offsets doesn't panic. actual.point_utf16_to_offset(left_point); actual.point_utf16_to_offset(right_point); offset_utf16.0 += 1; if unit == b'\n' as u16 { - point_utf16 += PointUtf16::new(1, 0); + point_utf16.0 += PointUtf16::new(1, 0); } else { - point_utf16 += PointUtf16::new(0, 1); + point_utf16.0 += PointUtf16::new(0, 1); } } diff --git a/crates/rope/src/unclipped.rs b/crates/rope/src/unclipped.rs new file mode 100644 index 0000000000..937cbca053 --- /dev/null +++ b/crates/rope/src/unclipped.rs @@ -0,0 +1,57 @@ +use crate::{ChunkSummary, TextDimension, TextSummary}; +use std::ops::{Add, AddAssign, Sub, SubAssign}; + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Unclipped(pub T); + +impl From for Unclipped { + fn from(value: T) -> Self { + Unclipped(value) + } +} + +impl<'a, T: sum_tree::Dimension<'a, ChunkSummary>> sum_tree::Dimension<'a, ChunkSummary> + for Unclipped +{ + fn add_summary(&mut self, summary: &'a ChunkSummary, _: &()) { + self.0.add_summary(summary, &()); + } +} + +impl TextDimension for Unclipped { + fn from_text_summary(summary: &TextSummary) -> Self { + Unclipped(T::from_text_summary(summary)) + } + + fn add_assign(&mut self, other: &Self) { + TextDimension::add_assign(&mut self.0, &other.0); + } +} + +impl> Add> for Unclipped { + type Output = Unclipped; + + fn add(self, rhs: Unclipped) -> Self::Output { + Unclipped(self.0 + rhs.0) + } +} + +impl> Sub> for Unclipped { + type Output = Unclipped; + + fn sub(self, rhs: Unclipped) -> Self::Output { + Unclipped(self.0 - rhs.0) + } +} + +impl> AddAssign> for Unclipped { + fn add_assign(&mut self, rhs: Unclipped) { + self.0 += rhs.0; + } +} + +impl> SubAssign> for Unclipped { + fn sub_assign(&mut self, rhs: Unclipped) { + self.0 -= rhs.0; + } +} diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 6f26e0dfa1..f7c7bfd6bc 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -403,8 +403,10 @@ message Symbol { string name = 4; int32 kind = 5; string path = 6; - Point start = 7; - Point end = 8; + // Cannot use generate anchors for unopend files, + // so we are forced to use point coords instead + PointUtf16 start = 7; + PointUtf16 end = 8; bytes signature = 9; } @@ -1033,7 +1035,7 @@ message Range { uint64 end = 2; } -message Point { +message PointUtf16 { uint32 row = 1; uint32 column = 2; } diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index e801e00757..cb83c2c370 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -28,6 +28,7 @@ pub struct Settings { pub buffer_font_family: FamilyId, pub default_buffer_font_size: f32, pub buffer_font_size: f32, + pub active_pane_magnification: f32, pub cursor_blink: bool, pub hover_popover_enabled: bool, pub show_completions_on_input: bool, @@ -253,6 +254,8 @@ pub struct SettingsFileContent { #[serde(default)] pub buffer_font_size: Option, #[serde(default)] + pub active_pane_magnification: Option, + #[serde(default)] pub cursor_blink: Option, #[serde(default)] pub hover_popover_enabled: Option, @@ -312,6 +315,7 @@ impl Settings { .load_family(&[defaults.buffer_font_family.as_ref().unwrap()]) .unwrap(), buffer_font_size: defaults.buffer_font_size.unwrap(), + active_pane_magnification: defaults.active_pane_magnification.unwrap(), default_buffer_font_size: defaults.buffer_font_size.unwrap(), cursor_blink: defaults.cursor_blink.unwrap(), hover_popover_enabled: defaults.hover_popover_enabled.unwrap(), @@ -367,6 +371,10 @@ impl Settings { data.projects_online_by_default, ); merge(&mut self.buffer_font_size, data.buffer_font_size); + merge( + &mut self.active_pane_magnification, + data.active_pane_magnification, + ); merge(&mut self.default_buffer_font_size, data.buffer_font_size); merge(&mut self.cursor_blink, data.cursor_blink); merge(&mut self.hover_popover_enabled, data.hover_popover_enabled); @@ -458,6 +466,7 @@ impl Settings { experiments: FeatureFlags::default(), buffer_font_family: cx.font_cache().load_family(&["Monaco"]).unwrap(), buffer_font_size: 14., + active_pane_magnification: 1., default_buffer_font_size: 14., cursor_blink: true, hover_popover_enabled: true, diff --git a/crates/terminal/src/mappings/keys.rs b/crates/terminal/src/mappings/keys.rs index 199f42df65..ddcd6c5898 100644 --- a/crates/terminal/src/mappings/keys.rs +++ b/crates/terminal/src/mappings/keys.rs @@ -36,18 +36,6 @@ impl Modifiers { } } -///This function checks if to_esc_str would work, assuming all terminal settings are off. -///Note that this function is conservative. It can fail in cases where the actual to_esc_str succeeds. -///This is unavoidable for our use case. GPUI cannot wait until we acquire the terminal -///lock to determine whether we could actually send the keystroke with the current settings. Therefore, -///This conservative guess is used instead. Note that in practice the case where this method -///Returns false when the actual terminal would consume the keystroke never happens. All keystrokes -///that depend on terminal modes also have a mapping that doesn't depend on the terminal mode. -///This is fragile, but as these mappings are locked up in legacy compatibility, it's probably good enough -pub fn might_convert(keystroke: &Keystroke) -> bool { - to_esc_str(keystroke, &TermMode::NONE, false).is_some() -} - pub fn to_esc_str(keystroke: &Keystroke, mode: &TermMode, alt_is_meta: bool) -> Option { let modifiers = Modifiers::new(keystroke); diff --git a/crates/terminal/src/mappings/mouse.rs b/crates/terminal/src/mappings/mouse.rs index 2254eea5af..4dd97aa4c6 100644 --- a/crates/terminal/src/mappings/mouse.rs +++ b/crates/terminal/src/mappings/mouse.rs @@ -97,7 +97,7 @@ impl MouseButton { } fn from_scroll(e: &ScrollWheelEvent) -> Self { - if e.delta.y() > 0. { + if e.delta.raw().y() > 0. { MouseButton::ScrollUp } else { MouseButton::ScrollDown diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 2c9135d331..7e469e19fe 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -407,13 +407,18 @@ impl TerminalBuilder { 'outer: loop { let mut events = vec![]; let mut timer = cx.background().timer(Duration::from_millis(4)).fuse(); - + let mut wakeup = false; loop { futures::select_biased! { _ = timer => break, event = self.events_rx.next() => { if let Some(event) = event { - events.push(event); + if matches!(event, AlacTermEvent::Wakeup) { + wakeup = true; + } else { + events.push(event); + } + if events.len() > 100 { break; } @@ -424,11 +429,15 @@ impl TerminalBuilder { } } - if events.is_empty() { + if events.is_empty() && wakeup == false { smol::future::yield_now().await; break 'outer; } else { this.upgrade(&cx)?.update(&mut cx, |this, cx| { + if wakeup { + this.process_event(&AlacTermEvent::Wakeup, cx); + } + for event in events { this.process_event(&event, cx); } @@ -627,7 +636,7 @@ impl Terminal { term.grid_mut().reset_region(..cursor.line); // Copy the current line up - let line = term.grid()[cursor.line][..cursor.column] + let line = term.grid()[cursor.line][..Column(term.grid().columns())] .iter() .cloned() .enumerate() @@ -1136,7 +1145,7 @@ impl Terminal { fn determine_scroll_lines(&mut self, e: &MouseScrollWheel, mouse_mode: bool) -> Option { let scroll_multiplier = if mouse_mode { 1. } else { SCROLL_MULTIPLIER }; - + let line_height = self.last_content.size.line_height; match e.phase { /* Reset scroll state on started */ Some(gpui::TouchPhase::Started) => { @@ -1145,11 +1154,11 @@ impl Terminal { } /* Calculate the appropriate scroll lines */ Some(gpui::TouchPhase::Moved) => { - let old_offset = (self.scroll_px / self.last_content.size.line_height) as i32; + let old_offset = (self.scroll_px / line_height) as i32; - self.scroll_px += e.delta.y() * scroll_multiplier; + self.scroll_px += e.delta.pixel_delta(line_height).y() * scroll_multiplier; - let new_offset = (self.scroll_px / self.last_content.size.line_height) as i32; + let new_offset = (self.scroll_px / line_height) as i32; // Whenever we hit the edges, reset our stored scroll to 0 // so we can respond to changes in direction quickly @@ -1159,7 +1168,7 @@ impl Terminal { } /* Fall back to delta / line_height */ None => Some( - ((e.delta.y() * scroll_multiplier) / self.last_content.size.line_height) as i32, + ((e.delta.pixel_delta(line_height).y() * scroll_multiplier) / line_height) as i32, ), _ => None, } diff --git a/crates/text/src/locator.rs b/crates/text/src/locator.rs index 9d38a51df0..07b73ace05 100644 --- a/crates/text/src/locator.rs +++ b/crates/text/src/locator.rs @@ -3,8 +3,8 @@ use smallvec::{smallvec, SmallVec}; use std::iter; lazy_static! { - pub static ref MIN: Locator = Locator::min(); - pub static ref MAX: Locator = Locator::max(); + static ref MIN: Locator = Locator::min(); + static ref MAX: Locator = Locator::max(); } #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -19,6 +19,14 @@ impl Locator { Self(smallvec![u64::MAX]) } + pub fn min_ref() -> &'static Self { + &*MIN + } + + pub fn max_ref() -> &'static Self { + &*MAX + } + pub fn assign(&mut self, other: &Self) { self.0.resize(other.0.len(), 0); self.0.copy_from_slice(&other.0); diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 72ae018a16..5c2f7b7a51 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -1594,8 +1594,12 @@ impl BufferSnapshot { self.visible_text.point_utf16_to_offset(point) } - pub fn point_utf16_to_point(&self, point: PointUtf16) -> Point { - self.visible_text.point_utf16_to_point(point) + pub fn unclipped_point_utf16_to_offset(&self, point: Unclipped) -> usize { + self.visible_text.unclipped_point_utf16_to_offset(point) + } + + pub fn unclipped_point_utf16_to_point(&self, point: Unclipped) -> Point { + self.visible_text.unclipped_point_utf16_to_point(point) } pub fn offset_utf16_to_offset(&self, offset: OffsetUtf16) -> usize { @@ -1766,9 +1770,9 @@ impl BufferSnapshot { fn fragment_id_for_anchor(&self, anchor: &Anchor) -> &Locator { if *anchor == Anchor::MIN { - &locator::MIN + Locator::min_ref() } else if *anchor == Anchor::MAX { - &locator::MAX + Locator::max_ref() } else { let anchor_key = InsertionFragmentKey { timestamp: anchor.timestamp, @@ -1803,7 +1807,10 @@ impl BufferSnapshot { } pub fn anchor_at(&self, position: T, bias: Bias) -> Anchor { - let offset = position.to_offset(self); + self.anchor_at_offset(position.to_offset(self), bias) + } + + fn anchor_at_offset(&self, offset: usize, bias: Bias) -> Anchor { if bias == Bias::Left && offset == 0 { Anchor::MIN } else if bias == Bias::Right && offset == self.len() { @@ -1840,7 +1847,7 @@ impl BufferSnapshot { self.visible_text.clip_offset_utf16(offset, bias) } - pub fn clip_point_utf16(&self, point: PointUtf16, bias: Bias) -> PointUtf16 { + pub fn clip_point_utf16(&self, point: Unclipped, bias: Bias) -> PointUtf16 { self.visible_text.clip_point_utf16(point, bias) } @@ -2354,32 +2361,20 @@ pub trait ToOffset { } impl ToOffset for Point { - fn to_offset<'a>(&self, snapshot: &BufferSnapshot) -> usize { + fn to_offset(&self, snapshot: &BufferSnapshot) -> usize { snapshot.point_to_offset(*self) } } -impl ToOffset for PointUtf16 { - fn to_offset<'a>(&self, snapshot: &BufferSnapshot) -> usize { - snapshot.point_utf16_to_offset(*self) - } -} - impl ToOffset for usize { - fn to_offset<'a>(&self, snapshot: &BufferSnapshot) -> usize { + fn to_offset(&self, snapshot: &BufferSnapshot) -> usize { assert!(*self <= snapshot.len(), "offset {self} is out of range"); *self } } -impl ToOffset for OffsetUtf16 { - fn to_offset<'a>(&self, snapshot: &BufferSnapshot) -> usize { - snapshot.offset_utf16_to_offset(*self) - } -} - impl ToOffset for Anchor { - fn to_offset<'a>(&self, snapshot: &BufferSnapshot) -> usize { + fn to_offset(&self, snapshot: &BufferSnapshot) -> usize { snapshot.summary_for_anchor(self) } } @@ -2390,31 +2385,43 @@ impl<'a, T: ToOffset> ToOffset for &'a T { } } +impl ToOffset for PointUtf16 { + fn to_offset(&self, snapshot: &BufferSnapshot) -> usize { + snapshot.point_utf16_to_offset(*self) + } +} + +impl ToOffset for Unclipped { + fn to_offset(&self, snapshot: &BufferSnapshot) -> usize { + snapshot.unclipped_point_utf16_to_offset(*self) + } +} + pub trait ToPoint { fn to_point(&self, snapshot: &BufferSnapshot) -> Point; } impl ToPoint for Anchor { - fn to_point<'a>(&self, snapshot: &BufferSnapshot) -> Point { + fn to_point(&self, snapshot: &BufferSnapshot) -> Point { snapshot.summary_for_anchor(self) } } impl ToPoint for usize { - fn to_point<'a>(&self, snapshot: &BufferSnapshot) -> Point { + fn to_point(&self, snapshot: &BufferSnapshot) -> Point { snapshot.offset_to_point(*self) } } -impl ToPoint for PointUtf16 { - fn to_point<'a>(&self, snapshot: &BufferSnapshot) -> Point { - snapshot.point_utf16_to_point(*self) +impl ToPoint for Point { + fn to_point(&self, _: &BufferSnapshot) -> Point { + *self } } -impl ToPoint for Point { - fn to_point<'a>(&self, _: &BufferSnapshot) -> Point { - *self +impl ToPoint for Unclipped { + fn to_point(&self, snapshot: &BufferSnapshot) -> Point { + snapshot.unclipped_point_utf16_to_point(*self) } } @@ -2423,25 +2430,25 @@ pub trait ToPointUtf16 { } impl ToPointUtf16 for Anchor { - fn to_point_utf16<'a>(&self, snapshot: &BufferSnapshot) -> PointUtf16 { + fn to_point_utf16(&self, snapshot: &BufferSnapshot) -> PointUtf16 { snapshot.summary_for_anchor(self) } } impl ToPointUtf16 for usize { - fn to_point_utf16<'a>(&self, snapshot: &BufferSnapshot) -> PointUtf16 { + fn to_point_utf16(&self, snapshot: &BufferSnapshot) -> PointUtf16 { snapshot.offset_to_point_utf16(*self) } } impl ToPointUtf16 for PointUtf16 { - fn to_point_utf16<'a>(&self, _: &BufferSnapshot) -> PointUtf16 { + fn to_point_utf16(&self, _: &BufferSnapshot) -> PointUtf16 { *self } } impl ToPointUtf16 for Point { - fn to_point_utf16<'a>(&self, snapshot: &BufferSnapshot) -> PointUtf16 { + fn to_point_utf16(&self, snapshot: &BufferSnapshot) -> PointUtf16 { snapshot.point_to_point_utf16(*self) } } @@ -2451,45 +2458,23 @@ pub trait ToOffsetUtf16 { } impl ToOffsetUtf16 for Anchor { - fn to_offset_utf16<'a>(&self, snapshot: &BufferSnapshot) -> OffsetUtf16 { + fn to_offset_utf16(&self, snapshot: &BufferSnapshot) -> OffsetUtf16 { snapshot.summary_for_anchor(self) } } impl ToOffsetUtf16 for usize { - fn to_offset_utf16<'a>(&self, snapshot: &BufferSnapshot) -> OffsetUtf16 { + fn to_offset_utf16(&self, snapshot: &BufferSnapshot) -> OffsetUtf16 { snapshot.offset_to_offset_utf16(*self) } } impl ToOffsetUtf16 for OffsetUtf16 { - fn to_offset_utf16<'a>(&self, _snapshot: &BufferSnapshot) -> OffsetUtf16 { + fn to_offset_utf16(&self, _snapshot: &BufferSnapshot) -> OffsetUtf16 { *self } } -pub trait Clip { - fn clip(&self, bias: Bias, snapshot: &BufferSnapshot) -> Self; -} - -impl Clip for usize { - fn clip(&self, bias: Bias, snapshot: &BufferSnapshot) -> Self { - snapshot.clip_offset(*self, bias) - } -} - -impl Clip for Point { - fn clip(&self, bias: Bias, snapshot: &BufferSnapshot) -> Self { - snapshot.clip_point(*self, bias) - } -} - -impl Clip for PointUtf16 { - fn clip(&self, bias: Bias, snapshot: &BufferSnapshot) -> Self { - snapshot.clip_point_utf16(*self, bias) - } -} - pub trait FromAnchor { fn from_anchor(anchor: &Anchor, snapshot: &BufferSnapshot) -> Self; } diff --git a/crates/theme_testbench/Cargo.toml b/crates/theme_testbench/Cargo.toml index 9cb063c1e5..5fb263501f 100644 --- a/crates/theme_testbench/Cargo.toml +++ b/crates/theme_testbench/Cargo.toml @@ -15,4 +15,4 @@ settings = { path = "../settings" } workspace = { path = "../workspace" } project = { path = "../project" } -smallvec = { version = "1.6", features = ["union"] } \ No newline at end of file +smallvec = { version = "1.6", features = ["union"] } diff --git a/crates/util/Cargo.toml b/crates/util/Cargo.toml index c083137156..fc16eeb53c 100644 --- a/crates/util/Cargo.toml +++ b/crates/util/Cargo.toml @@ -11,6 +11,7 @@ test-support = ["serde_json", "tempdir", "git2"] [dependencies] anyhow = "1.0.38" +backtrace = "0.3" futures = "0.3" log = { version = "0.4.16", features = ["kv_unstable_serde"] } lazy_static = "1.4.0" diff --git a/crates/util/src/lib.rs b/crates/util/src/lib.rs index e35f2df7d4..22d63a0996 100644 --- a/crates/util/src/lib.rs +++ b/crates/util/src/lib.rs @@ -1,6 +1,7 @@ #[cfg(any(test, feature = "test-support"))] pub mod test; +pub use backtrace::Backtrace; use futures::Future; use rand::{seq::SliceRandom, Rng}; use std::{ @@ -10,6 +11,18 @@ use std::{ task::{Context, Poll}, }; +#[macro_export] +macro_rules! debug_panic { + ( $($fmt_arg:tt)* ) => { + if cfg!(debug_assertions) { + panic!( $($fmt_arg)* ); + } else { + let backtrace = $crate::Backtrace::new(); + log::error!("{}\n{:?}", format_args!($($fmt_arg)*), backtrace); + } + }; +} + pub fn truncate(s: &str, max_chars: usize) -> &str { match s.char_indices().nth(max_chars) { None => s, diff --git a/crates/vim/Cargo.toml b/crates/vim/Cargo.toml index 44f2a8cb16..daefebdbdd 100644 --- a/crates/vim/Cargo.toml +++ b/crates/vim/Cargo.toml @@ -42,4 +42,4 @@ language = { path = "../language", features = ["test-support"] } project = { path = "../project", features = ["test-support"] } util = { path = "../util", features = ["test-support"] } settings = { path = "../settings" } -workspace = { path = "../workspace", features = ["test-support"] } \ No newline at end of file +workspace = { path = "../workspace", features = ["test-support"] } diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index ff454d81a8..95f6c3d8b4 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -114,12 +114,12 @@ pub fn change(_: &mut Workspace, _: &VisualChange, cx: &mut ViewContext>, + active_pane: &ViewHandle, cx: &mut RenderContext, ) -> ElementBox { - self.root - .render(project, theme, follower_states, active_call, cx) + self.root.render( + project, + theme, + follower_states, + active_call, + active_pane, + cx, + ) } pub(crate) fn panes(&self) -> Vec<&ViewHandle> { @@ -104,12 +112,20 @@ impl Member { Member::Axis(PaneAxis { axis, members }) } + fn contains(&self, needle: &ViewHandle) -> bool { + match self { + Member::Axis(axis) => axis.members.iter().any(|member| member.contains(needle)), + Member::Pane(pane) => pane == needle, + } + } + pub fn render( &self, project: &ModelHandle, theme: &Theme, follower_states: &FollowerStatesByLeader, active_call: Option<&ModelHandle>, + active_pane: &ViewHandle, cx: &mut RenderContext, ) -> ElementBox { enum FollowIntoExternalProject {} @@ -236,7 +252,14 @@ impl Member { .with_children(prompt) .boxed() } - Member::Axis(axis) => axis.render(project, theme, follower_states, active_call, cx), + Member::Axis(axis) => axis.render( + project, + theme, + follower_states, + active_call, + active_pane, + cx, + ), } } @@ -337,12 +360,19 @@ impl PaneAxis { theme: &Theme, follower_state: &FollowerStatesByLeader, active_call: Option<&ModelHandle>, + active_pane: &ViewHandle, cx: &mut RenderContext, ) -> ElementBox { let last_member_ix = self.members.len() - 1; Flex::new(self.axis) .with_children(self.members.iter().enumerate().map(|(ix, member)| { - let mut member = member.render(project, theme, follower_state, active_call, cx); + let mut flex = 1.0; + if member.contains(active_pane) { + flex = cx.global::().active_pane_magnification; + } + + let mut member = + member.render(project, theme, follower_state, active_call, active_pane, cx); if ix < last_member_ix { let mut border = theme.workspace.pane_divider; border.left = false; @@ -356,7 +386,7 @@ impl PaneAxis { member = Container::new(member).with_border(border).boxed(); } - FlexItem::new(member).flex(1.0, true).boxed() + FlexItem::new(member).flex(flex, true).boxed() })) .boxed() } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 2296741ed3..9021867dfc 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2784,6 +2784,7 @@ impl View for Workspace { &theme, &self.follower_states_by_leader, self.active_call(), + self.active_pane(), cx, )) .flex(1., true) diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index a2413fb13b..a3023918e3 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] description = "The fast, collaborative code editor." edition = "2021" name = "zed" -version = "0.65.0" +version = "0.67.0" [lib] name = "zed" @@ -95,6 +95,7 @@ tree-sitter-c = "0.20.1" tree-sitter-cpp = "0.20.0" tree-sitter-css = { git = "https://github.com/tree-sitter/tree-sitter-css", rev = "769203d0f9abe1a9a691ac2b9fe4bb4397a73c51" } tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "05e3631c6a0701c1fa518b0fee7be95a2ceef5e2" } +tree-sitter-embedded-template = "0.20.0" tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "aeb2f33b366fd78d5789ff104956ce23508b85db" } tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "137e1ce6a02698fc246cdb9c6b886ed1de9a1ed8" } tree-sitter-rust = "0.20.3" diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index c1e17a8e1a..4c33e7329f 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -12,6 +12,7 @@ mod installation; mod json; mod language_plugin; mod python; +mod ruby; mod rust; mod typescript; @@ -116,7 +117,16 @@ pub async fn init(languages: Arc, _executor: Arc) tree_sitter_html::language(), Some(CachedLspAdapter::new(html::HtmlLspAdapter).await), ), - ("ruby", tree_sitter_ruby::language(), None), + ( + "ruby", + tree_sitter_ruby::language(), + Some(CachedLspAdapter::new(ruby::RubyLanguageServer).await), + ), + ( + "erb", + tree_sitter_embedded_template::language(), + Some(CachedLspAdapter::new(ruby::RubyLanguageServer).await), + ), ] { languages.add(language(name, grammar, lsp_adapter)); } diff --git a/crates/zed/src/languages/erb/config.toml b/crates/zed/src/languages/erb/config.toml new file mode 100644 index 0000000000..280219a119 --- /dev/null +++ b/crates/zed/src/languages/erb/config.toml @@ -0,0 +1,8 @@ +name = "ERB" +path_suffixes = ["erb"] +autoclose_before = ">})" +brackets = [ + { start = "<", end = ">", close = true, newline = true }, +] + +block_comment = ["<%#", "%>"] \ No newline at end of file diff --git a/crates/zed/src/languages/erb/highlights.scm b/crates/zed/src/languages/erb/highlights.scm new file mode 100644 index 0000000000..91b21d081f --- /dev/null +++ b/crates/zed/src/languages/erb/highlights.scm @@ -0,0 +1,12 @@ +(comment_directive) @comment + +[ + "<%#" + "<%" + "<%=" + "<%_" + "<%-" + "%>" + "-%>" + "_%>" +] @keyword \ No newline at end of file diff --git a/crates/zed/src/languages/erb/injections.scm b/crates/zed/src/languages/erb/injections.scm new file mode 100644 index 0000000000..7a69a818ef --- /dev/null +++ b/crates/zed/src/languages/erb/injections.scm @@ -0,0 +1,7 @@ +((code) @content + (#set! "language" "ruby") + (#set! "combined")) + +((content) @content + (#set! "language" "html") + (#set! "combined")) diff --git a/crates/zed/src/languages/python.rs b/crates/zed/src/languages/python.rs index e6e55eeac4..ba6ccf7bf0 100644 --- a/crates/zed/src/languages/python.rs +++ b/crates/zed/src/languages/python.rs @@ -87,6 +87,25 @@ impl LspAdapter for PythonLspAdapter { .log_err() } + async fn process_completion(&self, item: &mut lsp::CompletionItem) { + // Pyright assigns each completion item a `sortText` of the form `XX.YYYY.name`. + // Where `XX` is the sorting category, `YYYY` is based on most recent usage, + // and `name` is the symbol name itself. + // + // Because the the symbol name is included, there generally are not ties when + // sorting by the `sortText`, so the symbol's fuzzy match score is not taken + // into account. Here, we remove the symbol name from the sortText in order + // to allow our own fuzzy score to be used to break ties. + // + // see https://github.com/microsoft/pyright/blob/95ef4e103b9b2f129c9320427e51b73ea7cf78bd/packages/pyright-internal/src/languageService/completionProvider.ts#LL2873 + let Some(sort_text) = &mut item.sort_text else { return }; + let mut parts = sort_text.split('.'); + let Some(first) = parts.next() else { return }; + let Some(second) = parts.next() else { return }; + let Some(_) = parts.next() else { return }; + sort_text.replace_range(first.len() + second.len() + 1.., ""); + } + async fn label_for_completion( &self, item: &lsp::CompletionItem, diff --git a/crates/zed/src/languages/ruby.rs b/crates/zed/src/languages/ruby.rs new file mode 100644 index 0000000000..6aad293d34 --- /dev/null +++ b/crates/zed/src/languages/ruby.rs @@ -0,0 +1,145 @@ +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use client::http::HttpClient; +use language::{LanguageServerName, LspAdapter}; +use std::{any::Any, path::PathBuf, sync::Arc}; + +pub struct RubyLanguageServer; + +#[async_trait] +impl LspAdapter for RubyLanguageServer { + async fn name(&self) -> LanguageServerName { + LanguageServerName("solargraph".into()) + } + + async fn server_args(&self) -> Vec { + vec!["stdio".into()] + } + + async fn fetch_latest_server_version( + &self, + _: Arc, + ) -> Result> { + Ok(Box::new(())) + } + + async fn fetch_server_binary( + &self, + _version: Box, + _: Arc, + _container_dir: PathBuf, + ) -> Result { + Err(anyhow!("solargraph must be installed manually")) + } + + async fn cached_server_binary(&self, _container_dir: PathBuf) -> Option { + Some("solargraph".into()) + } + + async fn label_for_completion( + &self, + item: &lsp::CompletionItem, + language: &Arc, + ) -> Option { + let label = &item.label; + let grammar = language.grammar()?; + let highlight_id = match item.kind? { + lsp::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method")?, + lsp::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant")?, + lsp::CompletionItemKind::CLASS | lsp::CompletionItemKind::MODULE => { + grammar.highlight_id_for_name("type")? + } + lsp::CompletionItemKind::KEYWORD => { + if label.starts_with(":") { + grammar.highlight_id_for_name("string.special.symbol")? + } else { + grammar.highlight_id_for_name("keyword")? + } + } + lsp::CompletionItemKind::VARIABLE => { + if label.starts_with("@") { + grammar.highlight_id_for_name("property")? + } else { + return None; + } + } + _ => return None, + }; + Some(language::CodeLabel { + text: label.clone(), + runs: vec![(0..label.len(), highlight_id)], + filter_range: 0..label.len(), + }) + } + + async fn label_for_symbol( + &self, + label: &str, + kind: lsp::SymbolKind, + language: &Arc, + ) -> Option { + let grammar = language.grammar()?; + match kind { + lsp::SymbolKind::METHOD => { + let mut parts = label.split('#'); + let classes = parts.next()?; + let method = parts.next()?; + if parts.next().is_some() { + return None; + } + + let class_id = grammar.highlight_id_for_name("type")?; + let method_id = grammar.highlight_id_for_name("function.method")?; + + let mut ix = 0; + let mut runs = Vec::new(); + for (i, class) in classes.split("::").enumerate() { + if i > 0 { + ix += 2; + } + let end_ix = ix + class.len(); + runs.push((ix..end_ix, class_id)); + ix = end_ix; + } + + ix += 1; + let end_ix = ix + method.len(); + runs.push((ix..end_ix, method_id)); + Some(language::CodeLabel { + text: label.to_string(), + runs, + filter_range: 0..label.len(), + }) + } + lsp::SymbolKind::CONSTANT => { + let constant_id = grammar.highlight_id_for_name("constant")?; + Some(language::CodeLabel { + text: label.to_string(), + runs: vec![(0..label.len(), constant_id)], + filter_range: 0..label.len(), + }) + } + lsp::SymbolKind::CLASS | lsp::SymbolKind::MODULE => { + let class_id = grammar.highlight_id_for_name("type")?; + + let mut ix = 0; + let mut runs = Vec::new(); + for (i, class) in label.split("::").enumerate() { + if i > 0 { + ix += "::".len(); + } + let end_ix = ix + class.len(); + runs.push((ix..end_ix, class_id)); + ix = end_ix; + } + + Some(language::CodeLabel { + text: label.to_string(), + runs, + filter_range: 0..label.len(), + }) + } + _ => return None, + } + } +} diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 5a7ee2dbae..e6437feb6b 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -211,21 +211,6 @@ fn init_paths() { std::fs::create_dir_all(&*zed::paths::LANGUAGES_DIR).expect("could not create languages path"); std::fs::create_dir_all(&*zed::paths::DB_DIR).expect("could not create database path"); std::fs::create_dir_all(&*zed::paths::LOGS_DIR).expect("could not create logs path"); - - // Copy setting files from legacy locations. TODO: remove this after a few releases. - thread::spawn(|| { - if std::fs::metadata(&*zed::paths::legacy::SETTINGS).is_ok() - && std::fs::metadata(&*zed::paths::SETTINGS).is_err() - { - std::fs::copy(&*zed::paths::legacy::SETTINGS, &*zed::paths::SETTINGS).log_err(); - } - - if std::fs::metadata(&*zed::paths::legacy::KEYMAP).is_ok() - && std::fs::metadata(&*zed::paths::KEYMAP).is_err() - { - std::fs::copy(&*zed::paths::legacy::KEYMAP, &*zed::paths::KEYMAP).log_err(); - } - }); } fn init_logger() { diff --git a/script/lib/bump-version.sh b/script/lib/bump-version.sh index 7800cc4408..ce95536950 100755 --- a/script/lib/bump-version.sh +++ b/script/lib/bump-version.sh @@ -13,6 +13,7 @@ if [[ -n $(git status --short --untracked-files=no) ]]; then fi which cargo-set-version > /dev/null || cargo install cargo-edit +which jq > /dev/null || brew install jq cargo set-version --package $package --bump $version_increment cargo check --quiet