From f77b6ab79c6fc8619b7853934519ce1c19c6b3fd Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 19 Nov 2024 13:43:24 -0700 Subject: [PATCH 01/68] Fix space repeating in terminal (#20877) This is broken because of the way we try to emulate macOS's ApplePressAndHoldEnabled. Release Notes: - Fixed holding down space in the terminal (preview only) --- crates/gpui/src/platform/mac/events.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/gpui/src/platform/mac/events.rs b/crates/gpui/src/platform/mac/events.rs index aeff08ada8..51716cccb4 100644 --- a/crates/gpui/src/platform/mac/events.rs +++ b/crates/gpui/src/platform/mac/events.rs @@ -260,7 +260,10 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke { #[allow(non_upper_case_globals)] let key = match first_char { - Some(SPACE_KEY) => "space".to_string(), + Some(SPACE_KEY) => { + ime_key = Some(" ".to_string()); + "space".to_string() + } Some(BACKSPACE_KEY) => "backspace".to_string(), Some(ENTER_KEY) | Some(NUMPAD_ENTER_KEY) => "enter".to_string(), Some(ESCAPE_KEY) => "escape".to_string(), From 705a06c3dd62cd85613092163575bc7cf10b9d30 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 19 Nov 2024 16:38:14 -0700 Subject: [PATCH 02/68] Send Country/OS/Version amplitude style (#20884) Release Notes: - N/A --- crates/collab/src/api/events.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/collab/src/api/events.rs b/crates/collab/src/api/events.rs index 57ac43ca56..1c936bac39 100644 --- a/crates/collab/src/api/events.rs +++ b/crates/collab/src/api/events.rs @@ -1561,6 +1561,9 @@ fn for_snowflake( let user_properties = Some(serde_json::json!({ "is_staff": body.is_staff, + "Country": country_code.clone(), + "OS": format!("{} {}", body.os_name, body.os_version.clone().unwrap_or_default()), + "Version": body.app_version.clone(), })); Some(SnowflakeRow { From c2668bc953c6675e7d7c31014f045b28aff6f99a Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 19 Nov 2024 19:08:33 -0700 Subject: [PATCH 03/68] Fix draft-releaase-notes (#20885) Turns out this was broken because (a) we didn't have tags fetched, and (b) because the gh-release action we use is buggy. Release Notes: - N/A --- .github/workflows/ci.yml | 21 ++++----------------- script/create-draft-release | 8 ++++++++ 2 files changed, 12 insertions(+), 17 deletions(-) create mode 100755 script/create-draft-release diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ee6f81dd84..f22a8a518e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -244,6 +244,7 @@ jobs: # # 25 was chosen arbitrarily. fetch-depth: 25 + fetch-tags: true clean: false - name: Limit target directory size @@ -261,6 +262,9 @@ jobs: mkdir -p target/ # Ignore any errors that occur while drafting release notes to not fail the build. script/draft-release-notes "$RELEASE_VERSION" "$RELEASE_CHANNEL" > target/release-notes.md || true + script/create-draft-release target/release-notes.md + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Generate license file run: script/generate-licenses @@ -306,7 +310,6 @@ jobs: target/aarch64-apple-darwin/release/Zed-aarch64.dmg target/x86_64-apple-darwin/release/Zed-x86_64.dmg target/release/Zed.dmg - body_path: target/release-notes.md env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -401,19 +404,3 @@ jobs: target/release/zed-linux-aarch64.tar.gz env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - auto-publish-release: - timeout-minutes: 60 - name: Create a Linux bundle - runs-on: - - self-hosted - if: ${{ startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre') }} - needs: [bundle-mac, bundle-linux-aarch64, bundle-linux] - steps: - - name: Upload app bundle to release - uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1 - with: - draft: false - prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/script/create-draft-release b/script/create-draft-release new file mode 100755 index 0000000000..e72c6d141c --- /dev/null +++ b/script/create-draft-release @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +preview="" +if [[ "$GITHUB_REF_NAME" == *"-pre" ]]; then + preview="-p" +fi + +gh release create -d "$GITHUB_REF_NAME" -F "$1" $preview From ad6a07e57426a8ef85e3a488f15130fc9b279204 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 19 Nov 2024 20:00:03 -0700 Subject: [PATCH 04/68] Remove comments from discord release announcements (#20888) Release Notes: - N/A --- script/draft-release-notes | 4 ---- 1 file changed, 4 deletions(-) diff --git a/script/draft-release-notes b/script/draft-release-notes index 287997ff79..eeb53bbb22 100755 --- a/script/draft-release-notes +++ b/script/draft-release-notes @@ -64,10 +64,6 @@ async function main() { } console.log(releaseNotes.join("\n") + "\n"); - console.log(""); } function getCommits(oldTag, newTag) { From 3c57a4071cd3e3400a1e0b3329fccb0f0477e1ea Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 19 Nov 2024 20:00:11 -0700 Subject: [PATCH 05/68] vim: Fix jj to exit insert mode (#20890) Release Notes: - (Preview only) fixed binding `jj` to exit insert mode --- crates/gpui/src/window.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 9a028c1f01..ec1fd601ec 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -3038,7 +3038,7 @@ impl<'a> WindowContext<'a> { return true; } - if let Some(input) = keystroke.ime_key { + if let Some(input) = keystroke.with_simulated_ime().ime_key { if let Some(mut input_handler) = self.window.platform_window.take_input_handler() { input_handler.dispatch_input(&input, self); self.window.platform_window.set_input_handler(input_handler); @@ -3482,7 +3482,13 @@ impl<'a> WindowContext<'a> { if !self.propagate_event { continue 'replay; } - if let Some(input) = replay.keystroke.ime_key.as_ref().cloned() { + if let Some(input) = replay + .keystroke + .with_simulated_ime() + .ime_key + .as_ref() + .cloned() + { if let Some(mut input_handler) = self.window.platform_window.take_input_handler() { input_handler.dispatch_input(&input, self); self.window.platform_window.set_input_handler(input_handler) From e03968f53832ac6d42cc338f57f20035a848652f Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 20 Nov 2024 14:22:07 +0100 Subject: [PATCH 06/68] pane: Fix panic when dragging non-pinned item onto it's pinned copy in another pane (#20900) Closes #20889 Release Notes: - N/A --- crates/workspace/src/pane.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 22d06ec21a..e9b81d4554 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -2455,6 +2455,8 @@ impl Pane { to_pane = workspace.split_pane(to_pane, split_direction, cx); } let old_ix = from_pane.read(cx).index_for_item_id(item_id); + let old_len = to_pane.read(cx).items.len(); + move_item(&from_pane, &to_pane, item_id, ix, cx); if to_pane == from_pane { if let Some(old_index) = old_ix { to_pane.update(cx, |this, _| { @@ -2472,7 +2474,10 @@ impl Pane { } } else { to_pane.update(cx, |this, _| { - if this.has_pinned_tabs() && ix < this.pinned_tab_count { + if this.items.len() > old_len // Did we not deduplicate on drag? + && this.has_pinned_tabs() + && ix < this.pinned_tab_count + { this.pinned_tab_count += 1; } }); @@ -2484,7 +2489,6 @@ impl Pane { } }) } - move_item(&from_pane, &to_pane, item_id, ix, cx); }); }) .log_err(); From 743165fa6c5c46d2dab907bc0122f18e1465b7b2 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Wed, 20 Nov 2024 14:38:56 +0100 Subject: [PATCH 07/68] Fix assistant hints showing up when selecting \n in Vim mode (#20899) We also need to check whether the selection is empty, not just whether its head is on an empty line. Release Notes: - N/A Co-authored-by: Antonio --- crates/editor/src/editor.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d303ecf0f3..7f31cdedd3 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -11875,7 +11875,15 @@ impl Editor { style: &EditorStyle, cx: &mut WindowContext, ) -> Option { - if !self.newest_selection_head_on_empty_line(cx) || self.has_active_inline_completion(cx) { + let selection = self.selections.newest::(cx); + if !selection.is_empty() { + return None; + }; + + let snapshot = self.buffer.read(cx).snapshot(cx); + let buffer_row = MultiBufferRow(selection.head().row); + + if snapshot.line_len(buffer_row) != 0 || self.has_active_inline_completion(cx) { return None; } From b63394f4bd1037faf4f3d43e2119d1415232c595 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 20 Nov 2024 10:45:44 -0500 Subject: [PATCH 08/68] v0.164.x dev --- Cargo.lock | 2 +- crates/zed/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 527190baca..d65fa24b4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15422,7 +15422,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.163.0" +version = "0.164.0" dependencies = [ "activity_indicator", "anyhow", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index e5d4cb7623..6f511c2951 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -2,7 +2,7 @@ description = "The fast, collaborative code editor." edition = "2021" name = "zed" -version = "0.163.0" +version = "0.164.0" publish = false license = "GPL-3.0-or-later" authors = ["Zed Team "] From 973498e075999b295cf5fd3910be9fbc34a77976 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 20 Nov 2024 10:53:51 -0500 Subject: [PATCH 09/68] context_servers: Make `settings` field show up in settings completions (#20905) This PR fixes an issue where the `settings` field for a context server would not show up in the completions when editing the Zed settings. It seems that `schemars` doesn't like the `serde_json::Value` as a setting type when generating the JSON Schema. To address this, we are using a custom schema of an empty object (as we don't yet have any other information as to the structure of a given context server's settings). Release Notes: - context_servers: Fixed `settings` field not being suggested in completions when editing `settings.json`. --- crates/context_servers/src/manager.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/context_servers/src/manager.rs b/crates/context_servers/src/manager.rs index fc0c77e821..9b9520e223 100644 --- a/crates/context_servers/src/manager.rs +++ b/crates/context_servers/src/manager.rs @@ -24,6 +24,8 @@ use gpui::{AsyncAppContext, EventEmitter, Model, ModelContext, Subscription, Tas use log; use parking_lot::RwLock; use project::Project; +use schemars::gen::SchemaGenerator; +use schemars::schema::{InstanceType, Schema, SchemaObject}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsSources, SettingsStore}; @@ -43,9 +45,17 @@ pub struct ContextServerSettings { #[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, Debug, Default)] pub struct ServerConfig { pub command: Option, + #[schemars(schema_with = "server_config_settings_json_schema")] pub settings: Option, } +fn server_config_settings_json_schema(_generator: &mut SchemaGenerator) -> Schema { + Schema::Object(SchemaObject { + instance_type: Some(InstanceType::Object.into()), + ..Default::default() + }) +} + #[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, Debug)] pub struct ServerCommand { pub path: String, From 41fd9189e33b966228020417ffb53fea85435e05 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 20 Nov 2024 11:30:14 -0500 Subject: [PATCH 10/68] context_servers: Document settings (#20907) This PR documents the settings type for context servers so that the documentation shows up when editing the `settings.json` file. Release Notes: - N/A --- crates/context_servers/src/manager.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/context_servers/src/manager.rs b/crates/context_servers/src/manager.rs index 9b9520e223..c95fcd239d 100644 --- a/crates/context_servers/src/manager.rs +++ b/crates/context_servers/src/manager.rs @@ -38,13 +38,21 @@ use crate::{ #[derive(Deserialize, Serialize, Default, Clone, PartialEq, Eq, JsonSchema, Debug)] pub struct ContextServerSettings { + /// Settings for context servers used in the Assistant. #[serde(default)] pub context_servers: HashMap, ServerConfig>, } #[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, Debug, Default)] pub struct ServerConfig { + /// The command to run this context server. + /// + /// This will override the command set by an extension. pub command: Option, + /// The settings for this context server. + /// + /// Consult the documentation for the context server to see what settings + /// are supported. #[schemars(schema_with = "server_config_settings_json_schema")] pub settings: Option, } From 1475a7000f790e9133263182afca4fa3a93f5ed4 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 20 Nov 2024 10:27:50 -0700 Subject: [PATCH 11/68] Don't re-render the menu so often (#20914) Closes #20710 Release Notes: - Fixes opening the menu when Chinese Pinyin keyboard is in use --- crates/zed/src/zed.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 0f10f1914b..867ffa91e6 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -824,8 +824,13 @@ pub fn handle_keymap_file_changes( }) .detach(); - cx.on_keyboard_layout_change(move |_| { - keyboard_layout_tx.unbounded_send(()).ok(); + let mut current_mapping = settings::get_key_equivalents(cx.keyboard_layout()); + cx.on_keyboard_layout_change(move |cx| { + let next_mapping = settings::get_key_equivalents(cx.keyboard_layout()); + if next_mapping != current_mapping { + current_mapping = next_mapping; + keyboard_layout_tx.unbounded_send(()).ok(); + } }) .detach(); From 7e67753d51bc2f7db294f3de46380b28ec0e4e7d Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Wed, 20 Nov 2024 18:32:05 +0000 Subject: [PATCH 12/68] ci: Fix for checkout action with fetch-tags (#20917) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f22a8a518e..43af9309fe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -244,8 +244,8 @@ jobs: # # 25 was chosen arbitrarily. fetch-depth: 25 - fetch-tags: true clean: false + ref: ${{ github.ref }} - name: Limit target directory size run: script/clear-target-dir-if-larger-than 100 From 8c342ef706708aa141dda25e73c9d129201a6aeb Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Wed, 20 Nov 2024 18:35:00 +0000 Subject: [PATCH 13/68] Bump JSON schemas: package.json, tsconfig.json (#20910) Add script/update-json-schemas Updated JSON schemas to [SchemaStore/schemastore@569a343](https://github.com/SchemaStore/schemastore/tree/569a343137332470676617964bf332e06c1812eb) (2024-11-19) --- .../languages/src/json/schemas/package.json | 11 ++-- .../languages/src/json/schemas/tsconfig.json | 62 ++++++++++++++----- script/update-json-schemas | 25 ++++++++ 3 files changed, 79 insertions(+), 19 deletions(-) create mode 100755 script/update-json-schemas diff --git a/crates/languages/src/json/schemas/package.json b/crates/languages/src/json/schemas/package.json index 42c8f3c114..79d2457276 100644 --- a/crates/languages/src/json/schemas/package.json +++ b/crates/languages/src/json/schemas/package.json @@ -139,7 +139,7 @@ } }, "patternProperties": { - "^(?![\\.0-9]).": { + "^[^.0-9]+$": { "$ref": "#/definitions/packageExportsEntryOrFallback", "description": "The module path that is resolved when this environment matches the property name." } @@ -616,7 +616,7 @@ } } }, - "bundledDependencies": { + "bundleDependencies": { "description": "Array of package names that will be bundled when publishing the package.", "oneOf": [ { @@ -630,8 +630,8 @@ } ] }, - "bundleDependencies": { - "description": "DEPRECATED: This field is honored, but \"bundledDependencies\" is the correct field name.", + "bundledDependencies": { + "description": "DEPRECATED: This field is honored, but \"bundleDependencies\" is the correct field name.", "oneOf": [ { "type": "array", @@ -734,6 +734,9 @@ "registry": { "type": "string", "format": "uri" + }, + "provenance": { + "type": "boolean" } }, "additionalProperties": true diff --git a/crates/languages/src/json/schemas/tsconfig.json b/crates/languages/src/json/schemas/tsconfig.json index 808fc6f966..9174a58537 100644 --- a/crates/languages/src/json/schemas/tsconfig.json +++ b/crates/languages/src/json/schemas/tsconfig.json @@ -232,7 +232,7 @@ "$comment": "The value of 'null' is UNDOCUMENTED (https://github.com/microsoft/TypeScript/pull/18058).", "description": "Enable importing files with any extension, provided a declaration file is present.", "type": ["boolean", "null"], - "markdownDescription": "Enable importing files with any extension, provided a declaration file is present.\n\nSee more: https://www.typescriptlang.org/tsconfig#allowImportingTsExtensions" + "markdownDescription": "Enable importing files with any extension, provided a declaration file is present.\n\nSee more: https://www.typescriptlang.org/tsconfig#allowArbitraryExtensions" }, "allowImportingTsExtensions": { "$comment": "The value of 'null' is UNDOCUMENTED (https://github.com/microsoft/TypeScript/pull/18058).", @@ -426,17 +426,17 @@ "anyOf": [ { "enum": [ - "Classic", - "Node", - "Node10", - "Node16", - "NodeNext", - "Bundler" + "classic", + "node", + "node10", + "node16", + "nodenext", + "bundler" ], "markdownEnumDescriptions": [ - "It’s recommended to use `\"Node16\"` instead", - "Deprecated, use `\"Node10\"` in TypeScript 5.0+ instead", - "It’s recommended to use `\"Node16\"` instead", + "It’s recommended to use `\"node16\"` instead", + "Deprecated, use `\"node10\"` in TypeScript 5.0+ instead", + "It’s recommended to use `\"node16\"` instead", "This is the recommended setting for libraries and Node.js applications", "This is the recommended setting for libraries and Node.js applications", "This is the recommended setting in TypeScript 5.0+ for applications that use a bundler" @@ -497,10 +497,10 @@ }, "noUnusedLocals": { "$comment": "The value of 'null' is UNDOCUMENTED (https://github.com/microsoft/TypeScript/pull/18058).", - "description": "Enable error reporting when a local variables aren't read.", + "description": "Enable error reporting when a local variable isn't read.", "type": ["boolean", "null"], "default": false, - "markdownDescription": "Enable error reporting when a local variables aren't read.\n\nSee more: https://www.typescriptlang.org/tsconfig#noUnusedLocals" + "markdownDescription": "Enable error reporting when a local variable isn't read.\n\nSee more: https://www.typescriptlang.org/tsconfig#noUnusedLocals" }, "noUnusedParameters": { "$comment": "The value of 'null' is UNDOCUMENTED (https://github.com/microsoft/TypeScript/pull/18058).", @@ -949,14 +949,19 @@ "ESNext.Array", "ESNext.AsyncIterable", "ESNext.BigInt", + "ESNext.Collection", "ESNext.Intl", + "ESNext.Object", "ESNext.Promise", + "ESNext.Regexp", "ESNext.String", "ESNext.Symbol", "DOM", + "DOM.AsyncIterable", "DOM.Iterable", "ScriptHost", "WebWorker", + "WebWorker.AsyncIterable", "WebWorker.ImportScripts", "Webworker.Iterable", "ES7", @@ -1022,13 +1027,13 @@ "pattern": "^[Ee][Ss][Nn][Ee][Xx][Tt](\\.([Aa][Rr][Rr][Aa][Yy]|[Aa][Ss][Yy][Nn][Cc][Ii][Tt][Ee][Rr][Aa][Bb][Ll][Ee]|[Bb][Ii][Gg][Ii][Nn][Tt]|[Ii][Nn][Tt][Ll]|[Pp][Rr][Oo][Mm][Ii][Ss][Ee]|[Ss][Tt][Rr][Ii][Nn][Gg]|[Ss][Yy][Mm][Bb][Oo][Ll]|[Ww][Ee][Aa][Kk][Rr][Ee][Ff]|[Dd][Ee][Cc][Oo][Rr][Aa][Tt][Oo][Rr][Ss]|[Dd][Ii][Ss][Pp][Oo][Ss][Aa][Bb][Ll][Ee]))?$" }, { - "pattern": "^[Dd][Oo][Mm](\\.[Ii][Tt][Ee][Rr][Aa][Bb][Ll][Ee])?$" + "pattern": "^[Dd][Oo][Mm](\\.([Aa][Ss][Yy][Nn][Cc])?[Ii][Tt][Ee][Rr][Aa][Bb][Ll][Ee])?$" }, { "pattern": "^[Ss][Cc][Rr][Ii][Pp][Tt][Hh][Oo][Ss][Tt]$" }, { - "pattern": "^[Ww][Ee][Bb][Ww][Oo][Rr][Kk][Ee][Rr](\\.([Ii][Mm][Pp][Oo][Rr][Tt][Ss][Cc][Rr][Ii][Pp][Tt][Ss]|[Ii][Tt][Ee][Rr][Aa][Bb][Ll][Ee]))?$" + "pattern": "^[Ww][Ee][Bb][Ww][Oo][Rr][Kk][Ee][Rr](\\.([Ii][Mm][Pp][Oo][Rr][Tt][Ss][Cc][Rr][Ii][Pp][Tt][Ss]|([Aa][Ss][Yy][Nn][Cc])?[Ii][Tt][Ee][Rr][Aa][Bb][Ll][Ee]))?$" }, { "pattern": "^[Dd][Ee][Cc][Oo][Rr][Aa][Tt][Oo][Rr][Ss](\\.([Ll][Ee][Gg][Aa][Cc][Yy]))?$" @@ -1203,6 +1208,34 @@ "description": "Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting.", "type": ["boolean", "null"], "markdownDescription": "Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting.\n\nSee more: https://www.typescriptlang.org/tsconfig#verbatimModuleSyntax" + }, + "noCheck": { + "$comment": "The value of 'null' is UNDOCUMENTED (https://github.com/microsoft/TypeScript/pull/18058).", + "description": "Disable full type checking (only critical parse and emit errors will be reported)", + "type": ["boolean", "null"], + "default": false, + "markdownDescription": "Disable full type checking (only critical parse and emit errors will be reported)\n\nSee more: https://www.typescriptlang.org/tsconfig#noCheck" + }, + "isolatedDeclarations": { + "$comment": "The value of 'null' is UNDOCUMENTED (https://github.com/microsoft/TypeScript/pull/18058).", + "description": "Require sufficient annotation on exports so other tools can trivially generate declaration files.", + "type": ["boolean", "null"], + "default": false, + "markdownDescription": "Require sufficient annotation on exports so other tools can trivially generate declaration files.\n\nSee more: https://www.typescriptlang.org/tsconfig#isolatedDeclarations" + }, + "noUncheckedSideEffectImports": { + "$comment": "The value of 'null' is UNDOCUMENTED (https://github.com/microsoft/TypeScript/pull/18058).", + "description": "Check side effect imports.", + "type": ["boolean", "null"], + "default": false, + "markdownDescription": "Check side effect imports.\n\nSee more: https://www.typescriptlang.org/tsconfig#noUncheckedSideEffectImports" + }, + "strictBuiltinIteratorReturn": { + "$comment": "The value of 'null' is UNDOCUMENTED (https://github.com/microsoft/TypeScript/pull/18058).", + "description": "Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'.", + "type": ["boolean", "null"], + "default": false, + "markdownDescription": "Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'.\n\nSee more: https://www.typescriptlang.org/tsconfig#strictBuiltinIteratorReturn" } } } @@ -1423,4 +1456,3 @@ "title": "JSON schema for the TypeScript compiler's configuration file", "type": "object" } - diff --git a/script/update-json-schemas b/script/update-json-schemas new file mode 100755 index 0000000000..182e0ff03b --- /dev/null +++ b/script/update-json-schemas @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +set -euo pipefail + +cd "$(dirname "$0")/.." || exit 1 +cd crates/languages/src/json/schemas +files=( + "tsconfig.json" + "package.json" +) +for file in "${files[@]}"; do + curl -sL -o "$file" "https://raw.githubusercontent.com/SchemaStore/schemastore/master/src/schemas/json/$file" +done + +HASH="$(curl -s 'https://api.github.com/repos/SchemaStore/schemastore/commits/HEAD' | jq -r '.sha')" +SHORT_HASH="${HASH:0:7}" +DATE="$(curl -s 'https://api.github.com/repos/SchemaStore/schemastore/commits/HEAD' |jq -r .commit.author.date | cut -c1-10)" +echo +echo "Updated JSON schemas to [SchemaStore/schemastore@$SHORT_HASH](https://github.com/SchemaStore/schemastore/tree/$HASH) ($DATE)" +echo +for file in "${files[@]}"; do + echo "- [$file](https://github.com/SchemaStore/schemastore/commits/master/src/schemas/json/$file)" \ + "@ [$SHORT_HASH](https://raw.githubusercontent.com/SchemaStore/schemastore/$HASH/src/schemas/json/$file)" +done +echo From e0761db62dda8f4ed3ceb78a496ca9a6f3eaed6f Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 20 Nov 2024 13:46:44 -0700 Subject: [PATCH 14/68] Revert: "a" for "vim::AngleBrackets" (#20918) The replacement "g" didn't seem to work for everyone. Closes #20912 Updates #20104 Release Notes: - vim: Restores `dia` to mean "delete in argument" instead of "delete within angle brackets". To keep this in your own keymap use: ``` { "context": "vim_operator == a || vim_operator == i || vim_operator == cs", "use_layout_keys": true, "bindings": { "a": "vim::AngleBrackets" } } ``` --- assets/keymaps/vim.json | 3 +-- crates/vim/src/object.rs | 18 +++++++++--------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 83e332a3f4..10b2009511 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -381,8 +381,7 @@ "shift-b": "vim::CurlyBrackets", "<": "vim::AngleBrackets", ">": "vim::AngleBrackets", - "a": "vim::AngleBrackets", - "g": "vim::Argument" + "a": "vim::Argument" } }, { diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index 7c1f2fdb4c..f97312e7f8 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -1407,7 +1407,7 @@ mod test { // Generic arguments cx.set_state("fn boop() {}", Mode::Normal); - cx.simulate_keystrokes("v i g"); + cx.simulate_keystrokes("v i a"); cx.assert_state("fn boop<«A: Debugˇ», B>() {}", Mode::Visual); // Function arguments @@ -1415,11 +1415,11 @@ mod test { "fn boop(ˇarg_a: (Tuple, Of, Types), arg_b: String) {}", Mode::Normal, ); - cx.simulate_keystrokes("d a g"); + cx.simulate_keystrokes("d a a"); cx.assert_state("fn boop(ˇarg_b: String) {}", Mode::Normal); cx.set_state("std::namespace::test(\"strinˇg\", a.b.c())", Mode::Normal); - cx.simulate_keystrokes("v a g"); + cx.simulate_keystrokes("v a a"); cx.assert_state("std::namespace::test(«\"string\", ˇ»a.b.c())", Mode::Visual); // Tuple, vec, and array arguments @@ -1427,34 +1427,34 @@ mod test { "fn boop(arg_a: (Tuple, Ofˇ, Types), arg_b: String) {}", Mode::Normal, ); - cx.simulate_keystrokes("c i g"); + cx.simulate_keystrokes("c i a"); cx.assert_state( "fn boop(arg_a: (Tuple, ˇ, Types), arg_b: String) {}", Mode::Insert, ); cx.set_state("let a = (test::call(), 'p', my_macro!{ˇ});", Mode::Normal); - cx.simulate_keystrokes("c a g"); + cx.simulate_keystrokes("c a a"); cx.assert_state("let a = (test::call(), 'p'ˇ);", Mode::Insert); cx.set_state("let a = [test::call(ˇ), 300];", Mode::Normal); - cx.simulate_keystrokes("c i g"); + cx.simulate_keystrokes("c i a"); cx.assert_state("let a = [ˇ, 300];", Mode::Insert); cx.set_state( "let a = vec![Vec::new(), vecˇ![test::call(), 300]];", Mode::Normal, ); - cx.simulate_keystrokes("c a g"); + cx.simulate_keystrokes("c a a"); cx.assert_state("let a = vec![Vec::new()ˇ];", Mode::Insert); // Cursor immediately before / after brackets cx.set_state("let a = [test::call(first_arg)ˇ]", Mode::Normal); - cx.simulate_keystrokes("v i g"); + cx.simulate_keystrokes("v i a"); cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual); cx.set_state("let a = [test::callˇ(first_arg)]", Mode::Normal); - cx.simulate_keystrokes("v i g"); + cx.simulate_keystrokes("v i a"); cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual); } From e31f44450e5e8ba77250b27a81ee733ad14ddd81 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 20 Nov 2024 16:05:43 -0500 Subject: [PATCH 15/68] title_bar: Remove dependency on `extensions_ui` (#20929) This PR removes a dependency on the `extensions_ui` from the `title_bar` crate. This dependency only existed to reference the `Extensions` action, which has now been moved to the `zed_actions` crate. This allows `title_bar` to move up in the crate dependency graph. Release Notes: - N/A --- Cargo.lock | 3 +-- crates/extensions_ui/Cargo.toml | 1 + crates/extensions_ui/src/extensions_ui.rs | 4 ++-- crates/title_bar/Cargo.toml | 1 - crates/title_bar/src/title_bar.rs | 4 ++-- crates/welcome/Cargo.toml | 1 - crates/welcome/src/welcome.rs | 2 +- crates/zed/src/zed/app_menus.rs | 2 +- crates/zed_actions/src/lib.rs | 1 + 9 files changed, 9 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d65fa24b4a..bb2fb86dad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4218,6 +4218,7 @@ dependencies = [ "vim", "wasmtime-wasi", "workspace", + "zed_actions", ] [[package]] @@ -12575,7 +12576,6 @@ dependencies = [ "collections", "command_palette", "editor", - "extensions_ui", "feature_flags", "feedback", "gpui", @@ -14358,7 +14358,6 @@ dependencies = [ "client", "db", "editor", - "extensions_ui", "fuzzy", "gpui", "inline_completion_button", diff --git a/crates/extensions_ui/Cargo.toml b/crates/extensions_ui/Cargo.toml index 9709aa7a2b..2ff2f21696 100644 --- a/crates/extensions_ui/Cargo.toml +++ b/crates/extensions_ui/Cargo.toml @@ -44,6 +44,7 @@ util.workspace = true vim.workspace = true wasmtime-wasi.workspace = true workspace.workspace = true +zed_actions.workspace = true [dev-dependencies] editor = { workspace = true, features = ["test-support"] } diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs index c2ef9cf9e6..01e2b1dd66 100644 --- a/crates/extensions_ui/src/extensions_ui.rs +++ b/crates/extensions_ui/src/extensions_ui.rs @@ -38,12 +38,12 @@ use crate::extension_version_selector::{ ExtensionVersionSelector, ExtensionVersionSelectorDelegate, }; -actions!(zed, [Extensions, InstallDevExtension]); +actions!(zed, [InstallDevExtension]); pub fn init(cx: &mut AppContext) { cx.observe_new_views(move |workspace: &mut Workspace, cx| { workspace - .register_action(move |workspace, _: &Extensions, cx| { + .register_action(move |workspace, _: &zed_actions::Extensions, cx| { let existing = workspace .active_pane() .read(cx) diff --git a/crates/title_bar/Cargo.toml b/crates/title_bar/Cargo.toml index df991613ae..569231bb9c 100644 --- a/crates/title_bar/Cargo.toml +++ b/crates/title_bar/Cargo.toml @@ -32,7 +32,6 @@ auto_update.workspace = true call.workspace = true client.workspace = true command_palette.workspace = true -extensions_ui.workspace = true feedback.workspace = true feature_flags.workspace = true gpui.workspace = true diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index 2ea9ddafd7..44301520ac 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -581,7 +581,7 @@ impl TitleBar { .action("Settings", zed_actions::OpenSettings.boxed_clone()) .action("Key Bindings", Box::new(zed_actions::OpenKeymap)) .action("Themes…", theme_selector::Toggle::default().boxed_clone()) - .action("Extensions", extensions_ui::Extensions.boxed_clone()) + .action("Extensions", zed_actions::Extensions.boxed_clone()) .separator() .link( "Book Onboarding", @@ -617,7 +617,7 @@ impl TitleBar { menu.action("Settings", zed_actions::OpenSettings.boxed_clone()) .action("Key Bindings", Box::new(zed_actions::OpenKeymap)) .action("Themes…", theme_selector::Toggle::default().boxed_clone()) - .action("Extensions", extensions_ui::Extensions.boxed_clone()) + .action("Extensions", zed_actions::Extensions.boxed_clone()) .separator() .link( "Book Onboarding", diff --git a/crates/welcome/Cargo.toml b/crates/welcome/Cargo.toml index 0db1af9252..30645d5f12 100644 --- a/crates/welcome/Cargo.toml +++ b/crates/welcome/Cargo.toml @@ -18,7 +18,6 @@ test-support = [] anyhow.workspace = true client.workspace = true db.workspace = true -extensions_ui.workspace = true fuzzy.workspace = true gpui.workspace = true inline_completion_button.workspace = true diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index c8d5bf6dfc..02ce0750c4 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -250,7 +250,7 @@ impl Render for WelcomePage { "welcome page: open extensions".to_string(), ); cx.dispatch_action(Box::new( - extensions_ui::Extensions, + zed_actions::Extensions, )); })), ) diff --git a/crates/zed/src/zed/app_menus.rs b/crates/zed/src/zed/app_menus.rs index 5c01724ba7..09e21f20ab 100644 --- a/crates/zed/src/zed/app_menus.rs +++ b/crates/zed/src/zed/app_menus.rs @@ -32,7 +32,7 @@ pub fn app_menus() -> Vec { items: vec![], }), MenuItem::separator(), - MenuItem::action("Extensions", extensions_ui::Extensions), + MenuItem::action("Extensions", zed_actions::Extensions), MenuItem::action("Install CLI", install_cli::Install), MenuItem::separator(), MenuItem::action("Hide Zed", super::Hide), diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index 7ea5c923c2..bbe774652e 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -32,6 +32,7 @@ actions!( Quit, OpenKeymap, About, + Extensions, OpenLicenses, OpenTelemetryLog, DecreaseBufferFontSize, From e076f55d7827edff28196a15705329c49f828a91 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 20 Nov 2024 16:19:20 -0500 Subject: [PATCH 16/68] language_model: Remove dependency on `inline_completion_button` (#20930) This PR removes a dependency on the `inline_completion_button` crate from the `language_model` crate. We were taking on this dependency solely to call `initiate_sign_in`, which can easily be moved to the `copilot` crate. This allows `language_model` to move up in the crate dependency graph. Release Notes: - N/A --- Cargo.lock | 4 +- crates/copilot/src/copilot.rs | 4 +- crates/copilot/src/sign_in.rs | 71 +++++++++++++++++- crates/inline_completion_button/Cargo.toml | 1 - .../src/inline_completion_button.rs | 72 +------------------ crates/language_model/Cargo.toml | 1 - .../src/provider/copilot_chat.rs | 4 +- crates/welcome/Cargo.toml | 2 +- crates/welcome/src/welcome.rs | 2 +- 9 files changed, 78 insertions(+), 83 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bb2fb86dad..c0f9fd746f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6076,7 +6076,6 @@ dependencies = [ "supermaven", "theme", "ui", - "util", "workspace", "zed_actions", ] @@ -6521,7 +6520,6 @@ dependencies = [ "gpui", "http_client", "image", - "inline_completion_button", "language", "log", "menu", @@ -14356,11 +14354,11 @@ version = "0.1.0" dependencies = [ "anyhow", "client", + "copilot", "db", "editor", "fuzzy", "gpui", - "inline_completion_button", "install_cli", "picker", "project", diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index b654df1d6e..7ea289706c 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -38,8 +38,8 @@ use std::{ }; use util::{fs::remove_matching, maybe, ResultExt}; -pub use copilot_completion_provider::CopilotCompletionProvider; -pub use sign_in::CopilotCodeVerification; +pub use crate::copilot_completion_provider::CopilotCompletionProvider; +pub use crate::sign_in::{initiate_sign_in, CopilotCodeVerification}; actions!( copilot, diff --git a/crates/copilot/src/sign_in.rs b/crates/copilot/src/sign_in.rs index d63710983b..68f0eed577 100644 --- a/crates/copilot/src/sign_in.rs +++ b/crates/copilot/src/sign_in.rs @@ -5,10 +5,79 @@ use gpui::{ Styled, Subscription, ViewContext, }; use ui::{prelude::*, Button, Label, Vector, VectorName}; -use workspace::ModalView; +use util::ResultExt as _; +use workspace::notifications::NotificationId; +use workspace::{ModalView, Toast, Workspace}; const COPILOT_SIGN_UP_URL: &str = "https://github.com/features/copilot"; +struct CopilotStartingToast; + +pub fn initiate_sign_in(cx: &mut WindowContext) { + let Some(copilot) = Copilot::global(cx) else { + return; + }; + let status = copilot.read(cx).status(); + let Some(workspace) = cx.window_handle().downcast::() else { + return; + }; + match status { + Status::Starting { task } => { + let Some(workspace) = cx.window_handle().downcast::() else { + return; + }; + + let Ok(workspace) = workspace.update(cx, |workspace, cx| { + workspace.show_toast( + Toast::new( + NotificationId::unique::(), + "Copilot is starting...", + ), + cx, + ); + workspace.weak_handle() + }) else { + return; + }; + + cx.spawn(|mut cx| async move { + task.await; + if let Some(copilot) = cx.update(|cx| Copilot::global(cx)).ok().flatten() { + workspace + .update(&mut cx, |workspace, cx| match copilot.read(cx).status() { + Status::Authorized => workspace.show_toast( + Toast::new( + NotificationId::unique::(), + "Copilot has started!", + ), + cx, + ), + _ => { + workspace.dismiss_toast( + &NotificationId::unique::(), + cx, + ); + copilot + .update(cx, |copilot, cx| copilot.sign_in(cx)) + .detach_and_log_err(cx); + } + }) + .log_err(); + } + }) + .detach(); + } + _ => { + copilot.update(cx, |this, cx| this.sign_in(cx)).detach(); + workspace + .update(cx, |this, cx| { + this.toggle_modal(cx, |cx| CopilotCodeVerification::new(&copilot, cx)); + }) + .ok(); + } + } +} + pub struct CopilotCodeVerification { status: Status, connect_clicked: bool, diff --git a/crates/inline_completion_button/Cargo.toml b/crates/inline_completion_button/Cargo.toml index 13b2bfa2ea..427d0dafd8 100644 --- a/crates/inline_completion_button/Cargo.toml +++ b/crates/inline_completion_button/Cargo.toml @@ -23,7 +23,6 @@ paths.workspace = true settings.workspace = true supermaven.workspace = true ui.workspace = true -util.workspace = true workspace.workspace = true zed_actions.workspace = true diff --git a/crates/inline_completion_button/src/inline_completion_button.rs b/crates/inline_completion_button/src/inline_completion_button.rs index 8f727fd2fe..5470678d38 100644 --- a/crates/inline_completion_button/src/inline_completion_button.rs +++ b/crates/inline_completion_button/src/inline_completion_button.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use copilot::{Copilot, CopilotCodeVerification, Status}; +use copilot::{Copilot, Status}; use editor::{scroll::Autoscroll, Editor}; use fs::Fs; use gpui::{ @@ -15,7 +15,6 @@ use language::{ use settings::{update_settings_file, Settings, SettingsStore}; use std::{path::Path, sync::Arc}; use supermaven::{AccountStatus, Supermaven}; -use util::ResultExt; use workspace::{ create_and_open_local_file, item::ItemHandle, @@ -29,8 +28,6 @@ use zed_actions::OpenBrowser; const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot"; -struct CopilotStartingToast; - struct CopilotErrorToast; pub struct InlineCompletionButton { @@ -221,7 +218,7 @@ impl InlineCompletionButton { pub fn build_copilot_start_menu(&mut self, cx: &mut ViewContext) -> View { let fs = self.fs.clone(); ContextMenu::build(cx, |menu, _| { - menu.entry("Sign In", None, initiate_sign_in) + menu.entry("Sign In", None, copilot::initiate_sign_in) .entry("Disable Copilot", None, { let fs = fs.clone(); move |cx| hide_copilot(fs.clone(), cx) @@ -484,68 +481,3 @@ fn hide_copilot(fs: Arc, cx: &mut AppContext) { .inline_completion_provider = Some(InlineCompletionProvider::None); }); } - -pub fn initiate_sign_in(cx: &mut WindowContext) { - let Some(copilot) = Copilot::global(cx) else { - return; - }; - let status = copilot.read(cx).status(); - let Some(workspace) = cx.window_handle().downcast::() else { - return; - }; - match status { - Status::Starting { task } => { - let Some(workspace) = cx.window_handle().downcast::() else { - return; - }; - - let Ok(workspace) = workspace.update(cx, |workspace, cx| { - workspace.show_toast( - Toast::new( - NotificationId::unique::(), - "Copilot is starting...", - ), - cx, - ); - workspace.weak_handle() - }) else { - return; - }; - - cx.spawn(|mut cx| async move { - task.await; - if let Some(copilot) = cx.update(|cx| Copilot::global(cx)).ok().flatten() { - workspace - .update(&mut cx, |workspace, cx| match copilot.read(cx).status() { - Status::Authorized => workspace.show_toast( - Toast::new( - NotificationId::unique::(), - "Copilot has started!", - ), - cx, - ), - _ => { - workspace.dismiss_toast( - &NotificationId::unique::(), - cx, - ); - copilot - .update(cx, |copilot, cx| copilot.sign_in(cx)) - .detach_and_log_err(cx); - } - }) - .log_err(); - } - }) - .detach(); - } - _ => { - copilot.update(cx, |this, cx| this.sign_in(cx)).detach(); - workspace - .update(cx, |this, cx| { - this.toggle_modal(cx, |cx| CopilotCodeVerification::new(&copilot, cx)); - }) - .ok(); - } - } -} diff --git a/crates/language_model/Cargo.toml b/crates/language_model/Cargo.toml index e88675bbae..faca4adcc2 100644 --- a/crates/language_model/Cargo.toml +++ b/crates/language_model/Cargo.toml @@ -32,7 +32,6 @@ futures.workspace = true google_ai = { workspace = true, features = ["schemars"] } gpui.workspace = true http_client.workspace = true -inline_completion_button.workspace = true log.workspace = true menu.workspace = true ollama = { workspace = true, features = ["schemars"] } diff --git a/crates/language_model/src/provider/copilot_chat.rs b/crates/language_model/src/provider/copilot_chat.rs index a991e81fbc..0eaeaa2e3d 100644 --- a/crates/language_model/src/provider/copilot_chat.rs +++ b/crates/language_model/src/provider/copilot_chat.rs @@ -383,9 +383,7 @@ impl Render for ConfigurationView { .icon_size(IconSize::Medium) .style(ui::ButtonStyle::Filled) .full_width() - .on_click(|_, cx| { - inline_completion_button::initiate_sign_in(cx) - }), + .on_click(|_, cx| copilot::initiate_sign_in(cx)), ) .child( div().flex().w_full().items_center().child( diff --git a/crates/welcome/Cargo.toml b/crates/welcome/Cargo.toml index 30645d5f12..8ec245290d 100644 --- a/crates/welcome/Cargo.toml +++ b/crates/welcome/Cargo.toml @@ -17,10 +17,10 @@ test-support = [] [dependencies] anyhow.workspace = true client.workspace = true +copilot.workspace = true db.workspace = true fuzzy.workspace = true gpui.workspace = true -inline_completion_button.workspace = true install_cli.workspace = true picker.workspace = true project.workspace = true diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index 02ce0750c4..89f12aa37e 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -177,7 +177,7 @@ impl Render for WelcomePage { this.telemetry.report_app_event( "welcome page: sign in to copilot".to_string(), ); - inline_completion_button::initiate_sign_in(cx); + copilot::initiate_sign_in(cx); }), ), ) From 29c9f0f6a1879ac9bdacfa044f821ad46b68b2b9 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 20 Nov 2024 16:51:13 -0500 Subject: [PATCH 17/68] Extract `InlineCompletionProvider` to its own crate (#20935) This PR extracts the `InlineCompletionProvider` trait and its related types out of `editor` and into a new `inline_completion` crate. By doing so we're able to remove a dependency on `editor` from the `copilot` and `supermaven` crates. We did have to move `editor::Direction` into the `inline_completion` crate, as it is referenced by the `InlineCompletionProvider`. This should find a better home, at some point. Release Notes: - N/A --- Cargo.lock | 13 +++++++++++++ Cargo.toml | 2 ++ crates/copilot/Cargo.toml | 8 ++++---- .../copilot/src/copilot_completion_provider.rs | 2 +- crates/editor/Cargo.toml | 1 + crates/editor/src/editor.rs | 10 ++-------- crates/inline_completion/Cargo.toml | 18 ++++++++++++++++++ crates/inline_completion/LICENSE-GPL | 1 + .../src/inline_completion.rs} | 11 ++++++++++- crates/supermaven/Cargo.toml | 6 +++--- .../src/supermaven_completion_provider.rs | 2 +- 11 files changed, 56 insertions(+), 18 deletions(-) create mode 100644 crates/inline_completion/Cargo.toml create mode 120000 crates/inline_completion/LICENSE-GPL rename crates/{editor/src/inline_completion_provider.rs => inline_completion/src/inline_completion.rs} (93%) diff --git a/Cargo.lock b/Cargo.lock index c0f9fd746f..c27b9b303c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2876,6 +2876,7 @@ dependencies = [ "gpui", "http_client", "indoc", + "inline_completion", "language", "lsp", "menu", @@ -3721,6 +3722,7 @@ dependencies = [ "gpui", "http_client", "indoc", + "inline_completion", "itertools 0.13.0", "language", "linkify", @@ -6056,6 +6058,16 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "inline_completion" +version = "0.1.0" +dependencies = [ + "gpui", + "language", + "project", + "text", +] + [[package]] name = "inline_completion_button" version = "0.1.0" @@ -11781,6 +11793,7 @@ dependencies = [ "futures 0.3.31", "gpui", "http_client", + "inline_completion", "language", "log", "postage", diff --git a/Cargo.toml b/Cargo.toml index 98922a7ca2..252549d116 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,7 @@ members = [ "crates/http_client", "crates/image_viewer", "crates/indexed_docs", + "crates/inline_completion", "crates/inline_completion_button", "crates/install_cli", "crates/journal", @@ -221,6 +222,7 @@ html_to_markdown = { path = "crates/html_to_markdown" } http_client = { path = "crates/http_client" } image_viewer = { path = "crates/image_viewer" } indexed_docs = { path = "crates/indexed_docs" } +inline_completion = { path = "crates/inline_completion" } inline_completion_button = { path = "crates/inline_completion_button" } install_cli = { path = "crates/install_cli" } journal = { path = "crates/journal" } diff --git a/crates/copilot/Cargo.toml b/crates/copilot/Cargo.toml index 2a54497562..2cbe76c16e 100644 --- a/crates/copilot/Cargo.toml +++ b/crates/copilot/Cargo.toml @@ -29,14 +29,14 @@ anyhow.workspace = true async-compression.workspace = true async-tar.workspace = true chrono.workspace = true -collections.workspace = true client.workspace = true +collections.workspace = true command_palette_hooks.workspace = true -editor.workspace = true fs.workspace = true futures.workspace = true gpui.workspace = true http_client.workspace = true +inline_completion.workspace = true language.workspace = true lsp.workspace = true menu.workspace = true @@ -44,12 +44,12 @@ node_runtime.workspace = true parking_lot.workspace = true paths.workspace = true project.workspace = true +schemars = { workspace = true, optional = true } serde.workspace = true serde_json.workspace = true -schemars = { workspace = true, optional = true } -strum.workspace = true settings.workspace = true smol.workspace = true +strum.workspace = true task.workspace = true ui.workspace = true util.workspace = true diff --git a/crates/copilot/src/copilot_completion_provider.rs b/crates/copilot/src/copilot_completion_provider.rs index 059d3a4236..85fe20f1ae 100644 --- a/crates/copilot/src/copilot_completion_provider.rs +++ b/crates/copilot/src/copilot_completion_provider.rs @@ -1,8 +1,8 @@ use crate::{Completion, Copilot}; use anyhow::Result; use client::telemetry::Telemetry; -use editor::{CompletionProposal, Direction, InlayProposal, InlineCompletionProvider}; use gpui::{AppContext, EntityId, Model, ModelContext, Task}; +use inline_completion::{CompletionProposal, Direction, InlayProposal, InlineCompletionProvider}; use language::{ language_settings::{all_language_settings, AllLanguageSettings}, Buffer, OffsetRangeExt, ToOffset, diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index a27ac97d41..8d03fa79f0 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -46,6 +46,7 @@ git.workspace = true gpui.workspace = true http_client.workspace = true indoc.workspace = true +inline_completion.workspace = true itertools.workspace = true language.workspace = true linkify.workspace = true diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 7f31cdedd3..1435681587 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -28,7 +28,6 @@ mod hover_popover; mod hunk_diff; mod indent_guides; mod inlay_hint_cache; -mod inline_completion_provider; pub mod items; mod linked_editing_ranges; mod lsp_ext; @@ -87,7 +86,8 @@ pub(crate) use hunk_diff::HoveredHunk; use hunk_diff::{diff_hunk_to_display, ExpandedHunks}; use indent_guides::ActiveIndentGuidesState; use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy}; -pub use inline_completion_provider::*; +pub use inline_completion::Direction; +use inline_completion::{InlayProposal, InlineCompletionProvider, InlineCompletionProviderHandle}; pub use items::MAX_TAB_TITLE_LEN; use itertools::Itertools; use language::{ @@ -273,12 +273,6 @@ enum DocumentHighlightRead {} enum DocumentHighlightWrite {} enum InputComposition {} -#[derive(Copy, Clone, PartialEq, Eq)] -pub enum Direction { - Prev, - Next, -} - #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum Navigated { Yes, diff --git a/crates/inline_completion/Cargo.toml b/crates/inline_completion/Cargo.toml new file mode 100644 index 0000000000..237b0ff43f --- /dev/null +++ b/crates/inline_completion/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "inline_completion" +version = "0.1.0" +edition = "2021" +publish = false +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/inline_completion.rs" + +[dependencies] +gpui.workspace = true +language.workspace = true +project.workspace = true +text.workspace = true diff --git a/crates/inline_completion/LICENSE-GPL b/crates/inline_completion/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/inline_completion/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/editor/src/inline_completion_provider.rs b/crates/inline_completion/src/inline_completion.rs similarity index 93% rename from crates/editor/src/inline_completion_provider.rs rename to crates/inline_completion/src/inline_completion.rs index 1085a6294e..689bc03174 100644 --- a/crates/editor/src/inline_completion_provider.rs +++ b/crates/inline_completion/src/inline_completion.rs @@ -1,9 +1,18 @@ -use crate::Direction; use gpui::{AppContext, Model, ModelContext}; use language::Buffer; use std::ops::Range; use text::{Anchor, Rope}; +// TODO: Find a better home for `Direction`. +// +// This should live in an ancestor crate of `editor` and `inline_completion`, +// but at time of writing there isn't an obvious spot. +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum Direction { + Prev, + Next, +} + pub enum InlayProposal { Hint(Anchor, project::InlayHint), Suggestion(Anchor, Rope), diff --git a/crates/supermaven/Cargo.toml b/crates/supermaven/Cargo.toml index e04d0ef51b..fd0adb0d98 100644 --- a/crates/supermaven/Cargo.toml +++ b/crates/supermaven/Cargo.toml @@ -16,17 +16,17 @@ doctest = false anyhow.workspace = true client.workspace = true collections.workspace = true -editor.workspace = true -gpui.workspace = true futures.workspace = true +gpui.workspace = true +inline_completion.workspace = true language.workspace = true log.workspace = true postage.workspace = true serde.workspace = true serde_json.workspace = true settings.workspace = true -supermaven_api.workspace = true smol.workspace = true +supermaven_api.workspace = true text.workspace = true ui.workspace = true unicode-segmentation.workspace = true diff --git a/crates/supermaven/src/supermaven_completion_provider.rs b/crates/supermaven/src/supermaven_completion_provider.rs index b9185c9762..5e77cc21ef 100644 --- a/crates/supermaven/src/supermaven_completion_provider.rs +++ b/crates/supermaven/src/supermaven_completion_provider.rs @@ -1,9 +1,9 @@ use crate::{Supermaven, SupermavenCompletionStateId}; use anyhow::Result; use client::telemetry::Telemetry; -use editor::{CompletionProposal, Direction, InlayProposal, InlineCompletionProvider}; use futures::StreamExt as _; use gpui::{AppContext, EntityId, Model, ModelContext, Task}; +use inline_completion::{CompletionProposal, Direction, InlayProposal, InlineCompletionProvider}; use language::{language_settings::all_language_settings, Anchor, Buffer, BufferSnapshot}; use std::{ ops::{AddAssign, Range}, From ebca6a8f3d151e8b5af5922a2fc3055870feff01 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 20 Nov 2024 15:34:24 -0700 Subject: [PATCH 18/68] Send os_version and country to amplitude (#20936) Release Notes: - N/A --- crates/client/src/telemetry.rs | 2 ++ crates/collab/src/api/events.rs | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index fcb9ced4e5..583f9757c4 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -224,6 +224,8 @@ impl Telemetry { cx.background_executor() .spawn({ let state = state.clone(); + let os_version = os_version(); + state.lock().os_version = Some(os_version.clone()); async move { if let Some(tempfile) = File::create(Self::log_file_path()).log_err() { state.lock().log_file = Some(tempfile); diff --git a/crates/collab/src/api/events.rs b/crates/collab/src/api/events.rs index 1c936bac39..2679193cad 100644 --- a/crates/collab/src/api/events.rs +++ b/crates/collab/src/api/events.rs @@ -1555,15 +1555,15 @@ fn for_snowflake( ); map.insert("signed_in".to_string(), event.signed_in.into()); if let Some(country_code) = country_code.as_ref() { - map.insert("country_code".to_string(), country_code.clone().into()); + map.insert("country".to_string(), country_code.clone().into()); } } + // NOTE: most amplitude user properties are read out of our event_properties + // dictionary. See https://app.amplitude.com/data/zed/Zed/sources/detail/production/falcon%3A159998 + // for how that is configured. let user_properties = Some(serde_json::json!({ "is_staff": body.is_staff, - "Country": country_code.clone(), - "OS": format!("{} {}", body.os_name, body.os_version.clone().unwrap_or_default()), - "Version": body.app_version.clone(), })); Some(SnowflakeRow { From 427c2017c3b43e3f48ee6e7ce42c4c789c5517cb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 20 Nov 2024 15:34:59 -0700 Subject: [PATCH 19/68] Update Rust crate serde_json to v1.0.133 (#20932) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [serde_json](https://redirect.github.com/serde-rs/json) | dependencies | patch | `1.0.132` -> `1.0.133` | | [serde_json](https://redirect.github.com/serde-rs/json) | workspace.dependencies | patch | `1.0.132` -> `1.0.133` | --- ### Release Notes
serde-rs/json (serde_json) ### [`v1.0.133`](https://redirect.github.com/serde-rs/json/releases/tag/v1.0.133) [Compare Source](https://redirect.github.com/serde-rs/json/compare/v1.0.132...v1.0.133) - Implement From<\[T; N]> for serde_json::Value ([#​1215](https://redirect.github.com/serde-rs/json/issues/1215))
--- ### Configuration 📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone America/New_York, Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about these updates again. --- - [ ] If you want to rebase/retry this PR, check this box --- Release Notes: - N/A Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c27b9b303c..6d38e2f6b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10864,9 +10864,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.132" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ "indexmap 2.6.0", "itoa", From 6d4a5f9ad2d53f915040064472fe6cb38743af2f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 20 Nov 2024 15:35:08 -0700 Subject: [PATCH 20/68] Update Rust crate libc to v0.2.164 (#20931) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [libc](https://redirect.github.com/rust-lang/libc) | workspace.dependencies | patch | `0.2.162` -> `0.2.164` | --- ### Release Notes
rust-lang/libc (libc) ### [`v0.2.164`](https://redirect.github.com/rust-lang/libc/blob/HEAD/CHANGELOG.md#02164---2024-11-16) [Compare Source](https://redirect.github.com/rust-lang/libc/compare/0.2.163...0.2.164) ##### MSRV This release increases the MSRV of `libc` to 1.63. ##### Other - CI: remove tests with rust < 1.63 [#​4051](https://redirect.github.com/rust-lang/libc/pull/4051) - MSRV: document the MSRV of the stable channel to be 1.63 [#​4040](https://redirect.github.com/rust-lang/libc/pull/4040) - MacOS: move ifconf to s_no_extra_traits [#​4051](https://redirect.github.com/rust-lang/libc/pull/4051) ### [`v0.2.163`](https://redirect.github.com/rust-lang/libc/blob/HEAD/CHANGELOG.md#02163---2024-11-16) [Compare Source](https://redirect.github.com/rust-lang/libc/compare/0.2.162...0.2.163) ##### Added - Aix: add more `dlopen` flags [#​4044](https://redirect.github.com/rust-lang/libc/pull/4044) - Android: add group calls [#​3499](https://redirect.github.com/rust-lang/libc/pull/3499) - FreeBSD: add `TCP_FUNCTION_BLK` and `TCP_FUNCTION_ALIAS` [#​4047](https://redirect.github.com/rust-lang/libc/pull/4047) - Linux: add `confstr` [#​3612](https://redirect.github.com/rust-lang/libc/pull/3612) - Solarish: add `aio` [#​4033](https://redirect.github.com/rust-lang/libc/pull/4033) - Solarish: add `arc4random*` [#​3944](https://redirect.github.com/rust-lang/libc/pull/3944) ##### Changed - Emscripten: upgrade emsdk to 3.1.68 [#​3962](https://redirect.github.com/rust-lang/libc/pull/3962) - Hurd: use more standard types [#​3733](https://redirect.github.com/rust-lang/libc/pull/3733) - Hurd: use the standard `ssize_t = isize` [#​4029](https://redirect.github.com/rust-lang/libc/pull/4029) - Solaris: fix `confstr` and `ucontext_t` [#​4035](https://redirect.github.com/rust-lang/libc/pull/4035) ##### Other - CI: add Solaris [#​4035](https://redirect.github.com/rust-lang/libc/pull/4035) - CI: add `i686-unknown-freebsd` [#​3997](https://redirect.github.com/rust-lang/libc/pull/3997) - CI: ensure that calls to `sort` do not depend on locale [#​4026](https://redirect.github.com/rust-lang/libc/pull/4026) - Specify `rust-version` in `Cargo.toml` [#​4041](https://redirect.github.com/rust-lang/libc/pull/4041)
--- ### Configuration 📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone America/New_York, Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- Release Notes: - N/A Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6d38e2f6b1..6c0e7b4614 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6680,9 +6680,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "libc" -version = "0.2.162" +version = "0.2.164" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398" +checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f" [[package]] name = "libdbus-sys" From 33bed8d680ffcf0c19ebf442a44e4223099da02f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 20 Nov 2024 15:36:27 -0700 Subject: [PATCH 21/68] Update Rust crate ctor to v0.2.9 (#20928) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [ctor](https://redirect.github.com/mmastrac/rust-ctor) | workspace.dependencies | patch | `0.2.8` -> `0.2.9` | --- ### Configuration 📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone America/New_York, Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- Release Notes: - N/A Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6c0e7b4614..429c80e9d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3351,9 +3351,9 @@ dependencies = [ [[package]] name = "ctor" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ "quote", "syn 2.0.87", From 335b112abda15767c2540dc011d9ce404be93522 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 21 Nov 2024 00:43:03 +0100 Subject: [PATCH 22/68] title_bar: Remove dependency on recent_projects (#20942) Use actions defined in zed_actions to interface with that crate instead. One drawback of this is that we now hide call controls when any modal is visible (we used to hide them just when ssh modal was deployed). Release Notes: - N/A --- Cargo.lock | 2 +- crates/recent_projects/Cargo.toml | 1 + crates/recent_projects/src/recent_projects.rs | 15 +-------------- crates/title_bar/Cargo.toml | 1 - crates/title_bar/src/application_menu.rs | 2 +- crates/title_bar/src/collab.rs | 4 +--- crates/title_bar/src/title_bar.rs | 17 ++++++++--------- crates/zed/src/zed/app_menus.rs | 2 +- crates/zed_actions/src/lib.rs | 8 ++++++++ 9 files changed, 22 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 429c80e9d4..3cf7a59177 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9684,6 +9684,7 @@ dependencies = [ "ui", "util", "workspace", + "zed_actions", ] [[package]] @@ -12594,7 +12595,6 @@ dependencies = [ "notifications", "pretty_assertions", "project", - "recent_projects", "remote", "rpc", "serde", diff --git a/crates/recent_projects/Cargo.toml b/crates/recent_projects/Cargo.toml index b1759de778..827afff7c0 100644 --- a/crates/recent_projects/Cargo.toml +++ b/crates/recent_projects/Cargo.toml @@ -40,6 +40,7 @@ ui.workspace = true util.workspace = true workspace.workspace = true paths.workspace = true +zed_actions.workspace = true [dev-dependencies] editor = { workspace = true, features = ["test-support"] } diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index e01309cacd..c08136cdf5 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -16,7 +16,6 @@ use picker::{ Picker, PickerDelegate, }; pub use remote_servers::RemoteServerProjects; -use serde::Deserialize; use settings::Settings; pub use ssh_connections::SshSettings; use std::{ @@ -29,19 +28,7 @@ use workspace::{ CloseIntent, ModalView, OpenOptions, SerializedWorkspaceLocation, Workspace, WorkspaceId, WORKSPACE_DB, }; - -#[derive(PartialEq, Clone, Deserialize, Default)] -pub struct OpenRecent { - #[serde(default = "default_create_new_window")] - pub create_new_window: bool, -} - -fn default_create_new_window() -> bool { - false -} - -gpui::impl_actions!(projects, [OpenRecent]); -gpui::actions!(projects, [OpenRemote]); +use zed_actions::{OpenRecent, OpenRemote}; pub fn init(cx: &mut AppContext) { SshSettings::register(cx); diff --git a/crates/title_bar/Cargo.toml b/crates/title_bar/Cargo.toml index 569231bb9c..05bd1be502 100644 --- a/crates/title_bar/Cargo.toml +++ b/crates/title_bar/Cargo.toml @@ -37,7 +37,6 @@ feature_flags.workspace = true gpui.workspace = true notifications.workspace = true project.workspace = true -recent_projects.workspace = true remote.workspace = true rpc.workspace = true serde.workspace = true diff --git a/crates/title_bar/src/application_menu.rs b/crates/title_bar/src/application_menu.rs index 13ee10c141..c3994f81d7 100644 --- a/crates/title_bar/src/application_menu.rs +++ b/crates/title_bar/src/application_menu.rs @@ -100,7 +100,7 @@ impl Render for ApplicationMenu { .action("Open a new Project...", Box::new(workspace::Open)) .action( "Open Recent Projects...", - Box::new(recent_projects::OpenRecent { + Box::new(zed_actions::OpenRecent { create_new_window: false, }), ) diff --git a/crates/title_bar/src/collab.rs b/crates/title_bar/src/collab.rs index 805c0e7202..649dfb34f7 100644 --- a/crates/title_bar/src/collab.rs +++ b/crates/title_bar/src/collab.rs @@ -284,9 +284,7 @@ impl TitleBar { let is_connecting_to_project = self .workspace - .update(cx, |workspace, cx| { - recent_projects::is_connecting_over_ssh(workspace, cx) - }) + .update(cx, |workspace, cx| workspace.has_active_modal(cx)) .unwrap_or(false); let room = room.read(cx); diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index 44301520ac..bcf13a5ac7 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -18,7 +18,6 @@ use gpui::{ StatefulInteractiveElement, Styled, Subscription, View, ViewContext, VisualContext, WeakView, }; use project::{Project, RepositoryEntry}; -use recent_projects::{OpenRemote, RecentProjects}; use rpc::proto; use smallvec::SmallVec; use std::sync::Arc; @@ -30,7 +29,7 @@ use ui::{ use util::ResultExt; use vcs_menu::{BranchList, OpenRecent as ToggleVcsMenu}; use workspace::{notifications::NotifyResultExt, Workspace}; -use zed_actions::OpenBrowser; +use zed_actions::{OpenBrowser, OpenRecent, OpenRemote}; #[cfg(feature = "stories")] pub use stories::*; @@ -397,7 +396,6 @@ impl TitleBar { "Open recent project".to_string() }; - let workspace = self.workspace.clone(); Button::new("project_name_trigger", name) .when(!is_project_selected, |b| b.color(Color::Muted)) .style(ButtonStyle::Subtle) @@ -405,18 +403,19 @@ impl TitleBar { .tooltip(move |cx| { Tooltip::for_action( "Recent Projects", - &recent_projects::OpenRecent { + &zed_actions::OpenRecent { create_new_window: false, }, cx, ) }) .on_click(cx.listener(move |_, _, cx| { - if let Some(workspace) = workspace.upgrade() { - workspace.update(cx, |workspace, cx| { - RecentProjects::open(workspace, false, cx); - }) - } + cx.dispatch_action( + OpenRecent { + create_new_window: false, + } + .boxed_clone(), + ); })) } diff --git a/crates/zed/src/zed/app_menus.rs b/crates/zed/src/zed/app_menus.rs index 09e21f20ab..824704fca5 100644 --- a/crates/zed/src/zed/app_menus.rs +++ b/crates/zed/src/zed/app_menus.rs @@ -50,7 +50,7 @@ pub fn app_menus() -> Vec { MenuItem::action("Open…", workspace::Open), MenuItem::action( "Open Recent...", - recent_projects::OpenRecent { + zed_actions::OpenRecent { create_new_window: true, }, ), diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index bbe774652e..2f33583429 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -50,3 +50,11 @@ pub struct InlineAssist { } impl_actions!(assistant, [InlineAssist]); + +#[derive(PartialEq, Clone, Deserialize, Default)] +pub struct OpenRecent { + #[serde(default)] + pub create_new_window: bool, +} +gpui::impl_actions!(projects, [OpenRecent]); +gpui::actions!(projects, [OpenRemote]); From cbba44900d07f12142df0ba2cd90534f2d83d815 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 20 Nov 2024 18:49:34 -0500 Subject: [PATCH 23/68] Add `language_models` crate to house language model providers (#20945) This PR adds a new `language_models` crate to house the various language model providers. By extracting the provider definitions out of `language_model`, we're able to remove `language_model`'s dependency on `editor`, which improves incremental compilation when changing `editor`. Release Notes: - N/A --- Cargo.lock | 43 +++++++--- Cargo.toml | 2 + crates/assistant/Cargo.toml | 1 + crates/assistant/src/assistant_panel.rs | 10 +-- crates/assistant/src/assistant_settings.rs | 11 ++- crates/assistant/src/context.rs | 6 +- crates/assistant/src/inline_assistant.rs | 5 +- .../src/terminal_inline_assistant.rs | 4 +- crates/language_model/Cargo.toml | 34 +------- .../{provider/fake.rs => fake_provider.rs} | 0 crates/language_model/src/language_model.rs | 28 +++---- crates/language_model/src/registry.rs | 75 ++--------------- crates/language_models/Cargo.toml | 49 ++++++++++++ crates/language_models/LICENSE-GPL | 1 + crates/language_models/src/language_models.rs | 80 +++++++++++++++++++ .../src/logging.rs | 0 .../src/provider.rs | 2 - .../src/provider/anthropic.rs | 15 ++-- .../src/provider/cloud.rs | 22 ++--- .../src/provider/copilot_chat.rs | 11 ++- .../src/provider/google.rs | 13 +-- .../src/provider/ollama.rs | 13 +-- .../src/provider/open_ai.rs | 12 +-- .../src/settings.rs | 20 +++-- crates/zed/Cargo.toml | 1 + crates/zed/src/main.rs | 3 +- crates/zed/src/zed.rs | 3 +- 27 files changed, 265 insertions(+), 199 deletions(-) rename crates/language_model/src/{provider/fake.rs => fake_provider.rs} (100%) create mode 100644 crates/language_models/Cargo.toml create mode 120000 crates/language_models/LICENSE-GPL create mode 100644 crates/language_models/src/language_models.rs rename crates/{language_model => language_models}/src/logging.rs (100%) rename crates/{language_model => language_models}/src/provider.rs (64%) rename crates/{language_model => language_models}/src/provider/anthropic.rs (98%) rename crates/{language_model => language_models}/src/provider/cloud.rs (98%) rename crates/{language_model => language_models}/src/provider/copilot_chat.rs (98%) rename crates/{language_model => language_models}/src/provider/google.rs (98%) rename crates/{language_model => language_models}/src/provider/ollama.rs (98%) rename crates/{language_model => language_models}/src/provider/open_ai.rs (99%) rename crates/{language_model => language_models}/src/settings.rs (97%) diff --git a/Cargo.lock b/Cargo.lock index 3cf7a59177..a8ff3abe01 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -402,6 +402,7 @@ dependencies = [ "indoc", "language", "language_model", + "language_models", "languages", "log", "lsp", @@ -6520,27 +6521,48 @@ dependencies = [ "anthropic", "anyhow", "base64 0.22.1", - "client", "collections", - "copilot", - "ctor", - "editor", - "env_logger 0.11.5", - "feature_flags", "futures 0.3.31", "google_ai", "gpui", "http_client", "image", - "language", "log", - "menu", "ollama", "open_ai", "parking_lot", + "proto", + "schemars", + "serde", + "serde_json", + "smol", + "strum 0.25.0", + "ui", + "util", +] + +[[package]] +name = "language_models" +version = "0.1.0" +dependencies = [ + "anthropic", + "anyhow", + "client", + "collections", + "copilot", + "editor", + "feature_flags", + "fs", + "futures 0.3.31", + "google_ai", + "gpui", + "http_client", + "language_model", + "menu", + "ollama", + "open_ai", "project", "proto", - "rand 0.8.5", "schemars", "serde", "serde_json", @@ -6548,12 +6570,10 @@ dependencies = [ "smol", "strum 0.25.0", "telemetry_events", - "text", "theme", "thiserror 1.0.69", "tiktoken-rs", "ui", - "unindent", "util", ] @@ -15481,6 +15501,7 @@ dependencies = [ "journal", "language", "language_model", + "language_models", "language_selector", "language_tools", "languages", diff --git a/Cargo.toml b/Cargo.toml index 252549d116..8357160268 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,7 @@ members = [ "crates/journal", "crates/language", "crates/language_model", + "crates/language_models", "crates/language_selector", "crates/language_tools", "crates/languages", @@ -228,6 +229,7 @@ install_cli = { path = "crates/install_cli" } journal = { path = "crates/journal" } language = { path = "crates/language" } language_model = { path = "crates/language_model" } +language_models = { path = "crates/language_models" } language_selector = { path = "crates/language_selector" } language_tools = { path = "crates/language_tools" } languages = { path = "crates/languages" } diff --git a/crates/assistant/Cargo.toml b/crates/assistant/Cargo.toml index 21153b6fcc..7f5aef3f46 100644 --- a/crates/assistant/Cargo.toml +++ b/crates/assistant/Cargo.toml @@ -50,6 +50,7 @@ indexed_docs.workspace = true indoc.workspace = true language.workspace = true language_model.workspace = true +language_models.workspace = true log.workspace = true lsp.workspace = true markdown.workspace = true diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index c89595c7da..ff60f2b918 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -50,11 +50,11 @@ use indexed_docs::IndexedDocsStore; use language::{ language_settings::SoftWrap, BufferSnapshot, LanguageRegistry, LspAdapterDelegate, ToOffset, }; -use language_model::{ - provider::cloud::PROVIDER_ID, LanguageModelProvider, LanguageModelProviderId, - LanguageModelRegistry, Role, -}; use language_model::{LanguageModelImage, LanguageModelToolUse}; +use language_model::{ + LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, Role, + ZED_CLOUD_PROVIDER_ID, +}; use multi_buffer::MultiBufferRow; use picker::{Picker, PickerDelegate}; use project::lsp_store::LocalLspAdapterDelegate; @@ -664,7 +664,7 @@ impl AssistantPanel { // If we're signed out and don't have a provider configured, or we're signed-out AND Zed.dev is // the provider, we want to show a nudge to sign in. let show_zed_ai_notice = client_status.is_signed_out() - && active_provider.map_or(true, |provider| provider.id().0 == PROVIDER_ID); + && active_provider.map_or(true, |provider| provider.id().0 == ZED_CLOUD_PROVIDER_ID); self.show_zed_ai_notice = show_zed_ai_notice; cx.notify(); diff --git a/crates/assistant/src/assistant_settings.rs b/crates/assistant/src/assistant_settings.rs index 98188305fb..a782f05d03 100644 --- a/crates/assistant/src/assistant_settings.rs +++ b/crates/assistant/src/assistant_settings.rs @@ -5,13 +5,12 @@ use anthropic::Model as AnthropicModel; use feature_flags::FeatureFlagAppExt; use fs::Fs; use gpui::{AppContext, Pixels}; -use language_model::provider::open_ai; -use language_model::settings::{ - AnthropicSettingsContent, AnthropicSettingsContentV1, OllamaSettingsContent, - OpenAiSettingsContent, OpenAiSettingsContentV1, VersionedAnthropicSettingsContent, - VersionedOpenAiSettingsContent, +use language_model::{CloudModel, LanguageModel}; +use language_models::{ + provider::open_ai, AllLanguageModelSettings, AnthropicSettingsContent, + AnthropicSettingsContentV1, OllamaSettingsContent, OpenAiSettingsContent, + OpenAiSettingsContentV1, VersionedAnthropicSettingsContent, VersionedOpenAiSettingsContent, }; -use language_model::{settings::AllLanguageModelSettings, CloudModel, LanguageModel}; use ollama::Model as OllamaModel; use schemars::{schema::Schema, JsonSchema}; use serde::{Deserialize, Serialize}; diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index 39c31d7c58..570180ed74 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -25,13 +25,15 @@ use gpui::{ use language::{AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, Point, ToOffset}; use language_model::{ - logging::report_assistant_event, - provider::cloud::{MaxMonthlySpendReachedError, PaymentRequiredError}, LanguageModel, LanguageModelCacheConfiguration, LanguageModelCompletionEvent, LanguageModelImage, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult, LanguageModelToolUse, MessageContent, Role, StopReason, }; +use language_models::{ + provider::cloud::{MaxMonthlySpendReachedError, PaymentRequiredError}, + report_assistant_event, +}; use open_ai::Model as OpenAiModel; use paths::contexts_dir; use project::Project; diff --git a/crates/assistant/src/inline_assistant.rs b/crates/assistant/src/inline_assistant.rs index 22620ca2c2..855972c267 100644 --- a/crates/assistant/src/inline_assistant.rs +++ b/crates/assistant/src/inline_assistant.rs @@ -30,9 +30,10 @@ use gpui::{ }; use language::{Buffer, IndentKind, Point, Selection, TransactionId}; use language_model::{ - logging::report_assistant_event, LanguageModel, LanguageModelRegistry, LanguageModelRequest, - LanguageModelRequestMessage, LanguageModelTextStream, Role, + LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, + LanguageModelTextStream, Role, }; +use language_models::report_assistant_event; use multi_buffer::MultiBufferRow; use parking_lot::Mutex; use project::{CodeAction, ProjectTransaction}; diff --git a/crates/assistant/src/terminal_inline_assistant.rs b/crates/assistant/src/terminal_inline_assistant.rs index 2fb4b4ffda..51738b90e4 100644 --- a/crates/assistant/src/terminal_inline_assistant.rs +++ b/crates/assistant/src/terminal_inline_assistant.rs @@ -17,9 +17,9 @@ use gpui::{ }; use language::Buffer; use language_model::{ - logging::report_assistant_event, LanguageModelRegistry, LanguageModelRequest, - LanguageModelRequestMessage, Role, + LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role, }; +use language_models::report_assistant_event; use settings::Settings; use std::{ cmp, diff --git a/crates/language_model/Cargo.toml b/crates/language_model/Cargo.toml index faca4adcc2..0fc54d509d 100644 --- a/crates/language_model/Cargo.toml +++ b/crates/language_model/Cargo.toml @@ -13,56 +13,30 @@ path = "src/language_model.rs" doctest = false [features] -test-support = [ - "editor/test-support", - "language/test-support", - "project/test-support", - "text/test-support", -] +test-support = [] [dependencies] anthropic = { workspace = true, features = ["schemars"] } anyhow.workspace = true -client.workspace = true +base64.workspace = true collections.workspace = true -copilot = { workspace = true, features = ["schemars"] } -editor.workspace = true -feature_flags.workspace = true futures.workspace = true google_ai = { workspace = true, features = ["schemars"] } gpui.workspace = true http_client.workspace = true +image.workspace = true log.workspace = true -menu.workspace = true ollama = { workspace = true, features = ["schemars"] } open_ai = { workspace = true, features = ["schemars"] } parking_lot.workspace = true proto.workspace = true -project.workspace = true schemars.workspace = true serde.workspace = true serde_json.workspace = true -settings.workspace = true smol.workspace = true strum.workspace = true -telemetry_events.workspace = true -theme.workspace = true -thiserror.workspace = true -tiktoken-rs.workspace = true ui.workspace = true util.workspace = true -base64.workspace = true -image.workspace = true - [dev-dependencies] -ctor.workspace = true -editor = { workspace = true, features = ["test-support"] } -env_logger.workspace = true -language = { workspace = true, features = ["test-support"] } -log.workspace = true -project = { workspace = true, features = ["test-support"] } -proto = { workspace = true, features = ["test-support"] } -rand.workspace = true -text = { workspace = true, features = ["test-support"] } -unindent.workspace = true +gpui = { workspace = true, features = ["test-support"] } diff --git a/crates/language_model/src/provider/fake.rs b/crates/language_model/src/fake_provider.rs similarity index 100% rename from crates/language_model/src/provider/fake.rs rename to crates/language_model/src/fake_provider.rs diff --git a/crates/language_model/src/language_model.rs b/crates/language_model/src/language_model.rs index a2f5a072a9..f9df34a2d1 100644 --- a/crates/language_model/src/language_model.rs +++ b/crates/language_model/src/language_model.rs @@ -1,23 +1,19 @@ -pub mod logging; mod model; -pub mod provider; mod rate_limiter; mod registry; mod request; mod role; -pub mod settings; + +#[cfg(any(test, feature = "test-support"))] +pub mod fake_provider; use anyhow::Result; -use client::{Client, UserStore}; use futures::FutureExt; use futures::{future::BoxFuture, stream::BoxStream, StreamExt, TryStreamExt as _}; -use gpui::{ - AnyElement, AnyView, AppContext, AsyncAppContext, Model, SharedString, Task, WindowContext, -}; +use gpui::{AnyElement, AnyView, AppContext, AsyncAppContext, SharedString, Task, WindowContext}; pub use model::*; -use project::Fs; use proto::Plan; -pub(crate) use rate_limiter::*; +pub use rate_limiter::*; pub use registry::*; pub use request::*; pub use role::*; @@ -27,14 +23,10 @@ use std::fmt; use std::{future::Future, sync::Arc}; use ui::IconName; -pub fn init( - user_store: Model, - client: Arc, - fs: Arc, - cx: &mut AppContext, -) { - settings::init(fs, cx); - registry::init(user_store, client, cx); +pub const ZED_CLOUD_PROVIDER_ID: &str = "zed.dev"; + +pub fn init(cx: &mut AppContext) { + registry::init(cx); } /// The availability of a [`LanguageModel`]. @@ -184,7 +176,7 @@ pub trait LanguageModel: Send + Sync { } #[cfg(any(test, feature = "test-support"))] - fn as_fake(&self) -> &provider::fake::FakeLanguageModel { + fn as_fake(&self) -> &fake_provider::FakeLanguageModel { unimplemented!() } } diff --git a/crates/language_model/src/registry.rs b/crates/language_model/src/registry.rs index 72dfd998d4..88b2e8301c 100644 --- a/crates/language_model/src/registry.rs +++ b/crates/language_model/src/registry.rs @@ -1,76 +1,17 @@ -use crate::provider::cloud::RefreshLlmTokenListener; use crate::{ - provider::{ - anthropic::AnthropicLanguageModelProvider, cloud::CloudLanguageModelProvider, - copilot_chat::CopilotChatLanguageModelProvider, google::GoogleLanguageModelProvider, - ollama::OllamaLanguageModelProvider, open_ai::OpenAiLanguageModelProvider, - }, LanguageModel, LanguageModelId, LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderState, }; -use client::{Client, UserStore}; use collections::BTreeMap; use gpui::{AppContext, EventEmitter, Global, Model, ModelContext}; use std::sync::Arc; use ui::Context; -pub fn init(user_store: Model, client: Arc, cx: &mut AppContext) { - let registry = cx.new_model(|cx| { - let mut registry = LanguageModelRegistry::default(); - register_language_model_providers(&mut registry, user_store, client, cx); - registry - }); +pub fn init(cx: &mut AppContext) { + let registry = cx.new_model(|_cx| LanguageModelRegistry::default()); cx.set_global(GlobalLanguageModelRegistry(registry)); } -fn register_language_model_providers( - registry: &mut LanguageModelRegistry, - user_store: Model, - client: Arc, - cx: &mut ModelContext, -) { - use feature_flags::FeatureFlagAppExt; - - RefreshLlmTokenListener::register(client.clone(), cx); - - registry.register_provider( - AnthropicLanguageModelProvider::new(client.http_client(), cx), - cx, - ); - registry.register_provider( - OpenAiLanguageModelProvider::new(client.http_client(), cx), - cx, - ); - registry.register_provider( - OllamaLanguageModelProvider::new(client.http_client(), cx), - cx, - ); - registry.register_provider( - GoogleLanguageModelProvider::new(client.http_client(), cx), - cx, - ); - registry.register_provider(CopilotChatLanguageModelProvider::new(cx), cx); - - cx.observe_flag::(move |enabled, cx| { - let user_store = user_store.clone(); - let client = client.clone(); - LanguageModelRegistry::global(cx).update(cx, move |registry, cx| { - if enabled { - registry.register_provider( - CloudLanguageModelProvider::new(user_store.clone(), client.clone(), cx), - cx, - ); - } else { - registry.unregister_provider( - LanguageModelProviderId::from(crate::provider::cloud::PROVIDER_ID.to_string()), - cx, - ); - } - }); - }) - .detach(); -} - struct GlobalLanguageModelRegistry(Model); impl Global for GlobalLanguageModelRegistry {} @@ -106,8 +47,8 @@ impl LanguageModelRegistry { } #[cfg(any(test, feature = "test-support"))] - pub fn test(cx: &mut AppContext) -> crate::provider::fake::FakeLanguageModelProvider { - let fake_provider = crate::provider::fake::FakeLanguageModelProvider; + pub fn test(cx: &mut AppContext) -> crate::fake_provider::FakeLanguageModelProvider { + let fake_provider = crate::fake_provider::FakeLanguageModelProvider; let registry = cx.new_model(|cx| { let mut registry = Self::default(); registry.register_provider(fake_provider.clone(), cx); @@ -148,7 +89,7 @@ impl LanguageModelRegistry { } pub fn providers(&self) -> Vec> { - let zed_provider_id = LanguageModelProviderId(crate::provider::cloud::PROVIDER_ID.into()); + let zed_provider_id = LanguageModelProviderId("zed.dev".into()); let mut providers = Vec::with_capacity(self.providers.len()); if let Some(provider) = self.providers.get(&zed_provider_id) { providers.push(provider.clone()); @@ -269,7 +210,7 @@ impl LanguageModelRegistry { #[cfg(test)] mod tests { use super::*; - use crate::provider::fake::FakeLanguageModelProvider; + use crate::fake_provider::FakeLanguageModelProvider; #[gpui::test] fn test_register_providers(cx: &mut AppContext) { @@ -281,10 +222,10 @@ mod tests { let providers = registry.read(cx).providers(); assert_eq!(providers.len(), 1); - assert_eq!(providers[0].id(), crate::provider::fake::provider_id()); + assert_eq!(providers[0].id(), crate::fake_provider::provider_id()); registry.update(cx, |registry, cx| { - registry.unregister_provider(crate::provider::fake::provider_id(), cx); + registry.unregister_provider(crate::fake_provider::provider_id(), cx); }); let providers = registry.read(cx).providers(); diff --git a/crates/language_models/Cargo.toml b/crates/language_models/Cargo.toml new file mode 100644 index 0000000000..00d948bd2d --- /dev/null +++ b/crates/language_models/Cargo.toml @@ -0,0 +1,49 @@ +[package] +name = "language_models" +version = "0.1.0" +edition = "2021" +publish = false +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/language_models.rs" + +[dependencies] +anthropic = { workspace = true, features = ["schemars"] } +anyhow.workspace = true +client.workspace = true +collections.workspace = true +copilot = { workspace = true, features = ["schemars"] } +editor.workspace = true +feature_flags.workspace = true +fs.workspace = true +futures.workspace = true +google_ai = { workspace = true, features = ["schemars"] } +gpui.workspace = true +http_client.workspace = true +language_model.workspace = true +menu.workspace = true +ollama = { workspace = true, features = ["schemars"] } +open_ai = { workspace = true, features = ["schemars"] } +project.workspace = true +proto.workspace = true +schemars.workspace = true +serde.workspace = true +serde_json.workspace = true +settings.workspace = true +smol.workspace = true +strum.workspace = true +telemetry_events.workspace = true +theme.workspace = true +thiserror.workspace = true +tiktoken-rs.workspace = true +ui.workspace = true +util.workspace = true + +[dev-dependencies] +editor = { workspace = true, features = ["test-support"] } +language_model = { workspace = true, features = ["test-support"] } +project = { workspace = true, features = ["test-support"] } diff --git a/crates/language_models/LICENSE-GPL b/crates/language_models/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/language_models/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/language_models/src/language_models.rs b/crates/language_models/src/language_models.rs new file mode 100644 index 0000000000..028ea0cfa4 --- /dev/null +++ b/crates/language_models/src/language_models.rs @@ -0,0 +1,80 @@ +use std::sync::Arc; + +use client::{Client, UserStore}; +use fs::Fs; +use gpui::{AppContext, Model, ModelContext}; +use language_model::{LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID}; + +mod logging; +pub mod provider; +mod settings; + +use crate::provider::anthropic::AnthropicLanguageModelProvider; +use crate::provider::cloud::{CloudLanguageModelProvider, RefreshLlmTokenListener}; +use crate::provider::copilot_chat::CopilotChatLanguageModelProvider; +use crate::provider::google::GoogleLanguageModelProvider; +use crate::provider::ollama::OllamaLanguageModelProvider; +use crate::provider::open_ai::OpenAiLanguageModelProvider; +pub use crate::settings::*; +pub use logging::report_assistant_event; + +pub fn init( + user_store: Model, + client: Arc, + fs: Arc, + cx: &mut AppContext, +) { + crate::settings::init(fs, cx); + let registry = LanguageModelRegistry::global(cx); + registry.update(cx, |registry, cx| { + register_language_model_providers(registry, user_store, client, cx); + }); +} + +fn register_language_model_providers( + registry: &mut LanguageModelRegistry, + user_store: Model, + client: Arc, + cx: &mut ModelContext, +) { + use feature_flags::FeatureFlagAppExt; + + RefreshLlmTokenListener::register(client.clone(), cx); + + registry.register_provider( + AnthropicLanguageModelProvider::new(client.http_client(), cx), + cx, + ); + registry.register_provider( + OpenAiLanguageModelProvider::new(client.http_client(), cx), + cx, + ); + registry.register_provider( + OllamaLanguageModelProvider::new(client.http_client(), cx), + cx, + ); + registry.register_provider( + GoogleLanguageModelProvider::new(client.http_client(), cx), + cx, + ); + registry.register_provider(CopilotChatLanguageModelProvider::new(cx), cx); + + cx.observe_flag::(move |enabled, cx| { + let user_store = user_store.clone(); + let client = client.clone(); + LanguageModelRegistry::global(cx).update(cx, move |registry, cx| { + if enabled { + registry.register_provider( + CloudLanguageModelProvider::new(user_store.clone(), client.clone(), cx), + cx, + ); + } else { + registry.unregister_provider( + LanguageModelProviderId::from(ZED_CLOUD_PROVIDER_ID.to_string()), + cx, + ); + } + }); + }) + .detach(); +} diff --git a/crates/language_model/src/logging.rs b/crates/language_models/src/logging.rs similarity index 100% rename from crates/language_model/src/logging.rs rename to crates/language_models/src/logging.rs diff --git a/crates/language_model/src/provider.rs b/crates/language_models/src/provider.rs similarity index 64% rename from crates/language_model/src/provider.rs rename to crates/language_models/src/provider.rs index d2d162b75e..fb79b12e4d 100644 --- a/crates/language_model/src/provider.rs +++ b/crates/language_models/src/provider.rs @@ -1,8 +1,6 @@ pub mod anthropic; pub mod cloud; pub mod copilot_chat; -#[cfg(any(test, feature = "test-support"))] -pub mod fake; pub mod google; pub mod ollama; pub mod open_ai; diff --git a/crates/language_model/src/provider/anthropic.rs b/crates/language_models/src/provider/anthropic.rs similarity index 98% rename from crates/language_model/src/provider/anthropic.rs rename to crates/language_models/src/provider/anthropic.rs index 60e238b369..87460b824e 100644 --- a/crates/language_model/src/provider/anthropic.rs +++ b/crates/language_models/src/provider/anthropic.rs @@ -1,9 +1,4 @@ -use crate::{ - settings::AllLanguageModelSettings, LanguageModel, LanguageModelCacheConfiguration, - LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId, - LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest, RateLimiter, Role, -}; -use crate::{LanguageModelCompletionEvent, LanguageModelToolUse, StopReason}; +use crate::AllLanguageModelSettings; use anthropic::{AnthropicError, ContentDelta, Event, ResponseContent}; use anyhow::{anyhow, Context as _, Result}; use collections::{BTreeMap, HashMap}; @@ -15,6 +10,12 @@ use gpui::{ View, WhiteSpace, }; use http_client::HttpClient; +use language_model::{ + LanguageModel, LanguageModelCacheConfiguration, LanguageModelId, LanguageModelName, + LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName, + LanguageModelProviderState, LanguageModelRequest, RateLimiter, Role, +}; +use language_model::{LanguageModelCompletionEvent, LanguageModelToolUse, StopReason}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsStore}; @@ -256,7 +257,7 @@ pub fn count_anthropic_tokens( let mut string_messages = Vec::with_capacity(messages.len()); for message in messages { - use crate::MessageContent; + use language_model::MessageContent; let mut string_contents = String::new(); diff --git a/crates/language_model/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs similarity index 98% rename from crates/language_model/src/provider/cloud.rs rename to crates/language_models/src/provider/cloud.rs index 41e23b56e3..f54e8c8d19 100644 --- a/crates/language_model/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -1,10 +1,4 @@ use super::open_ai::count_open_ai_tokens; -use crate::provider::anthropic::map_to_language_model_completion_events; -use crate::{ - settings::AllLanguageModelSettings, CloudModel, LanguageModel, LanguageModelCacheConfiguration, - LanguageModelId, LanguageModelName, LanguageModelProviderId, LanguageModelProviderName, - LanguageModelProviderState, LanguageModelRequest, RateLimiter, -}; use anthropic::AnthropicError; use anyhow::{anyhow, Result}; use client::{ @@ -22,6 +16,14 @@ use gpui::{ ModelContext, ReadGlobal, Subscription, Task, }; use http_client::{AsyncBody, HttpClient, Method, Response, StatusCode}; +use language_model::{ + CloudModel, LanguageModel, LanguageModelCacheConfiguration, LanguageModelId, LanguageModelName, + LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState, + LanguageModelRequest, RateLimiter, ZED_CLOUD_PROVIDER_ID, +}; +use language_model::{ + LanguageModelAvailability, LanguageModelCompletionEvent, LanguageModelProvider, +}; use proto::TypedEnvelope; use schemars::JsonSchema; use serde::{de::DeserializeOwned, Deserialize, Serialize}; @@ -40,11 +42,11 @@ use strum::IntoEnumIterator; use thiserror::Error; use ui::{prelude::*, TintColor}; -use crate::{LanguageModelAvailability, LanguageModelCompletionEvent, LanguageModelProvider}; +use crate::provider::anthropic::map_to_language_model_completion_events; +use crate::AllLanguageModelSettings; use super::anthropic::count_anthropic_tokens; -pub const PROVIDER_ID: &str = "zed.dev"; pub const PROVIDER_NAME: &str = "Zed"; const ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: Option<&str> = @@ -255,7 +257,7 @@ impl LanguageModelProviderState for CloudLanguageModelProvider { impl LanguageModelProvider for CloudLanguageModelProvider { fn id(&self) -> LanguageModelProviderId { - LanguageModelProviderId(PROVIDER_ID.into()) + LanguageModelProviderId(ZED_CLOUD_PROVIDER_ID.into()) } fn name(&self) -> LanguageModelProviderName { @@ -535,7 +537,7 @@ impl LanguageModel for CloudLanguageModel { } fn provider_id(&self) -> LanguageModelProviderId { - LanguageModelProviderId(PROVIDER_ID.into()) + LanguageModelProviderId(ZED_CLOUD_PROVIDER_ID.into()) } fn provider_name(&self) -> LanguageModelProviderName { diff --git a/crates/language_model/src/provider/copilot_chat.rs b/crates/language_models/src/provider/copilot_chat.rs similarity index 98% rename from crates/language_model/src/provider/copilot_chat.rs rename to crates/language_models/src/provider/copilot_chat.rs index 0eaeaa2e3d..5ae1ad56c5 100644 --- a/crates/language_model/src/provider/copilot_chat.rs +++ b/crates/language_models/src/provider/copilot_chat.rs @@ -14,6 +14,11 @@ use gpui::{ percentage, svg, Animation, AnimationExt, AnyView, AppContext, AsyncAppContext, Model, Render, Subscription, Task, Transformation, }; +use language_model::{ + LanguageModel, LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, + LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName, + LanguageModelProviderState, LanguageModelRequest, RateLimiter, Role, +}; use settings::SettingsStore; use std::time::Duration; use strum::IntoEnumIterator; @@ -23,12 +28,6 @@ use ui::{ ViewContext, VisualContext, WindowContext, }; -use crate::{ - LanguageModel, LanguageModelId, LanguageModelName, LanguageModelProvider, - LanguageModelProviderId, LanguageModelProviderName, LanguageModelRequest, RateLimiter, Role, -}; -use crate::{LanguageModelCompletionEvent, LanguageModelProviderState}; - use super::anthropic::count_anthropic_tokens; use super::open_ai::count_open_ai_tokens; diff --git a/crates/language_model/src/provider/google.rs b/crates/language_models/src/provider/google.rs similarity index 98% rename from crates/language_model/src/provider/google.rs rename to crates/language_models/src/provider/google.rs index 94d5ffca7d..59589605ee 100644 --- a/crates/language_model/src/provider/google.rs +++ b/crates/language_models/src/provider/google.rs @@ -8,6 +8,12 @@ use gpui::{ View, WhiteSpace, }; use http_client::HttpClient; +use language_model::LanguageModelCompletionEvent; +use language_model::{ + LanguageModel, LanguageModelId, LanguageModelName, LanguageModelProvider, + LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState, + LanguageModelRequest, RateLimiter, +}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsStore}; @@ -17,12 +23,7 @@ use theme::ThemeSettings; use ui::{prelude::*, Icon, IconName, Tooltip}; use util::ResultExt; -use crate::LanguageModelCompletionEvent; -use crate::{ - settings::AllLanguageModelSettings, LanguageModel, LanguageModelId, LanguageModelName, - LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName, - LanguageModelProviderState, LanguageModelRequest, RateLimiter, -}; +use crate::AllLanguageModelSettings; const PROVIDER_ID: &str = "google"; const PROVIDER_NAME: &str = "Google AI"; diff --git a/crates/language_model/src/provider/ollama.rs b/crates/language_models/src/provider/ollama.rs similarity index 98% rename from crates/language_model/src/provider/ollama.rs rename to crates/language_models/src/provider/ollama.rs index 3485982781..4fef43afe0 100644 --- a/crates/language_model/src/provider/ollama.rs +++ b/crates/language_models/src/provider/ollama.rs @@ -2,6 +2,12 @@ use anyhow::{anyhow, bail, Result}; use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt}; use gpui::{AnyView, AppContext, AsyncAppContext, ModelContext, Subscription, Task}; use http_client::HttpClient; +use language_model::LanguageModelCompletionEvent; +use language_model::{ + LanguageModel, LanguageModelId, LanguageModelName, LanguageModelProvider, + LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState, + LanguageModelRequest, RateLimiter, Role, +}; use ollama::{ get_models, preload_model, stream_chat_completion, ChatMessage, ChatOptions, ChatRequest, ChatResponseDelta, KeepAlive, OllamaToolCall, @@ -13,12 +19,7 @@ use std::{collections::BTreeMap, sync::Arc}; use ui::{prelude::*, ButtonLike, Indicator}; use util::ResultExt; -use crate::LanguageModelCompletionEvent; -use crate::{ - settings::AllLanguageModelSettings, LanguageModel, LanguageModelId, LanguageModelName, - LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName, - LanguageModelProviderState, LanguageModelRequest, RateLimiter, Role, -}; +use crate::AllLanguageModelSettings; const OLLAMA_DOWNLOAD_URL: &str = "https://ollama.com/download"; const OLLAMA_LIBRARY_URL: &str = "https://ollama.com/library"; diff --git a/crates/language_model/src/provider/open_ai.rs b/crates/language_models/src/provider/open_ai.rs similarity index 99% rename from crates/language_model/src/provider/open_ai.rs rename to crates/language_models/src/provider/open_ai.rs index 2a51b9a648..5c740f93e6 100644 --- a/crates/language_model/src/provider/open_ai.rs +++ b/crates/language_models/src/provider/open_ai.rs @@ -7,6 +7,11 @@ use gpui::{ View, WhiteSpace, }; use http_client::HttpClient; +use language_model::{ + LanguageModel, LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, + LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName, + LanguageModelProviderState, LanguageModelRequest, RateLimiter, Role, +}; use open_ai::{ stream_completion, FunctionDefinition, ResponseStreamEvent, ToolChoice, ToolDefinition, }; @@ -19,12 +24,7 @@ use theme::ThemeSettings; use ui::{prelude::*, Icon, IconName, Tooltip}; use util::ResultExt; -use crate::LanguageModelCompletionEvent; -use crate::{ - settings::AllLanguageModelSettings, LanguageModel, LanguageModelId, LanguageModelName, - LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName, - LanguageModelProviderState, LanguageModelRequest, RateLimiter, Role, -}; +use crate::AllLanguageModelSettings; const PROVIDER_ID: &str = "openai"; const PROVIDER_NAME: &str = "OpenAI"; diff --git a/crates/language_model/src/settings.rs b/crates/language_models/src/settings.rs similarity index 97% rename from crates/language_model/src/settings.rs rename to crates/language_models/src/settings.rs index 275fcf0417..f6602427cb 100644 --- a/crates/language_model/src/settings.rs +++ b/crates/language_models/src/settings.rs @@ -2,22 +2,20 @@ use std::sync::Arc; use anyhow::Result; use gpui::AppContext; +use language_model::LanguageModelCacheConfiguration; use project::Fs; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{update_settings_file, Settings, SettingsSources}; -use crate::{ - provider::{ - self, - anthropic::AnthropicSettings, - cloud::{self, ZedDotDevSettings}, - copilot_chat::CopilotChatSettings, - google::GoogleSettings, - ollama::OllamaSettings, - open_ai::OpenAiSettings, - }, - LanguageModelCacheConfiguration, +use crate::provider::{ + self, + anthropic::AnthropicSettings, + cloud::{self, ZedDotDevSettings}, + copilot_chat::CopilotChatSettings, + google::GoogleSettings, + ollama::OllamaSettings, + open_ai::OpenAiSettings, }; /// Initializes the language model settings. diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 6f511c2951..8d12d7b9f9 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -61,6 +61,7 @@ install_cli.workspace = true journal.workspace = true language.workspace = true language_model.workspace = true +language_models.workspace = true language_selector.workspace = true language_tools.workspace = true languages = { workspace = true, features = ["load-grammars"] } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index c632843baa..9dbe00c617 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -387,7 +387,8 @@ fn main() { cx, ); supermaven::init(app_state.client.clone(), cx); - language_model::init( + language_model::init(cx); + language_models::init( app_state.user_store.clone(), app_state.client.clone(), app_state.fs.clone(), diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 867ffa91e6..73ecd00192 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -3504,7 +3504,8 @@ mod tests { app_state.client.http_client().clone(), cx, ); - language_model::init( + language_model::init(cx); + language_models::init( app_state.user_store.clone(), app_state.client.clone(), app_state.fs.clone(), From 536d7e53553d339be6a964dba2af3db8586411cd Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 21 Nov 2024 01:07:14 +0100 Subject: [PATCH 24/68] chore: Sever terminal_view <-> tasks_ui dependency (#20946) Closes #ISSUE Release Notes: - N/A --- Cargo.lock | 2 +- crates/tasks_ui/Cargo.toml | 2 +- crates/tasks_ui/src/lib.rs | 11 +++-- crates/tasks_ui/src/modal.rs | 47 ++-------------------- crates/terminal_view/Cargo.toml | 1 - crates/terminal_view/src/terminal_panel.rs | 2 +- crates/terminal_view/src/terminal_view.rs | 4 +- crates/zed_actions/src/lib.rs | 40 ++++++++++++++++++ 8 files changed, 56 insertions(+), 53 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a8ff3abe01..f7587449b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12196,6 +12196,7 @@ dependencies = [ "ui", "util", "workspace", + "zed_actions", ] [[package]] @@ -12299,7 +12300,6 @@ dependencies = [ "shellexpand 2.1.2", "smol", "task", - "tasks_ui", "terminal", "theme", "ui", diff --git a/crates/tasks_ui/Cargo.toml b/crates/tasks_ui/Cargo.toml index 265755319b..528d238329 100644 --- a/crates/tasks_ui/Cargo.toml +++ b/crates/tasks_ui/Cargo.toml @@ -25,7 +25,7 @@ ui.workspace = true util.workspace = true workspace.workspace = true language.workspace = true - +zed_actions.workspace = true [dev-dependencies] editor = { workspace = true, features = ["test-support"] } diff --git a/crates/tasks_ui/src/lib.rs b/crates/tasks_ui/src/lib.rs index 38b15403e2..02ced4f479 100644 --- a/crates/tasks_ui/src/lib.rs +++ b/crates/tasks_ui/src/lib.rs @@ -3,6 +3,7 @@ use editor::{tasks::task_context, Editor}; use gpui::{AppContext, Task as AsyncTask, ViewContext, WindowContext}; use modal::TasksModal; use project::{Location, WorktreeId}; +use task::TaskId; use workspace::tasks::schedule_task; use workspace::{tasks::schedule_resolved_task, Workspace}; @@ -25,9 +26,13 @@ pub fn init(cx: &mut AppContext) { .read(cx) .task_inventory() .and_then(|inventory| { - inventory - .read(cx) - .last_scheduled_task(action.task_id.as_ref()) + inventory.read(cx).last_scheduled_task( + action + .task_id + .as_ref() + .map(|id| TaskId(id.clone())) + .as_ref(), + ) }) { if action.reevaluate_context { diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index 3de116702a..3c7b767d5c 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -3,13 +3,13 @@ use std::sync::Arc; use crate::active_item_selection_properties; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - impl_actions, rems, Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusableView, + rems, Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusableView, InteractiveElement, Model, ParentElement, Render, SharedString, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, }; use picker::{highlighted_match_with_paths::HighlightedText, Picker, PickerDelegate}; use project::{task_store::TaskStore, TaskSourceKind}; -use task::{ResolvedTask, TaskContext, TaskId, TaskTemplate}; +use task::{ResolvedTask, TaskContext, TaskTemplate}; use ui::{ div, h_flex, v_flex, ActiveTheme, Button, ButtonCommon, ButtonSize, Clickable, Color, FluentBuilder as _, Icon, IconButton, IconButtonShape, IconName, IconSize, IntoElement, @@ -18,48 +18,7 @@ use ui::{ }; use util::ResultExt; use workspace::{tasks::schedule_resolved_task, ModalView, Workspace}; - -use serde::Deserialize; - -/// Spawn a task with name or open tasks modal -#[derive(PartialEq, Clone, Deserialize, Default)] -pub struct Spawn { - #[serde(default)] - /// Name of the task to spawn. - /// If it is not set, a modal with a list of available tasks is opened instead. - /// Defaults to None. - pub task_name: Option, -} - -impl Spawn { - pub fn modal() -> Self { - Self { task_name: None } - } -} - -/// Rerun last task -#[derive(PartialEq, Clone, Deserialize, Default)] -pub struct Rerun { - /// Controls whether the task context is reevaluated prior to execution of a task. - /// If it is not, environment variables such as ZED_COLUMN, ZED_FILE are gonna be the same as in the last execution of a task - /// If it is, these variables will be updated to reflect current state of editor at the time task::Rerun is executed. - /// default: false - #[serde(default)] - pub reevaluate_context: bool, - /// Overrides `allow_concurrent_runs` property of the task being reran. - /// Default: null - #[serde(default)] - pub allow_concurrent_runs: Option, - /// Overrides `use_new_terminal` property of the task being reran. - /// Default: null - #[serde(default)] - pub use_new_terminal: Option, - - /// If present, rerun the task with this ID, otherwise rerun the last task. - pub task_id: Option, -} - -impl_actions!(task, [Rerun, Spawn]); +pub use zed_actions::{Rerun, Spawn}; /// A modal used to spawn new tasks. pub(crate) struct TasksModalDelegate { diff --git a/crates/terminal_view/Cargo.toml b/crates/terminal_view/Cargo.toml index 09b0b0d2d5..64b979cdd6 100644 --- a/crates/terminal_view/Cargo.toml +++ b/crates/terminal_view/Cargo.toml @@ -24,7 +24,6 @@ itertools.workspace = true language.workspace = true project.workspace = true task.workspace = true -tasks_ui.workspace = true search.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 6d64ac1a48..2ca7561bdb 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -218,7 +218,7 @@ impl TerminalPanel { // context menu will be gone the moment we spawn the modal. .action( "Spawn task", - tasks_ui::Spawn::modal().boxed_clone(), + zed_actions::Spawn::modal().boxed_clone(), ) }); diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 6a23e45f54..21d20599b9 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -1044,8 +1044,8 @@ impl Item for TerminalView { .shape(ui::IconButtonShape::Square) .tooltip(|cx| Tooltip::text("Rerun task", cx)) .on_click(move |_, cx| { - cx.dispatch_action(Box::new(tasks_ui::Rerun { - task_id: Some(task_id.clone()), + cx.dispatch_action(Box::new(zed_actions::Rerun { + task_id: Some(task_id.0.clone()), allow_concurrent_runs: Some(true), use_new_terminal: Some(false), reevaluate_context: false, diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index 2f33583429..53f5b202a8 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -58,3 +58,43 @@ pub struct OpenRecent { } gpui::impl_actions!(projects, [OpenRecent]); gpui::actions!(projects, [OpenRemote]); + +/// Spawn a task with name or open tasks modal +#[derive(PartialEq, Clone, Deserialize, Default)] +pub struct Spawn { + #[serde(default)] + /// Name of the task to spawn. + /// If it is not set, a modal with a list of available tasks is opened instead. + /// Defaults to None. + pub task_name: Option, +} + +impl Spawn { + pub fn modal() -> Self { + Self { task_name: None } + } +} + +/// Rerun last task +#[derive(PartialEq, Clone, Deserialize, Default)] +pub struct Rerun { + /// Controls whether the task context is reevaluated prior to execution of a task. + /// If it is not, environment variables such as ZED_COLUMN, ZED_FILE are gonna be the same as in the last execution of a task + /// If it is, these variables will be updated to reflect current state of editor at the time task::Rerun is executed. + /// default: false + #[serde(default)] + pub reevaluate_context: bool, + /// Overrides `allow_concurrent_runs` property of the task being reran. + /// Default: null + #[serde(default)] + pub allow_concurrent_runs: Option, + /// Overrides `use_new_terminal` property of the task being reran. + /// Default: null + #[serde(default)] + pub use_new_terminal: Option, + + /// If present, rerun the task with this ID, otherwise rerun the last task. + pub task_id: Option, +} + +impl_actions!(task, [Spawn, Rerun]); From 33e84da657f279de1eb549eb351a5e026b608c3d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 20 Nov 2024 19:39:49 -0500 Subject: [PATCH 25/68] Update Rust crate cargo_metadata to 0.19 (#20948) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [cargo_metadata](https://redirect.github.com/oli-obk/cargo_metadata) | workspace.dependencies | minor | `0.18` -> `0.19` | --- ### Release Notes
oli-obk/cargo_metadata (cargo_metadata) ### [`v0.19.0`](https://redirect.github.com/oli-obk/cargo_metadata/blob/HEAD/CHANGELOG.md#0190---2024-11-20) [Compare Source](https://redirect.github.com/oli-obk/cargo_metadata/compare/0.18.1...0.19.0) ##### Added - Re-exported `semver` crate directly. - Added implementation of `std::ops::Index<&PackageId>` for `Resolve`. - Added `pub fn is_kind(&self, name: TargetKind) -> bool` to `Target`. - Added derived implementations of `PartialEq`, `Eq` and `Hash` for `Metadata` and its members' types. - Added default fields to `PackageBuilder`. - Added `pub fn new(name:version:id:path:) -> Self` to `PackageBuilder` for providing all required fields upfront. ##### Changed - Bumped MSRV from `1.42.0` to `1.56.0`. - Made `parse_stream` more versatile by accepting anything that implements `Read`. - Converted `TargetKind` and `CrateType` to an enum representation. ##### Removed - Removed re-exports for `BuildMetadata` and `Prerelease` from `semver` crate. - Removed `.is_lib(…)`, `.is_bin(…)`, `.is_example(…)`, `.is_test(…)`, `.is_bench(…)`, `.is_custom_build(…)`, and `.is_proc_macro(…)` from `Target` (in favor of adding `.is_kind(…)`). ##### Fixed - Added missing `manifest_path` field to `Artifact`. Fixes [#​187](https://redirect.github.com/oli-obk/cargo_metadata/issues/187).
--- ### Configuration 📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone America/New_York, Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- Release Notes: - N/A Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f7587449b1..f73dd6b8c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2108,9 +2108,9 @@ dependencies = [ [[package]] name = "cargo_metadata" -version = "0.18.1" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037" +checksum = "afc309ed89476c8957c50fb818f56fe894db857866c3e163335faa91dc34eb85" dependencies = [ "camino", "cargo-platform", diff --git a/Cargo.toml b/Cargo.toml index 8357160268..a5555864d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -336,7 +336,7 @@ blade-macros = { git = "https://github.com/kvark/blade", rev = "e142a3a5e678eb6a blade-util = { git = "https://github.com/kvark/blade", rev = "e142a3a5e678eb6a13e642ad8401b1f3aa38e969" } blake3 = "1.5.3" bytes = "1.0" -cargo_metadata = "0.18" +cargo_metadata = "0.19" cargo_toml = "0.20" chrono = { version = "0.4", features = ["serde"] } clap = { version = "4.4", features = ["derive"] } From 49ed932c1f2cd5b293b35fbdbc279550cc542ffb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=B0=8F=E7=99=BD?= <364772080@qq.com> Date: Thu, 21 Nov 2024 08:47:55 +0800 Subject: [PATCH 26/68] Fix line truncate crash on Windows (#17271) Closes #17267 We should update the `len` of `runs` when truncating. cc @huacnlee Release Notes: - N/A --- crates/gpui/src/elements/text.rs | 4 +- crates/gpui/src/text_system/line_wrapper.rs | 197 +++++++++++++++++--- 2 files changed, 174 insertions(+), 27 deletions(-) diff --git a/crates/gpui/src/elements/text.rs b/crates/gpui/src/elements/text.rs index 56b551737a..427097d1b7 100644 --- a/crates/gpui/src/elements/text.rs +++ b/crates/gpui/src/elements/text.rs @@ -263,7 +263,7 @@ impl TextLayout { .line_height .to_pixels(font_size.into(), cx.rem_size()); - let runs = if let Some(runs) = runs { + let mut runs = if let Some(runs) = runs { runs } else { vec![text_style.to_run(text.len())] @@ -306,7 +306,7 @@ impl TextLayout { let mut line_wrapper = cx.text_system().line_wrapper(text_style.font(), font_size); let text = if let Some(truncate_width) = truncate_width { - line_wrapper.truncate_line(text.clone(), truncate_width, ellipsis) + line_wrapper.truncate_line(text.clone(), truncate_width, ellipsis, &mut runs) } else { text.clone() }; diff --git a/crates/gpui/src/text_system/line_wrapper.rs b/crates/gpui/src/text_system/line_wrapper.rs index 3d38ca315c..1b99165eee 100644 --- a/crates/gpui/src/text_system/line_wrapper.rs +++ b/crates/gpui/src/text_system/line_wrapper.rs @@ -1,4 +1,4 @@ -use crate::{px, FontId, FontRun, Pixels, PlatformTextSystem, SharedString}; +use crate::{px, FontId, FontRun, Pixels, PlatformTextSystem, SharedString, TextRun}; use collections::HashMap; use std::{iter, sync::Arc}; @@ -104,6 +104,7 @@ impl LineWrapper { line: SharedString, truncate_width: Pixels, ellipsis: Option<&str>, + runs: &mut Vec, ) -> SharedString { let mut width = px(0.); let mut ellipsis_width = px(0.); @@ -124,15 +125,15 @@ impl LineWrapper { width += char_width; if width.floor() > truncate_width { - return SharedString::from(format!( - "{}{}", - &line[..truncate_ix], - ellipsis.unwrap_or("") - )); + let ellipsis = ellipsis.unwrap_or(""); + let result = SharedString::from(format!("{}{}", &line[..truncate_ix], ellipsis)); + update_runs_after_truncation(&result, ellipsis, runs); + + return result; } } - line.clone() + line } pub(crate) fn is_word_char(c: char) -> bool { @@ -195,6 +196,23 @@ impl LineWrapper { } } +fn update_runs_after_truncation(result: &str, ellipsis: &str, runs: &mut Vec) { + let mut truncate_at = result.len() - ellipsis.len(); + let mut run_end = None; + for (run_index, run) in runs.iter_mut().enumerate() { + if run.len <= truncate_at { + truncate_at -= run.len; + } else { + run.len = truncate_at + ellipsis.len(); + run_end = Some(run_index + 1); + break; + } + } + if let Some(run_end) = run_end { + runs.truncate(run_end); + } +} + /// A boundary between two lines of text. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct Boundary { @@ -213,7 +231,9 @@ impl Boundary { #[cfg(test)] mod tests { use super::*; - use crate::{font, TestAppContext, TestDispatcher}; + use crate::{ + font, Font, FontFeatures, FontStyle, FontWeight, Hsla, TestAppContext, TestDispatcher, + }; #[cfg(target_os = "macos")] use crate::{TextRun, WindowTextSystem, WrapBoundary}; use rand::prelude::*; @@ -232,6 +252,26 @@ mod tests { LineWrapper::new(id, px(16.), cx.text_system().platform_text_system.clone()) } + fn generate_test_runs(input_run_len: &[usize]) -> Vec { + input_run_len + .iter() + .map(|run_len| TextRun { + len: *run_len, + font: Font { + family: "Dummy".into(), + features: FontFeatures::default(), + fallbacks: None, + weight: FontWeight::default(), + style: FontStyle::Normal, + }, + color: Hsla::default(), + background_color: None, + underline: None, + strikethrough: None, + }) + .collect() + } + #[test] fn test_wrap_line() { let mut wrapper = build_wrapper(); @@ -293,28 +333,135 @@ mod tests { fn test_truncate_line() { let mut wrapper = build_wrapper(); - assert_eq!( - wrapper.truncate_line("aa bbb cccc ddddd eeee ffff gggg".into(), px(220.), None), - "aa bbb cccc ddddd eeee" + fn perform_test( + wrapper: &mut LineWrapper, + text: &'static str, + result: &'static str, + ellipsis: Option<&str>, + ) { + let dummy_run_lens = vec![text.len()]; + let mut dummy_runs = generate_test_runs(&dummy_run_lens); + assert_eq!( + wrapper.truncate_line(text.into(), px(220.), ellipsis, &mut dummy_runs), + result + ); + assert_eq!(dummy_runs.first().unwrap().len, result.len()); + } + + perform_test( + &mut wrapper, + "aa bbb cccc ddddd eeee ffff gggg", + "aa bbb cccc ddddd eeee", + None, ); - assert_eq!( - wrapper.truncate_line( - "aa bbb cccc ddddd eeee ffff gggg".into(), - px(220.), - Some("…") - ), - "aa bbb cccc ddddd eee…" + perform_test( + &mut wrapper, + "aa bbb cccc ddddd eeee ffff gggg", + "aa bbb cccc ddddd eee…", + Some("…"), ); - assert_eq!( - wrapper.truncate_line( - "aa bbb cccc ddddd eeee ffff gggg".into(), - px(220.), - Some("......") - ), - "aa bbb cccc dddd......" + perform_test( + &mut wrapper, + "aa bbb cccc ddddd eeee ffff gggg", + "aa bbb cccc dddd......", + Some("......"), ); } + #[test] + fn test_truncate_multiple_runs() { + let mut wrapper = build_wrapper(); + + fn perform_test( + wrapper: &mut LineWrapper, + text: &'static str, + result: &str, + run_lens: &[usize], + result_run_len: &[usize], + line_width: Pixels, + ) { + let mut dummy_runs = generate_test_runs(run_lens); + assert_eq!( + wrapper.truncate_line(text.into(), line_width, Some("…"), &mut dummy_runs), + result + ); + for (run, result_len) in dummy_runs.iter().zip(result_run_len) { + assert_eq!(run.len, *result_len); + } + } + // Case 0: Normal + // Text: abcdefghijkl + // Runs: Run0 { len: 12, ... } + // + // Truncate res: abcd… (truncate_at = 4) + // Run res: Run0 { string: abcd…, len: 7, ... } + perform_test(&mut wrapper, "abcdefghijkl", "abcd…", &[12], &[7], px(50.)); + // Case 1: Drop some runs + // Text: abcdefghijkl + // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... } + // + // Truncate res: abcdef… (truncate_at = 6) + // Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: ef…, len: + // 5, ... } + perform_test( + &mut wrapper, + "abcdefghijkl", + "abcdef…", + &[4, 4, 4], + &[4, 5], + px(70.), + ); + // Case 2: Truncate at start of some run + // Text: abcdefghijkl + // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... } + // + // Truncate res: abcdefgh… (truncate_at = 8) + // Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: efgh, len: + // 4, ... }, Run2 { string: …, len: 3, ... } + perform_test( + &mut wrapper, + "abcdefghijkl", + "abcdefgh…", + &[4, 4, 4], + &[4, 4, 3], + px(90.), + ); + } + + #[test] + fn test_update_run_after_truncation() { + fn perform_test(result: &str, run_lens: &[usize], result_run_lens: &[usize]) { + let mut dummy_runs = generate_test_runs(run_lens); + update_runs_after_truncation(result, "…", &mut dummy_runs); + for (run, result_len) in dummy_runs.iter().zip(result_run_lens) { + assert_eq!(run.len, *result_len); + } + } + // Case 0: Normal + // Text: abcdefghijkl + // Runs: Run0 { len: 12, ... } + // + // Truncate res: abcd… (truncate_at = 4) + // Run res: Run0 { string: abcd…, len: 7, ... } + perform_test("abcd…", &[12], &[7]); + // Case 1: Drop some runs + // Text: abcdefghijkl + // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... } + // + // Truncate res: abcdef… (truncate_at = 6) + // Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: ef…, len: + // 5, ... } + perform_test("abcdef…", &[4, 4, 4], &[4, 5]); + // Case 2: Truncate at start of some run + // Text: abcdefghijkl + // Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... } + // + // Truncate res: abcdefgh… (truncate_at = 8) + // Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: efgh, len: + // 4, ... }, Run2 { string: …, len: 3, ... } + perform_test("abcdefgh…", &[4, 4, 4], &[4, 4, 3]); + } + #[test] fn test_is_word_char() { #[track_caller] From 95ace0370672b4784821648887d57a95bf974291 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=B0=8F=E7=99=BD?= <364772080@qq.com> Date: Thu, 21 Nov 2024 08:52:38 +0800 Subject: [PATCH 27/68] windows: Set `CREATE_NO_WINDOW` for commands (#18447) - Closes: #18371 Release Notes: - N/A --- Cargo.lock | 9 ++---- crates/context_servers/src/client.rs | 4 +-- crates/evals/Cargo.toml | 3 +- crates/evals/src/eval.rs | 10 +++--- crates/extension/Cargo.toml | 1 + crates/extension/src/extension_builder.rs | 20 ++++++------ crates/git/Cargo.toml | 4 --- crates/git/src/blame.rs | 16 ++-------- crates/git/src/commit.rs | 15 ++------- crates/git/src/status.rs | 16 ++-------- crates/gpui/src/platform/windows/platform.rs | 2 +- crates/languages/Cargo.toml | 2 +- crates/languages/src/c.rs | 2 +- crates/languages/src/go.rs | 8 ++--- crates/languages/src/python.rs | 10 +++--- crates/languages/src/rust.rs | 8 ++--- crates/lsp/Cargo.toml | 3 -- crates/lsp/src/lsp.rs | 25 ++++++--------- crates/node_runtime/Cargo.toml | 1 - crates/node_runtime/src/node_runtime.rs | 31 ++++++++----------- crates/project/Cargo.toml | 3 -- crates/project/src/lsp_store.rs | 11 ++----- crates/remote/src/ssh_session.rs | 8 ++--- crates/repl/Cargo.toml | 3 -- crates/repl/src/kernels/mod.rs | 3 +- crates/repl/src/kernels/native_kernel.rs | 24 ++++----------- crates/supermaven/Cargo.toml | 3 -- crates/supermaven/src/supermaven.rs | 17 +++-------- crates/util/Cargo.toml | 1 + crates/util/src/command.rs | 32 ++++++++++++++++++++ crates/util/src/util.rs | 1 + 31 files changed, 122 insertions(+), 174 deletions(-) create mode 100644 crates/util/src/command.rs diff --git a/Cargo.lock b/Cargo.lock index f73dd6b8c5..8db4b8424d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4030,6 +4030,7 @@ dependencies = [ "serde_json", "settings", "smol", + "util", ] [[package]] @@ -4115,6 +4116,7 @@ dependencies = [ "serde", "serde_json", "toml 0.8.19", + "util", "wasm-encoder 0.215.0", "wasmparser 0.215.0", "wit-component", @@ -4936,7 +4938,6 @@ dependencies = [ "unindent", "url", "util", - "windows 0.58.0", ] [[package]] @@ -6951,7 +6952,6 @@ dependencies = [ "serde_json", "smol", "util", - "windows 0.58.0", ] [[package]] @@ -7490,7 +7490,6 @@ dependencies = [ "util", "walkdir", "which 6.0.3", - "windows 0.58.0", ] [[package]] @@ -9179,7 +9178,6 @@ dependencies = [ "url", "util", "which 6.0.3", - "windows 0.58.0", "worktree", ] @@ -9941,7 +9939,6 @@ dependencies = [ "ui", "util", "uuid", - "windows 0.58.0", "workspace", ] @@ -11829,7 +11826,6 @@ dependencies = [ "ui", "unicode-segmentation", "util", - "windows 0.58.0", ] [[package]] @@ -13548,6 +13544,7 @@ dependencies = [ "rust-embed", "serde", "serde_json", + "smol", "take-until", "tempfile", "tendril", diff --git a/crates/context_servers/src/client.rs b/crates/context_servers/src/client.rs index 8202e950d6..64aabb00e8 100644 --- a/crates/context_servers/src/client.rs +++ b/crates/context_servers/src/client.rs @@ -9,7 +9,7 @@ use serde_json::{value::RawValue, Value}; use smol::{ channel, io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, - process::{self, Child}, + process::Child, }; use std::{ fmt, @@ -152,7 +152,7 @@ impl Client { &binary.args ); - let mut command = process::Command::new(&binary.executable); + let mut command = util::command::new_smol_command(&binary.executable); command .args(&binary.args) .envs(binary.env.unwrap_or_default()) diff --git a/crates/evals/Cargo.toml b/crates/evals/Cargo.toml index 3057edcd1a..744094aeaf 100644 --- a/crates/evals/Cargo.toml +++ b/crates/evals/Cargo.toml @@ -30,9 +30,10 @@ languages.workspace = true node_runtime.workspace = true open_ai.workspace = true project.workspace = true +reqwest_client.workspace = true semantic_index.workspace = true serde.workspace = true serde_json.workspace = true settings.workspace = true smol.workspace = true -reqwest_client.workspace = true +util.workspace = true diff --git a/crates/evals/src/eval.rs b/crates/evals/src/eval.rs index 2db13ff392..67b73a835b 100644 --- a/crates/evals/src/eval.rs +++ b/crates/evals/src/eval.rs @@ -27,7 +27,7 @@ use std::time::Duration; use std::{ fs, path::Path, - process::{exit, Command, Stdio}, + process::{exit, Stdio}, sync::{ atomic::{AtomicUsize, Ordering::SeqCst}, Arc, @@ -667,7 +667,7 @@ async fn fetch_eval_repo( return; } if !repo_dir.join(".git").exists() { - let init_output = Command::new("git") + let init_output = util::command::new_std_command("git") .current_dir(&repo_dir) .args(&["init"]) .output() @@ -682,13 +682,13 @@ async fn fetch_eval_repo( } } let url = format!("https://github.com/{}.git", repo); - Command::new("git") + util::command::new_std_command("git") .current_dir(&repo_dir) .args(&["remote", "add", "-f", "origin", &url]) .stdin(Stdio::null()) .output() .unwrap(); - let fetch_output = Command::new("git") + let fetch_output = util::command::new_std_command("git") .current_dir(&repo_dir) .args(&["fetch", "--depth", "1", "origin", &sha]) .stdin(Stdio::null()) @@ -703,7 +703,7 @@ async fn fetch_eval_repo( ); return; } - let checkout_output = Command::new("git") + let checkout_output = util::command::new_std_command("git") .current_dir(&repo_dir) .args(&["checkout", &sha]) .output() diff --git a/crates/extension/Cargo.toml b/crates/extension/Cargo.toml index b4d23fd709..a96cf7155a 100644 --- a/crates/extension/Cargo.toml +++ b/crates/extension/Cargo.toml @@ -28,6 +28,7 @@ semantic_version.workspace = true serde.workspace = true serde_json.workspace = true toml.workspace = true +util.workspace = true wasm-encoder.workspace = true wasmparser.workspace = true wit-component.workspace = true diff --git a/crates/extension/src/extension_builder.rs b/crates/extension/src/extension_builder.rs index 25e6a1a485..a2d7ae573f 100644 --- a/crates/extension/src/extension_builder.rs +++ b/crates/extension/src/extension_builder.rs @@ -11,7 +11,7 @@ use serde::Deserialize; use std::{ env, fs, mem, path::{Path, PathBuf}, - process::{Command, Stdio}, + process::Stdio, sync::Arc, }; use wasm_encoder::{ComponentSectionId, Encode as _, RawSection, Section as _}; @@ -130,7 +130,7 @@ impl ExtensionBuilder { "compiling Rust crate for extension {}", extension_dir.display() ); - let output = Command::new("cargo") + let output = util::command::new_std_command("cargo") .args(["build", "--target", RUST_TARGET]) .args(options.release.then_some("--release")) .arg("--target-dir") @@ -237,7 +237,7 @@ impl ExtensionBuilder { let scanner_path = src_path.join("scanner.c"); log::info!("compiling {grammar_name} parser"); - let clang_output = Command::new(&clang_path) + let clang_output = util::command::new_std_command(&clang_path) .args(["-fPIC", "-shared", "-Os"]) .arg(format!("-Wl,--export=tree_sitter_{grammar_name}")) .arg("-o") @@ -264,7 +264,7 @@ impl ExtensionBuilder { let git_dir = directory.join(".git"); if directory.exists() { - let remotes_output = Command::new("git") + let remotes_output = util::command::new_std_command("git") .arg("--git-dir") .arg(&git_dir) .args(["remote", "-v"]) @@ -287,7 +287,7 @@ impl ExtensionBuilder { fs::create_dir_all(directory).with_context(|| { format!("failed to create grammar directory {}", directory.display(),) })?; - let init_output = Command::new("git") + let init_output = util::command::new_std_command("git") .arg("init") .current_dir(directory) .output()?; @@ -298,7 +298,7 @@ impl ExtensionBuilder { ); } - let remote_add_output = Command::new("git") + let remote_add_output = util::command::new_std_command("git") .arg("--git-dir") .arg(&git_dir) .args(["remote", "add", "origin", url]) @@ -312,14 +312,14 @@ impl ExtensionBuilder { } } - let fetch_output = Command::new("git") + let fetch_output = util::command::new_std_command("git") .arg("--git-dir") .arg(&git_dir) .args(["fetch", "--depth", "1", "origin", rev]) .output() .context("failed to execute `git fetch`")?; - let checkout_output = Command::new("git") + let checkout_output = util::command::new_std_command("git") .arg("--git-dir") .arg(&git_dir) .args(["checkout", rev]) @@ -346,7 +346,7 @@ impl ExtensionBuilder { } fn install_rust_wasm_target_if_needed(&self) -> Result<()> { - let rustc_output = Command::new("rustc") + let rustc_output = util::command::new_std_command("rustc") .arg("--print") .arg("sysroot") .output() @@ -363,7 +363,7 @@ impl ExtensionBuilder { return Ok(()); } - let output = Command::new("rustup") + let output = util::command::new_std_command("rustup") .args(["target", "add", RUST_TARGET]) .stderr(Stdio::piped()) .stdout(Stdio::inherit()) diff --git a/crates/git/Cargo.toml b/crates/git/Cargo.toml index 06a46b3b76..8723e41ce4 100644 --- a/crates/git/Cargo.toml +++ b/crates/git/Cargo.toml @@ -31,10 +31,6 @@ time.workspace = true url.workspace = true util.workspace = true -[target.'cfg(target_os = "windows")'.dependencies] -windows.workspace = true - - [dev-dependencies] unindent.workspace = true serde_json.workspace = true diff --git a/crates/git/src/blame.rs b/crates/git/src/blame.rs index 030309df96..8f87a8ca54 100644 --- a/crates/git/src/blame.rs +++ b/crates/git/src/blame.rs @@ -4,7 +4,7 @@ use anyhow::{anyhow, Context, Result}; use collections::{HashMap, HashSet}; use serde::{Deserialize, Serialize}; use std::io::Write; -use std::process::{Command, Stdio}; +use std::process::Stdio; use std::sync::Arc; use std::{ops::Range, path::Path}; use text::Rope; @@ -80,9 +80,7 @@ fn run_git_blame( path: &Path, contents: &Rope, ) -> Result { - let mut child = Command::new(git_binary); - - child + let child = util::command::new_std_command(git_binary) .current_dir(working_directory) .arg("blame") .arg("--incremental") @@ -91,15 +89,7 @@ fn run_git_blame( .arg(path.as_os_str()) .stdin(Stdio::piped()) .stdout(Stdio::piped()) - .stderr(Stdio::piped()); - - #[cfg(windows)] - { - use std::os::windows::process::CommandExt; - child.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0); - } - - let child = child + .stderr(Stdio::piped()) .spawn() .map_err(|e| anyhow!("Failed to start git blame process: {}", e))?; diff --git a/crates/git/src/commit.rs b/crates/git/src/commit.rs index bdac6ff287..f32ad226af 100644 --- a/crates/git/src/commit.rs +++ b/crates/git/src/commit.rs @@ -2,10 +2,6 @@ use crate::Oid; use anyhow::{anyhow, Result}; use collections::HashMap; use std::path::Path; -use std::process::Command; - -#[cfg(windows)] -use std::os::windows::process::CommandExt; pub fn get_messages(working_directory: &Path, shas: &[Oid]) -> Result> { if shas.is_empty() { @@ -14,19 +10,12 @@ pub fn get_messages(working_directory: &Path, shas: &[Oid]) -> Result Result { - let mut child = Command::new(git_binary); - - child + let child = util::command::new_std_command(git_binary) .current_dir(working_directory) .args([ "--no-optional-locks", @@ -37,15 +35,7 @@ impl GitStatus { })) .stdin(Stdio::null()) .stdout(Stdio::piped()) - .stderr(Stdio::piped()); - - #[cfg(windows)] - { - use std::os::windows::process::CommandExt; - child.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0); - } - - let child = child + .stderr(Stdio::piped()) .spawn() .map_err(|e| anyhow!("Failed to start git status process: {}", e))?; diff --git a/crates/gpui/src/platform/windows/platform.rs b/crates/gpui/src/platform/windows/platform.rs index 29443afabb..91e9816106 100644 --- a/crates/gpui/src/platform/windows/platform.rs +++ b/crates/gpui/src/platform/windows/platform.rs @@ -292,7 +292,7 @@ impl Platform for WindowsPlatform { pid, app_path.display(), ); - let restart_process = std::process::Command::new("powershell.exe") + let restart_process = util::command::new_std_command("powershell.exe") .arg("-command") .arg(script) .spawn(); diff --git a/crates/languages/Cargo.toml b/crates/languages/Cargo.toml index 96a44403bc..951423056e 100644 --- a/crates/languages/Cargo.toml +++ b/crates/languages/Cargo.toml @@ -29,7 +29,7 @@ load-grammars = [ "tree-sitter-rust", "tree-sitter-typescript", "tree-sitter-yaml", - "tree-sitter" + "tree-sitter", ] [dependencies] diff --git a/crates/languages/src/c.rs b/crates/languages/src/c.rs index a0e0f6dadb..5bfb7f0bc2 100644 --- a/crates/languages/src/c.rs +++ b/crates/languages/src/c.rs @@ -85,7 +85,7 @@ impl super::LspAdapter for CLspAdapter { } futures::io::copy(response.body_mut(), &mut file).await?; - let unzip_status = smol::process::Command::new("unzip") + let unzip_status = util::command::new_smol_command("unzip") .current_dir(&container_dir) .arg(&zip_path) .output() diff --git a/crates/languages/src/go.rs b/crates/languages/src/go.rs index 669f6918a9..64583ad61f 100644 --- a/crates/languages/src/go.rs +++ b/crates/languages/src/go.rs @@ -8,7 +8,7 @@ pub use language::*; use lsp::{LanguageServerBinary, LanguageServerName}; use regex::Regex; use serde_json::json; -use smol::{fs, process}; +use smol::fs; use std::{ any::Any, borrow::Cow, @@ -138,8 +138,8 @@ impl super::LspAdapter for GoLspAdapter { let gobin_dir = container_dir.join("gobin"); fs::create_dir_all(&gobin_dir).await?; - let go = delegate.which("go".as_ref()).await.unwrap_or("go".into()); - let install_output = process::Command::new(go) + + let install_output = util::command::new_smol_command("go") .env("GO111MODULE", "on") .env("GOBIN", &gobin_dir) .args(["install", "golang.org/x/tools/gopls@latest"]) @@ -157,7 +157,7 @@ impl super::LspAdapter for GoLspAdapter { } let installed_binary_path = gobin_dir.join("gopls"); - let version_output = process::Command::new(&installed_binary_path) + let version_output = util::command::new_smol_command(&installed_binary_path) .arg("version") .output() .await diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index a29eb1c679..a5fe479627 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -19,7 +19,7 @@ use pet_core::python_environment::PythonEnvironmentKind; use pet_core::Configuration; use project::lsp_store::language_server_settings; use serde_json::{json, Value}; -use smol::{lock::OnceCell, process::Command}; +use smol::lock::OnceCell; use std::cmp::Ordering; use std::str::FromStr; @@ -698,7 +698,7 @@ impl PyLspAdapter { let mut path = PathBuf::from(work_dir.as_ref()); path.push("pylsp-venv"); if !path.exists() { - Command::new(python_path) + util::command::new_smol_command(python_path) .arg("-m") .arg("venv") .arg("pylsp-venv") @@ -779,7 +779,7 @@ impl LspAdapter for PyLspAdapter { let venv = self.base_venv(delegate).await.map_err(|e| anyhow!(e))?; let pip_path = venv.join("bin").join("pip3"); ensure!( - Command::new(pip_path.as_path()) + util::command::new_smol_command(pip_path.as_path()) .arg("install") .arg("python-lsp-server") .output() @@ -789,7 +789,7 @@ impl LspAdapter for PyLspAdapter { "python-lsp-server installation failed" ); ensure!( - Command::new(pip_path.as_path()) + util::command::new_smol_command(pip_path.as_path()) .arg("install") .arg("python-lsp-server[all]") .output() @@ -799,7 +799,7 @@ impl LspAdapter for PyLspAdapter { "python-lsp-server[all] installation failed" ); ensure!( - Command::new(pip_path) + util::command::new_smol_command(pip_path) .arg("install") .arg("pylsp-mypy") .output() diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index 730f20b134..7f5912d73e 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -14,8 +14,7 @@ use std::{ any::Any, borrow::Cow, path::{Path, PathBuf}, - sync::Arc, - sync::LazyLock, + sync::{Arc, LazyLock}, }; use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName}; use util::{fs::remove_matching, maybe, ResultExt}; @@ -639,7 +638,7 @@ fn package_name_and_bin_name_from_abs_path( abs_path: &Path, project_env: Option<&HashMap>, ) -> Option<(String, String)> { - let mut command = std::process::Command::new("cargo"); + let mut command = util::command::new_std_command("cargo"); if let Some(envs) = project_env { command.envs(envs); } @@ -685,11 +684,10 @@ fn human_readable_package_name( package_directory: &Path, project_env: Option<&HashMap>, ) -> Option { - let mut command = std::process::Command::new("cargo"); + let mut command = util::command::new_std_command("cargo"); if let Some(envs) = project_env { command.envs(envs); } - let pkgid = String::from_utf8( command .current_dir(package_directory) diff --git a/crates/lsp/Cargo.toml b/crates/lsp/Cargo.toml index 3460bf34dd..f06173ac1b 100644 --- a/crates/lsp/Cargo.toml +++ b/crates/lsp/Cargo.toml @@ -32,9 +32,6 @@ smol.workspace = true util.workspace = true release_channel.workspace = true -[target.'cfg(windows)'.dependencies] -windows.workspace = true - [dev-dependencies] async-pipe.workspace = true ctor.workspace = true diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 5f0186e61e..87c04030bd 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -19,12 +19,9 @@ use serde_json::{json, value::RawValue, Value}; use smol::{ channel, io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, - process::{self, Child}, + process::Child, }; -#[cfg(target_os = "windows")] -use smol::process::windows::CommandExt; - use std::{ ffi::{OsStr, OsString}, fmt, @@ -346,23 +343,21 @@ impl LanguageServer { &binary.arguments ); - let mut command = process::Command::new(&binary.path); - command + let mut server = util::command::new_smol_command(&binary.path) .current_dir(working_dir) .args(&binary.arguments) .envs(binary.env.unwrap_or_default()) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) - .kill_on_drop(true); - #[cfg(windows)] - command.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0); - let mut server = command.spawn().with_context(|| { - format!( - "failed to spawn command. path: {:?}, working directory: {:?}, args: {:?}", - binary.path, working_dir, &binary.arguments - ) - })?; + .kill_on_drop(true) + .spawn() + .with_context(|| { + format!( + "failed to spawn command. path: {:?}, working directory: {:?}, args: {:?}", + binary.path, working_dir, &binary.arguments + ) + })?; let stdin = server.stdin.take().unwrap(); let stdout = server.stdout.take().unwrap(); diff --git a/crates/node_runtime/Cargo.toml b/crates/node_runtime/Cargo.toml index d852b7ebdf..20b6be407f 100644 --- a/crates/node_runtime/Cargo.toml +++ b/crates/node_runtime/Cargo.toml @@ -37,7 +37,6 @@ which.workspace = true [target.'cfg(windows)'.dependencies] async-std = { version = "1.12.0", features = ["unstable"] } -windows.workspace = true [dev-dependencies] tempfile.workspace = true diff --git a/crates/node_runtime/src/node_runtime.rs b/crates/node_runtime/src/node_runtime.rs index 9ad14bddc4..33df4f7d15 100644 --- a/crates/node_runtime/src/node_runtime.rs +++ b/crates/node_runtime/src/node_runtime.rs @@ -9,7 +9,7 @@ use http_client::{HttpClient, Uri}; use semver::Version; use serde::Deserialize; use smol::io::BufReader; -use smol::{fs, lock::Mutex, process::Command}; +use smol::{fs, lock::Mutex}; use std::ffi::OsString; use std::io; use std::process::{Output, Stdio}; @@ -20,9 +20,6 @@ use std::{ }; use util::ResultExt; -#[cfg(windows)] -use smol::process::windows::CommandExt; - #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct NodeBinaryOptions { pub allow_path_lookup: bool, @@ -315,9 +312,7 @@ impl ManagedNodeRuntime { let node_binary = node_dir.join(Self::NODE_PATH); let npm_file = node_dir.join(Self::NPM_PATH); - let mut command = Command::new(&node_binary); - - command + let result = util::command::new_smol_command(&node_binary) .env_clear() .arg(npm_file) .arg("--version") @@ -326,12 +321,9 @@ impl ManagedNodeRuntime { .stderr(Stdio::null()) .args(["--cache".into(), node_dir.join("cache")]) .args(["--userconfig".into(), node_dir.join("blank_user_npmrc")]) - .args(["--globalconfig".into(), node_dir.join("blank_global_npmrc")]); - - #[cfg(windows)] - command.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0); - - let result = command.status().await; + .args(["--globalconfig".into(), node_dir.join("blank_global_npmrc")]) + .status() + .await; let valid = matches!(result, Ok(status) if status.success()); if !valid { @@ -412,7 +404,7 @@ impl NodeRuntimeTrait for ManagedNodeRuntime { return Err(anyhow!("missing npm file")); } - let mut command = Command::new(node_binary); + let mut command = util::command::new_smol_command(node_binary); command.env_clear(); command.env("PATH", env_path); command.arg(npm_file).arg(subcommand); @@ -473,7 +465,7 @@ pub struct SystemNodeRuntime { impl SystemNodeRuntime { const MIN_VERSION: semver::Version = Version::new(18, 0, 0); async fn new(node: PathBuf, npm: PathBuf) -> Result> { - let output = Command::new(&node) + let output = util::command::new_smol_command(&node) .arg("--version") .output() .await @@ -543,7 +535,7 @@ impl NodeRuntimeTrait for SystemNodeRuntime { subcommand: &str, args: &[&str], ) -> anyhow::Result { - let mut command = Command::new(self.npm.clone()); + let mut command = util::command::new_smol_command(self.npm.clone()); command .env_clear() .env("PATH", std::env::var_os("PATH").unwrap_or_default()) @@ -639,7 +631,11 @@ impl NodeRuntimeTrait for UnavailableNodeRuntime { } } -fn configure_npm_command(command: &mut Command, directory: Option<&Path>, proxy: Option<&Uri>) { +fn configure_npm_command( + command: &mut smol::process::Command, + directory: Option<&Path>, + proxy: Option<&Uri>, +) { if let Some(directory) = directory { command.current_dir(directory); command.args(["--prefix".into(), directory.to_path_buf()]); @@ -674,6 +670,5 @@ fn configure_npm_command(command: &mut Command, directory: Option<&Path>, proxy: { command.env("ComSpec", val); } - command.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0); } } diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index b9fdd04be6..68fdb375f4 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -73,9 +73,6 @@ url.workspace = true which.workspace = true fancy-regex.workspace = true -[target.'cfg(target_os = "windows")'.dependencies] -windows.workspace = true - [dev-dependencies] client = { workspace = true, features = ["test-support"] } collections = { workspace = true, features = ["test-support"] } diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 0723ba689b..3ed311a51d 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -611,12 +611,7 @@ impl LocalLspStore { Some(worktree_path) })?; - let mut child = smol::process::Command::new(command); - #[cfg(target_os = "windows")] - { - use smol::process::windows::CommandExt; - child.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0); - } + let mut child = util::command::new_smol_command(command); if let Some(buffer_env) = buffer.env.as_ref() { child.envs(buffer_env); @@ -7935,7 +7930,7 @@ impl LspAdapterDelegate for LocalLspAdapterDelegate { }; let env = self.shell_env().await; - let output = smol::process::Command::new(&npm) + let output = util::command::new_smol_command(&npm) .args(["root", "-g"]) .envs(env) .current_dir(local_package_directory) @@ -7969,7 +7964,7 @@ impl LspAdapterDelegate for LocalLspAdapterDelegate { async fn try_exec(&self, command: LanguageServerBinary) -> Result<()> { let working_dir = self.worktree_root_path(); - let output = smol::process::Command::new(&command.path) + let output = util::command::new_smol_command(&command.path) .args(command.arguments) .envs(command.env.clone().unwrap_or_default()) .current_dir(working_dir) diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index 1ea76a24c8..87a58cb050 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -255,7 +255,7 @@ impl SshSocket { // and passes -l as an argument to sh, not to ls. // You need to do it like this: $ ssh host "sh -c 'ls -l /tmp'" fn ssh_command(&self, program: &str, args: &[&str]) -> process::Command { - let mut command = process::Command::new("ssh"); + let mut command = util::command::new_smol_command("ssh"); let to_run = iter::once(&program) .chain(args.iter()) .map(|token| { @@ -1224,7 +1224,7 @@ trait RemoteConnection: Send + Sync { struct SshRemoteConnection { socket: SshSocket, - master_process: Mutex>, + master_process: Mutex>, remote_binary_path: Option, _temp_dir: TempDir, } @@ -1258,7 +1258,7 @@ impl RemoteConnection for SshRemoteConnection { dest_path: PathBuf, cx: &AppContext, ) -> Task> { - let mut command = process::Command::new("scp"); + let mut command = util::command::new_smol_command("scp"); let output = self .socket .ssh_options(&mut command) @@ -1910,7 +1910,7 @@ impl SshRemoteConnection { async fn upload_file(&self, src_path: &Path, dest_path: &Path) -> Result<()> { log::debug!("uploading file {:?} to {:?}", src_path, dest_path); - let mut command = process::Command::new("scp"); + let mut command = util::command::new_smol_command("scp"); let output = self .socket .ssh_options(&mut command) diff --git a/crates/repl/Cargo.toml b/crates/repl/Cargo.toml index 3f59ca325b..60e8734771 100644 --- a/crates/repl/Cargo.toml +++ b/crates/repl/Cargo.toml @@ -49,9 +49,6 @@ uuid.workspace = true workspace.workspace = true picker.workspace = true -[target.'cfg(target_os = "windows")'.dependencies] -windows.workspace = true - [dev-dependencies] editor = { workspace = true, features = ["test-support"] } env_logger.workspace = true diff --git a/crates/repl/src/kernels/mod.rs b/crates/repl/src/kernels/mod.rs index cea5adb59e..3fe4c3c12d 100644 --- a/crates/repl/src/kernels/mod.rs +++ b/crates/repl/src/kernels/mod.rs @@ -16,7 +16,6 @@ pub use remote_kernels::*; use anyhow::Result; use runtimelib::{ExecutionState, JupyterKernelspec, JupyterMessage, KernelInfoReply}; -use smol::process::Command; use ui::SharedString; pub type JupyterMessageChannel = stream::SelectAll>; @@ -85,7 +84,7 @@ pub fn python_env_kernel_specifications( let python_path = toolchain.path.to_string(); // Check if ipykernel is installed - let ipykernel_check = Command::new(&python_path) + let ipykernel_check = util::command::new_smol_command(&python_path) .args(&["-c", "import ipykernel"]) .output() .await; diff --git a/crates/repl/src/kernels/native_kernel.rs b/crates/repl/src/kernels/native_kernel.rs index 8a232c3de9..03a57b34ef 100644 --- a/crates/repl/src/kernels/native_kernel.rs +++ b/crates/repl/src/kernels/native_kernel.rs @@ -48,7 +48,7 @@ impl LocalKernelSpecification { self.name ); - let mut cmd = Command::new(&argv[0]); + let mut cmd = util::command::new_smol_command(&argv[0]); for arg in &argv[1..] { if arg == "{connection_file}" { @@ -62,12 +62,6 @@ impl LocalKernelSpecification { cmd.envs(env); } - #[cfg(windows)] - { - use smol::process::windows::CommandExt; - cmd.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0); - } - Ok(cmd) } } @@ -350,17 +344,11 @@ pub async fn local_kernel_specifications(fs: Arc) -> Result, cx: &mut ModelContext, ) -> Result { - let mut process = Command::new(&binary_path); - process + let mut process = util::command::new_smol_command(&binary_path) .arg("stdio") .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) - .kill_on_drop(true); - - #[cfg(target_os = "windows")] - { - use smol::process::windows::CommandExt; - process.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0); - } - - let mut process = process.spawn().context("failed to start the binary")?; + .kill_on_drop(true) + .spawn() + .context("failed to start the binary")?; let stdin = process .stdin diff --git a/crates/util/Cargo.toml b/crates/util/Cargo.toml index 58c4686bf9..94d580e643 100644 --- a/crates/util/Cargo.toml +++ b/crates/util/Cargo.toml @@ -30,6 +30,7 @@ regex.workspace = true rust-embed.workspace = true serde.workspace = true serde_json.workspace = true +smol.workspace = true take-until = "0.2.0" tempfile = { workspace = true, optional = true } unicase.workspace = true diff --git a/crates/util/src/command.rs b/crates/util/src/command.rs new file mode 100644 index 0000000000..85e2234991 --- /dev/null +++ b/crates/util/src/command.rs @@ -0,0 +1,32 @@ +use std::ffi::OsStr; + +#[cfg(target_os = "windows")] +const CREATE_NO_WINDOW: u32 = 0x0800_0000_u32; + +#[cfg(target_os = "windows")] +pub fn new_std_command(program: impl AsRef) -> std::process::Command { + use std::os::windows::process::CommandExt; + + let mut command = std::process::Command::new(program); + command.creation_flags(CREATE_NO_WINDOW); + command +} + +#[cfg(not(target_os = "windows"))] +pub fn new_std_command(program: impl AsRef) -> std::process::Command { + std::process::Command::new(program) +} + +#[cfg(target_os = "windows")] +pub fn new_smol_command(program: impl AsRef) -> smol::process::Command { + use smol::process::windows::CommandExt; + + let mut command = smol::process::Command::new(program); + command.creation_flags(CREATE_NO_WINDOW); + command +} + +#[cfg(not(target_os = "windows"))] +pub fn new_smol_command(program: impl AsRef) -> smol::process::Command { + smol::process::Command::new(program) +} diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index e27fd65ac7..5141f85797 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -1,4 +1,5 @@ pub mod arc_cow; +pub mod command; pub mod fs; pub mod paths; pub mod serde; From 0e62b6dddd0ff9b6168b1c9ce1519efa109d5973 Mon Sep 17 00:00:00 2001 From: Ryan Hawkins Date: Wed, 20 Nov 2024 18:00:21 -0700 Subject: [PATCH 28/68] Add `file_scan_inclusions` setting to customize Zed file indexing (#16852) Closes #4745 Release Notes: - Added a new `file_scan_inclusions` setting to force Zed to index files that match the provided globs, even if they're gitignored. --------- Co-authored-by: Mikayla Maki --- assets/settings/default.json | 10 +- crates/project_panel/src/project_panel.rs | 1 + crates/worktree/Cargo.toml | 2 +- crates/worktree/src/worktree.rs | 85 ++++++-- crates/worktree/src/worktree_settings.rs | 36 +++- crates/worktree/src/worktree_tests.rs | 241 +++++++++++++++++++++- 6 files changed, 350 insertions(+), 25 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 3757dfe119..d654082e24 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -668,7 +668,7 @@ }, // Add files or globs of files that will be excluded by Zed entirely: // they will be skipped during FS scan(s), file tree and file search - // will lack the corresponding file entries. + // will lack the corresponding file entries. Overrides `file_scan_inclusions`. "file_scan_exclusions": [ "**/.git", "**/.svn", @@ -679,6 +679,14 @@ "**/.classpath", "**/.settings" ], + // Add files or globs of files that will be included by Zed, even when + // ignored by git. This is useful for files that are not tracked by git, + // but are still important to your project. Note that globs that are + // overly broad can slow down Zed's file scanning. Overridden by `file_scan_exclusions`. + "file_scan_inclusions": [ + ".env*", + "docker-compose.*.yml" + ], // Git gutter behavior configuration. "git": { // Control whether the git gutter is shown. May take 2 values: diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 94472f5576..9432d1e6d5 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -2033,6 +2033,7 @@ impl ProjectPanel { is_ignored: entry.is_ignored, is_external: false, is_private: false, + is_always_included: entry.is_always_included, git_status: entry.git_status, canonical_path: entry.canonical_path.clone(), char_bag: entry.char_bag, diff --git a/crates/worktree/Cargo.toml b/crates/worktree/Cargo.toml index da3676f15c..adbbf66d23 100644 --- a/crates/worktree/Cargo.toml +++ b/crates/worktree/Cargo.toml @@ -37,7 +37,7 @@ log.workspace = true parking_lot.workspace = true paths.workspace = true postage.workspace = true -rpc.workspace = true +rpc = { workspace = true, features = ["gpui"] } schemars.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 5bd064b534..bf072ca549 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -65,7 +65,10 @@ use std::{ }; use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet}; use text::{LineEnding, Rope}; -use util::{paths::home_dir, ResultExt}; +use util::{ + paths::{home_dir, PathMatcher}, + ResultExt, +}; pub use worktree_settings::WorktreeSettings; #[cfg(feature = "test-support")] @@ -134,6 +137,7 @@ pub struct RemoteWorktree { background_snapshot: Arc)>>, project_id: u64, client: AnyProtoClient, + file_scan_inclusions: PathMatcher, updates_tx: Option>, update_observer: Option>, snapshot_subscriptions: VecDeque<(usize, oneshot::Sender<()>)>, @@ -150,6 +154,7 @@ pub struct Snapshot { root_char_bag: CharBag, entries_by_path: SumTree, entries_by_id: SumTree, + always_included_entries: Vec>, repository_entries: TreeMap, /// A number that increases every time the worktree begins scanning @@ -433,7 +438,7 @@ impl Worktree { cx.observe_global::(move |this, cx| { if let Self::Local(this) = this { let settings = WorktreeSettings::get(settings_location, cx).clone(); - if settings != this.settings { + if this.settings != settings { this.settings = settings; this.restart_background_scanners(cx); } @@ -480,11 +485,19 @@ impl Worktree { let (background_updates_tx, mut background_updates_rx) = mpsc::unbounded(); let (mut snapshot_updated_tx, mut snapshot_updated_rx) = watch::channel(); + let worktree_id = snapshot.id(); + let settings_location = Some(SettingsLocation { + worktree_id, + path: Path::new(EMPTY_PATH), + }); + + let settings = WorktreeSettings::get(settings_location, cx).clone(); let worktree = RemoteWorktree { client, project_id, replica_id, snapshot, + file_scan_inclusions: settings.file_scan_inclusions.clone(), background_snapshot: background_snapshot.clone(), updates_tx: Some(background_updates_tx), update_observer: None, @@ -500,7 +513,10 @@ impl Worktree { while let Some(update) = background_updates_rx.next().await { { let mut lock = background_snapshot.lock(); - if let Err(error) = lock.0.apply_remote_update(update.clone()) { + if let Err(error) = lock + .0 + .apply_remote_update(update.clone(), &settings.file_scan_inclusions) + { log::error!("error applying worktree update: {}", error); } lock.1.push(update); @@ -1022,7 +1038,17 @@ impl LocalWorktree { let (path_prefixes_to_scan_tx, path_prefixes_to_scan_rx) = channel::unbounded(); self.scan_requests_tx = scan_requests_tx; self.path_prefixes_to_scan_tx = path_prefixes_to_scan_tx; + self.start_background_scanner(scan_requests_rx, path_prefixes_to_scan_rx, cx); + let always_included_entries = mem::take(&mut self.snapshot.always_included_entries); + log::debug!( + "refreshing entries for the following always included paths: {:?}", + always_included_entries + ); + + // Cleans up old always included entries to ensure they get updated properly. Otherwise, + // nested always included entries may not get updated and will result in out-of-date info. + self.refresh_entries_for_paths(always_included_entries); } fn start_background_scanner( @@ -1971,7 +1997,7 @@ impl RemoteWorktree { this.update(&mut cx, |worktree, _| { let worktree = worktree.as_remote_mut().unwrap(); let snapshot = &mut worktree.background_snapshot.lock().0; - let entry = snapshot.insert_entry(entry); + let entry = snapshot.insert_entry(entry, &worktree.file_scan_inclusions); worktree.snapshot = snapshot.clone(); entry })? @@ -2052,6 +2078,7 @@ impl Snapshot { abs_path, root_char_bag: root_name.chars().map(|c| c.to_ascii_lowercase()).collect(), root_name, + always_included_entries: Default::default(), entries_by_path: Default::default(), entries_by_id: Default::default(), repository_entries: Default::default(), @@ -2115,8 +2142,12 @@ impl Snapshot { self.entries_by_id.get(&entry_id, &()).is_some() } - fn insert_entry(&mut self, entry: proto::Entry) -> Result { - let entry = Entry::try_from((&self.root_char_bag, entry))?; + fn insert_entry( + &mut self, + entry: proto::Entry, + always_included_paths: &PathMatcher, + ) -> Result { + let entry = Entry::try_from((&self.root_char_bag, always_included_paths, entry))?; let old_entry = self.entries_by_id.insert_or_replace( PathEntry { id: entry.id, @@ -2170,7 +2201,11 @@ impl Snapshot { } } - pub(crate) fn apply_remote_update(&mut self, mut update: proto::UpdateWorktree) -> Result<()> { + pub(crate) fn apply_remote_update( + &mut self, + mut update: proto::UpdateWorktree, + always_included_paths: &PathMatcher, + ) -> Result<()> { log::trace!( "applying remote worktree update. {} entries updated, {} removed", update.updated_entries.len(), @@ -2193,7 +2228,7 @@ impl Snapshot { } for entry in update.updated_entries { - let entry = Entry::try_from((&self.root_char_bag, entry))?; + let entry = Entry::try_from((&self.root_char_bag, always_included_paths, entry))?; if let Some(PathEntry { path, .. }) = self.entries_by_id.get(&entry.id, &()) { entries_by_path_edits.push(Edit::Remove(PathKey(path.clone()))); } @@ -2713,7 +2748,7 @@ impl LocalSnapshot { for entry in self.entries_by_path.cursor::<()>(&()) { if entry.is_file() { assert_eq!(files.next().unwrap().inode, entry.inode); - if !entry.is_ignored && !entry.is_external { + if (!entry.is_ignored && !entry.is_external) || entry.is_always_included { assert_eq!(visible_files.next().unwrap().inode, entry.inode); } } @@ -2796,7 +2831,7 @@ impl LocalSnapshot { impl BackgroundScannerState { fn should_scan_directory(&self, entry: &Entry) -> bool { - (!entry.is_external && !entry.is_ignored) + (!entry.is_external && (!entry.is_ignored || entry.is_always_included)) || entry.path.file_name() == Some(*DOT_GIT) || entry.path.file_name() == Some(local_settings_folder_relative_path().as_os_str()) || self.scanned_dirs.contains(&entry.id) // If we've ever scanned it, keep scanning @@ -3369,6 +3404,12 @@ pub struct Entry { /// exclude them from searches. pub is_ignored: bool, + /// Whether this entry is always included in searches. + /// + /// This is used for entries that are always included in searches, even + /// if they are ignored by git. Overridden by file_scan_exclusions. + pub is_always_included: bool, + /// Whether this entry's canonical path is outside of the worktree. /// This means the entry is only accessible from the worktree root via a /// symlink. @@ -3440,6 +3481,7 @@ impl Entry { size: metadata.len, canonical_path, is_ignored: false, + is_always_included: false, is_external: false, is_private: false, git_status: None, @@ -3486,7 +3528,8 @@ impl sum_tree::Item for Entry { type Summary = EntrySummary; fn summary(&self, _cx: &()) -> Self::Summary { - let non_ignored_count = if self.is_ignored || self.is_external { + let non_ignored_count = if (self.is_ignored || self.is_external) && !self.is_always_included + { 0 } else { 1 @@ -4254,6 +4297,7 @@ impl BackgroundScanner { if child_entry.is_dir() { child_entry.is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, true); + child_entry.is_always_included = self.settings.is_path_always_included(&child_path); // Avoid recursing until crash in the case of a recursive symlink if job.ancestor_inodes.contains(&child_entry.inode) { @@ -4278,6 +4322,7 @@ impl BackgroundScanner { } } else { child_entry.is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, false); + child_entry.is_always_included = self.settings.is_path_always_included(&child_path); if !child_entry.is_ignored { if let Some(repo) = &containing_repository { if let Ok(repo_path) = child_entry.path.strip_prefix(&repo.work_directory) { @@ -4314,6 +4359,12 @@ impl BackgroundScanner { new_jobs.remove(job_ix); } } + if entry.is_always_included { + state + .snapshot + .always_included_entries + .push(entry.path.clone()); + } } state.populate_dir(&job.path, new_entries, new_ignore); @@ -4430,6 +4481,7 @@ impl BackgroundScanner { fs_entry.is_ignored = ignore_stack.is_abs_path_ignored(&abs_path, is_dir); fs_entry.is_external = is_external; fs_entry.is_private = self.is_path_private(path); + fs_entry.is_always_included = self.settings.is_path_always_included(path); if let (Some(scan_queue_tx), true) = (&scan_queue_tx, is_dir) { if state.should_scan_directory(&fs_entry) @@ -5317,7 +5369,7 @@ impl<'a> Traversal<'a> { if let Some(entry) = self.cursor.item() { if (self.include_files || !entry.is_file()) && (self.include_dirs || !entry.is_dir()) - && (self.include_ignored || !entry.is_ignored) + && (self.include_ignored || !entry.is_ignored || entry.is_always_included) { return true; } @@ -5448,10 +5500,12 @@ impl<'a> From<&'a Entry> for proto::Entry { } } -impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry { +impl<'a> TryFrom<(&'a CharBag, &PathMatcher, proto::Entry)> for Entry { type Error = anyhow::Error; - fn try_from((root_char_bag, entry): (&'a CharBag, proto::Entry)) -> Result { + fn try_from( + (root_char_bag, always_included, entry): (&'a CharBag, &PathMatcher, proto::Entry), + ) -> Result { let kind = if entry.is_dir { EntryKind::Dir } else { @@ -5462,7 +5516,7 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry { Ok(Entry { id: ProjectEntryId::from_proto(entry.id), kind, - path, + path: path.clone(), inode: entry.inode, mtime: entry.mtime.map(|time| time.into()), size: entry.size.unwrap_or(0), @@ -5470,6 +5524,7 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry { .canonical_path .map(|path_string| Box::from(Path::new(&path_string))), is_ignored: entry.is_ignored, + is_always_included: always_included.is_match(path.as_ref()), is_external: entry.is_external, git_status: git_status_from_proto(entry.git_status), is_private: false, diff --git a/crates/worktree/src/worktree_settings.rs b/crates/worktree/src/worktree_settings.rs index 32851d963a..f26dc4af0f 100644 --- a/crates/worktree/src/worktree_settings.rs +++ b/crates/worktree/src/worktree_settings.rs @@ -9,6 +9,7 @@ use util::paths::PathMatcher; #[derive(Clone, PartialEq, Eq)] pub struct WorktreeSettings { + pub file_scan_inclusions: PathMatcher, pub file_scan_exclusions: PathMatcher, pub private_files: PathMatcher, } @@ -21,13 +22,19 @@ impl WorktreeSettings { pub fn is_path_excluded(&self, path: &Path) -> bool { path.ancestors() - .any(|ancestor| self.file_scan_exclusions.is_match(ancestor)) + .any(|ancestor| self.file_scan_exclusions.is_match(&ancestor)) + } + + pub fn is_path_always_included(&self, path: &Path) -> bool { + path.ancestors() + .any(|ancestor| self.file_scan_inclusions.is_match(&ancestor)) } } #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] pub struct WorktreeSettingsContent { - /// Completely ignore files matching globs from `file_scan_exclusions` + /// Completely ignore files matching globs from `file_scan_exclusions`. Overrides + /// `file_scan_inclusions`. /// /// Default: [ /// "**/.git", @@ -42,6 +49,15 @@ pub struct WorktreeSettingsContent { #[serde(default)] pub file_scan_exclusions: Option>, + /// Always include files that match these globs when scanning for files, even if they're + /// ignored by git. This setting is overridden by `file_scan_exclusions`. + /// Default: [ + /// ".env*", + /// "docker-compose.*.yml", + /// ] + #[serde(default)] + pub file_scan_inclusions: Option>, + /// Treat the files matching these globs as `.env` files. /// Default: [ "**/.env*" ] pub private_files: Option>, @@ -59,11 +75,27 @@ impl Settings for WorktreeSettings { let result: WorktreeSettingsContent = sources.json_merge()?; let mut file_scan_exclusions = result.file_scan_exclusions.unwrap_or_default(); let mut private_files = result.private_files.unwrap_or_default(); + let mut parsed_file_scan_inclusions: Vec = result + .file_scan_inclusions + .unwrap_or_default() + .iter() + .flat_map(|glob| { + Path::new(glob) + .ancestors() + .map(|a| a.to_string_lossy().into()) + }) + .filter(|p| p != "") + .collect(); file_scan_exclusions.sort(); private_files.sort(); + parsed_file_scan_inclusions.sort(); Ok(Self { file_scan_exclusions: path_matchers(&file_scan_exclusions, "file_scan_exclusions")?, private_files: path_matchers(&private_files, "private_files")?, + file_scan_inclusions: path_matchers( + &parsed_file_scan_inclusions, + "file_scan_inclusions", + )?, }) } } diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index 75f86fa606..fbedd896e3 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -878,6 +878,211 @@ async fn test_write_file(cx: &mut TestAppContext) { }); } +#[gpui::test] +async fn test_file_scan_inclusions(cx: &mut TestAppContext) { + init_test(cx); + cx.executor().allow_parking(); + let dir = temp_tree(json!({ + ".gitignore": "**/target\n/node_modules\ntop_level.txt\n", + "target": { + "index": "blah2" + }, + "node_modules": { + ".DS_Store": "", + "prettier": { + "package.json": "{}", + }, + }, + "src": { + ".DS_Store": "", + "foo": { + "foo.rs": "mod another;\n", + "another.rs": "// another", + }, + "bar": { + "bar.rs": "// bar", + }, + "lib.rs": "mod foo;\nmod bar;\n", + }, + "top_level.txt": "top level file", + ".DS_Store": "", + })); + cx.update(|cx| { + cx.update_global::(|store, cx| { + store.update_user_settings::(cx, |project_settings| { + project_settings.file_scan_exclusions = Some(vec![]); + project_settings.file_scan_inclusions = Some(vec![ + "node_modules/**/package.json".to_string(), + "**/.DS_Store".to_string(), + ]); + }); + }); + }); + + let tree = Worktree::local( + dir.path(), + true, + Arc::new(RealFs::default()), + Default::default(), + &mut cx.to_async(), + ) + .await + .unwrap(); + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + tree.flush_fs_events(cx).await; + tree.read_with(cx, |tree, _| { + // Assert that file_scan_inclusions overrides file_scan_exclusions. + check_worktree_entries( + tree, + &[], + &["target", "node_modules"], + &["src/lib.rs", "src/bar/bar.rs", ".gitignore"], + &[ + "node_modules/prettier/package.json", + ".DS_Store", + "node_modules/.DS_Store", + "src/.DS_Store", + ], + ) + }); +} + +#[gpui::test] +async fn test_file_scan_exclusions_overrules_inclusions(cx: &mut TestAppContext) { + init_test(cx); + cx.executor().allow_parking(); + let dir = temp_tree(json!({ + ".gitignore": "**/target\n/node_modules\n", + "target": { + "index": "blah2" + }, + "node_modules": { + ".DS_Store": "", + "prettier": { + "package.json": "{}", + }, + }, + "src": { + ".DS_Store": "", + "foo": { + "foo.rs": "mod another;\n", + "another.rs": "// another", + }, + }, + ".DS_Store": "", + })); + + cx.update(|cx| { + cx.update_global::(|store, cx| { + store.update_user_settings::(cx, |project_settings| { + project_settings.file_scan_exclusions = Some(vec!["**/.DS_Store".to_string()]); + project_settings.file_scan_inclusions = Some(vec!["**/.DS_Store".to_string()]); + }); + }); + }); + + let tree = Worktree::local( + dir.path(), + true, + Arc::new(RealFs::default()), + Default::default(), + &mut cx.to_async(), + ) + .await + .unwrap(); + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + tree.flush_fs_events(cx).await; + tree.read_with(cx, |tree, _| { + // Assert that file_scan_inclusions overrides file_scan_exclusions. + check_worktree_entries( + tree, + &[".DS_Store, src/.DS_Store"], + &["target", "node_modules"], + &["src/foo/another.rs", "src/foo/foo.rs", ".gitignore"], + &[], + ) + }); +} + +#[gpui::test] +async fn test_file_scan_inclusions_reindexes_on_setting_change(cx: &mut TestAppContext) { + init_test(cx); + cx.executor().allow_parking(); + let dir = temp_tree(json!({ + ".gitignore": "**/target\n/node_modules/\n", + "target": { + "index": "blah2" + }, + "node_modules": { + ".DS_Store": "", + "prettier": { + "package.json": "{}", + }, + }, + "src": { + ".DS_Store": "", + "foo": { + "foo.rs": "mod another;\n", + "another.rs": "// another", + }, + }, + ".DS_Store": "", + })); + + cx.update(|cx| { + cx.update_global::(|store, cx| { + store.update_user_settings::(cx, |project_settings| { + project_settings.file_scan_exclusions = Some(vec![]); + project_settings.file_scan_inclusions = Some(vec!["node_modules/**".to_string()]); + }); + }); + }); + let tree = Worktree::local( + dir.path(), + true, + Arc::new(RealFs::default()), + Default::default(), + &mut cx.to_async(), + ) + .await + .unwrap(); + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + tree.flush_fs_events(cx).await; + + tree.read_with(cx, |tree, _| { + assert!(tree + .entry_for_path("node_modules") + .is_some_and(|f| f.is_always_included)); + assert!(tree + .entry_for_path("node_modules/prettier/package.json") + .is_some_and(|f| f.is_always_included)); + }); + + cx.update(|cx| { + cx.update_global::(|store, cx| { + store.update_user_settings::(cx, |project_settings| { + project_settings.file_scan_exclusions = Some(vec![]); + project_settings.file_scan_inclusions = Some(vec![]); + }); + }); + }); + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + tree.flush_fs_events(cx).await; + + tree.read_with(cx, |tree, _| { + assert!(tree + .entry_for_path("node_modules") + .is_some_and(|f| !f.is_always_included)); + assert!(tree + .entry_for_path("node_modules/prettier/package.json") + .is_some_and(|f| !f.is_always_included)); + }); +} + #[gpui::test] async fn test_file_scan_exclusions(cx: &mut TestAppContext) { init_test(cx); @@ -939,6 +1144,7 @@ async fn test_file_scan_exclusions(cx: &mut TestAppContext) { ], &["target", "node_modules"], &["src/lib.rs", "src/bar/bar.rs", ".gitignore"], + &[], ) }); @@ -970,6 +1176,7 @@ async fn test_file_scan_exclusions(cx: &mut TestAppContext) { "src/.DS_Store", ".DS_Store", ], + &[], ) }); } @@ -1051,6 +1258,7 @@ async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) { "src/bar/bar.rs", ".gitignore", ], + &[], ) }); @@ -1111,6 +1319,7 @@ async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) { "src/new_file", ".gitignore", ], + &[], ) }); } @@ -1140,14 +1349,14 @@ async fn test_fs_events_in_dot_git_worktree(cx: &mut TestAppContext) { .await; tree.flush_fs_events(cx).await; tree.read_with(cx, |tree, _| { - check_worktree_entries(tree, &[], &["HEAD", "foo"], &[]) + check_worktree_entries(tree, &[], &["HEAD", "foo"], &[], &[]) }); std::fs::write(dot_git_worktree_dir.join("new_file"), "new file contents") .unwrap_or_else(|e| panic!("Failed to create in {dot_git_worktree_dir:?} a new file: {e}")); tree.flush_fs_events(cx).await; tree.read_with(cx, |tree, _| { - check_worktree_entries(tree, &[], &["HEAD", "foo", "new_file"], &[]) + check_worktree_entries(tree, &[], &["HEAD", "foo", "new_file"], &[], &[]) }); } @@ -1180,8 +1389,12 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) { let snapshot = Arc::new(Mutex::new(tree.snapshot())); tree.observe_updates(0, cx, { let snapshot = snapshot.clone(); + let settings = tree.settings().clone(); move |update| { - snapshot.lock().apply_remote_update(update).unwrap(); + snapshot + .lock() + .apply_remote_update(update, &settings.file_scan_inclusions) + .unwrap(); async { true } } }); @@ -1474,12 +1687,14 @@ async fn test_random_worktree_operations_during_initial_scan( snapshot }); + let settings = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().settings()); + for (i, snapshot) in snapshots.into_iter().enumerate().rev() { let mut updated_snapshot = snapshot.clone(); for update in updates.lock().iter() { if update.scan_id >= updated_snapshot.scan_id() as u64 { updated_snapshot - .apply_remote_update(update.clone()) + .apply_remote_update(update.clone(), &settings.file_scan_inclusions) .unwrap(); } } @@ -1610,10 +1825,14 @@ async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng) ); } + let settings = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().settings()); + for (i, mut prev_snapshot) in snapshots.into_iter().enumerate().rev() { for update in updates.lock().iter() { if update.scan_id >= prev_snapshot.scan_id() as u64 { - prev_snapshot.apply_remote_update(update.clone()).unwrap(); + prev_snapshot + .apply_remote_update(update.clone(), &settings.file_scan_inclusions) + .unwrap(); } } @@ -2588,6 +2807,7 @@ fn check_worktree_entries( expected_excluded_paths: &[&str], expected_ignored_paths: &[&str], expected_tracked_paths: &[&str], + expected_included_paths: &[&str], ) { for path in expected_excluded_paths { let entry = tree.entry_for_path(path); @@ -2610,10 +2830,19 @@ fn check_worktree_entries( .entry_for_path(path) .unwrap_or_else(|| panic!("Missing entry for expected tracked path '{path}'")); assert!( - !entry.is_ignored, + !entry.is_ignored || entry.is_always_included, "expected path '{path}' to be tracked, but got entry: {entry:?}", ); } + for path in expected_included_paths { + let entry = tree + .entry_for_path(path) + .unwrap_or_else(|| panic!("Missing entry for expected included path '{path}'")); + assert!( + entry.is_always_included, + "expected path '{path}' to always be included, but got entry: {entry:?}", + ); + } } fn init_test(cx: &mut gpui::TestAppContext) { From a03770837ea2cee44811181aa3bc413767668a25 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 20 Nov 2024 18:21:09 -0800 Subject: [PATCH 29/68] Add extensions to the remote server (#20049) TODO: - [x] Double check strange PHP env detection - [x] Clippy & etc. Release Notes: - Added support for extension languages on the remote server --------- Co-authored-by: Conrad Irwin --- Cargo.lock | 4 + crates/extension_host/Cargo.toml | 2 + crates/extension_host/src/extension_host.rs | 158 +++++++- crates/extension_host/src/headless_host.rs | 379 ++++++++++++++++++ crates/proto/proto/zed.proto | 28 +- crates/proto/src/proto.rs | 5 + crates/recent_projects/Cargo.toml | 1 + crates/recent_projects/src/ssh_connections.rs | 10 + crates/remote/src/ssh_session.rs | 1 + crates/remote_server/Cargo.toml | 1 + crates/remote_server/src/headless_project.rs | 21 + 11 files changed, 606 insertions(+), 4 deletions(-) create mode 100644 crates/extension_host/src/headless_host.rs diff --git a/Cargo.lock b/Cargo.lock index 8db4b8424d..49c4a10efc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4170,6 +4170,7 @@ dependencies = [ "paths", "project", "release_channel", + "remote", "reqwest_client", "schemars", "semantic_version", @@ -4178,6 +4179,7 @@ dependencies = [ "serde_json_lenient", "settings", "task", + "tempfile", "theme", "toml 0.8.19", "url", @@ -9677,6 +9679,7 @@ dependencies = [ "anyhow", "auto_update", "editor", + "extension_host", "file_finder", "futures 0.3.31", "fuzzy", @@ -9852,6 +9855,7 @@ dependencies = [ "client", "clock", "env_logger 0.11.5", + "extension_host", "fork", "fs", "futures 0.3.31", diff --git a/crates/extension_host/Cargo.toml b/crates/extension_host/Cargo.toml index 856466e1a1..31d3df88aa 100644 --- a/crates/extension_host/Cargo.toml +++ b/crates/extension_host/Cargo.toml @@ -34,6 +34,7 @@ lsp.workspace = true node_runtime.workspace = true paths.workspace = true project.workspace = true +remote.workspace = true release_channel.workspace = true schemars.workspace = true semantic_version.workspace = true @@ -42,6 +43,7 @@ serde_json.workspace = true serde_json_lenient.workspace = true settings.workspace = true task.workspace = true +tempfile.workspace = true toml.workspace = true url.workspace = true util.workspace = true diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index 1adea4e0fb..a858123fd9 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -1,5 +1,6 @@ pub mod extension_lsp_adapter; pub mod extension_settings; +pub mod headless_host; pub mod wasm_host; #[cfg(test)] @@ -9,8 +10,8 @@ use crate::extension_lsp_adapter::ExtensionLspAdapter; use anyhow::{anyhow, bail, Context as _, Result}; use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; -use client::{telemetry::Telemetry, Client, ExtensionMetadata, GetExtensionsResponse}; -use collections::{btree_map, BTreeMap, HashSet}; +use client::{proto, telemetry::Telemetry, Client, ExtensionMetadata, GetExtensionsResponse}; +use collections::{btree_map, BTreeMap, HashMap, HashSet}; use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder}; use extension::Extension; pub use extension::ExtensionManifest; @@ -36,6 +37,7 @@ use lsp::LanguageServerName; use node_runtime::NodeRuntime; use project::ContextProviderWithTasks; use release_channel::ReleaseChannel; +use remote::SshRemoteClient; use semantic_version::SemanticVersion; use serde::{Deserialize, Serialize}; use settings::Settings; @@ -178,6 +180,8 @@ pub struct ExtensionStore { pub wasm_host: Arc, pub wasm_extensions: Vec<(Arc, WasmExtension)>, pub tasks: Vec>, + pub ssh_clients: HashMap>, + pub ssh_registered_tx: UnboundedSender<()>, } #[derive(Clone, Copy)] @@ -289,6 +293,7 @@ impl ExtensionStore { let index_path = extensions_dir.join("index.json"); let (reload_tx, mut reload_rx) = unbounded(); + let (connection_registered_tx, mut connection_registered_rx) = unbounded(); let mut this = Self { registration_hooks: extension_api.clone(), extension_index: Default::default(), @@ -312,6 +317,9 @@ impl ExtensionStore { telemetry, reload_tx, tasks: Vec::new(), + + ssh_clients: HashMap::default(), + ssh_registered_tx: connection_registered_tx, }; // The extensions store maintains an index file, which contains a complete @@ -386,6 +394,14 @@ impl ExtensionStore { .await; index_changed = false; } + + Self::update_ssh_clients(&this, &mut cx).await?; + } + _ = connection_registered_rx.next() => { + debounce_timer = cx + .background_executor() + .timer(RELOAD_DEBOUNCE_DURATION) + .fuse(); } extension_id = reload_rx.next() => { let Some(extension_id) = extension_id else { break; }; @@ -1431,6 +1447,144 @@ impl ExtensionStore { Ok(()) } + + fn prepare_remote_extension( + &mut self, + extension_id: Arc, + tmp_dir: PathBuf, + cx: &mut ModelContext, + ) -> Task> { + let src_dir = self.extensions_dir().join(extension_id.as_ref()); + let Some(loaded_extension) = self.extension_index.extensions.get(&extension_id).cloned() + else { + return Task::ready(Err(anyhow!("extension no longer installed"))); + }; + let fs = self.fs.clone(); + cx.background_executor().spawn(async move { + for well_known_path in ["extension.toml", "extension.json", "extension.wasm"] { + if fs.is_file(&src_dir.join(well_known_path)).await { + fs.copy_file( + &src_dir.join(well_known_path), + &tmp_dir.join(well_known_path), + fs::CopyOptions::default(), + ) + .await? + } + } + + for language_path in loaded_extension.manifest.languages.iter() { + if fs + .is_file(&src_dir.join(language_path).join("config.toml")) + .await + { + fs.create_dir(&tmp_dir.join(language_path)).await?; + fs.copy_file( + &src_dir.join(language_path).join("config.toml"), + &tmp_dir.join(language_path).join("config.toml"), + fs::CopyOptions::default(), + ) + .await? + } + } + + Ok(()) + }) + } + + async fn sync_extensions_over_ssh( + this: &WeakModel, + client: WeakModel, + cx: &mut AsyncAppContext, + ) -> Result<()> { + let extensions = this.update(cx, |this, _cx| { + this.extension_index + .extensions + .iter() + .filter_map(|(id, entry)| { + if entry.manifest.language_servers.is_empty() { + return None; + } + Some(proto::Extension { + id: id.to_string(), + version: entry.manifest.version.to_string(), + dev: entry.dev, + }) + }) + .collect() + })?; + + let response = client + .update(cx, |client, _cx| { + client + .proto_client() + .request(proto::SyncExtensions { extensions }) + })? + .await?; + + for missing_extension in response.missing_extensions.into_iter() { + let tmp_dir = tempfile::tempdir()?; + this.update(cx, |this, cx| { + this.prepare_remote_extension( + missing_extension.id.clone().into(), + tmp_dir.path().to_owned(), + cx, + ) + })? + .await?; + let dest_dir = PathBuf::from(&response.tmp_dir).join(missing_extension.clone().id); + log::info!("Uploading extension {}", missing_extension.clone().id); + + client + .update(cx, |client, cx| { + client.upload_directory(tmp_dir.path().to_owned(), dest_dir.clone(), cx) + })? + .await?; + + client + .update(cx, |client, _cx| { + client.proto_client().request(proto::InstallExtension { + tmp_dir: dest_dir.to_string_lossy().to_string(), + extension: Some(missing_extension), + }) + })? + .await?; + } + + anyhow::Ok(()) + } + + pub async fn update_ssh_clients( + this: &WeakModel, + cx: &mut AsyncAppContext, + ) -> Result<()> { + let clients = this.update(cx, |this, _cx| { + this.ssh_clients.retain(|_k, v| v.upgrade().is_some()); + this.ssh_clients.values().cloned().collect::>() + })?; + + for client in clients { + Self::sync_extensions_over_ssh(&this, client, cx) + .await + .log_err(); + } + + anyhow::Ok(()) + } + + pub fn register_ssh_client( + &mut self, + client: Model, + cx: &mut ModelContext, + ) { + let connection_options = client.read(cx).connection_options(); + if self.ssh_clients.contains_key(&connection_options.ssh_url()) { + return; + } + + self.ssh_clients + .insert(connection_options.ssh_url(), client.downgrade()); + self.ssh_registered_tx.unbounded_send(()).ok(); + } } fn load_plugin_queries(root_path: &Path) -> LanguageQueries { diff --git a/crates/extension_host/src/headless_host.rs b/crates/extension_host/src/headless_host.rs new file mode 100644 index 0000000000..e297794bf1 --- /dev/null +++ b/crates/extension_host/src/headless_host.rs @@ -0,0 +1,379 @@ +use std::{path::PathBuf, sync::Arc}; + +use anyhow::{anyhow, Context as _, Result}; +use client::{proto, TypedEnvelope}; +use collections::{HashMap, HashSet}; +use extension::{Extension, ExtensionManifest}; +use fs::{Fs, RemoveOptions, RenameOptions}; +use gpui::{AppContext, AsyncAppContext, Context, Model, ModelContext, Task, WeakModel}; +use http_client::HttpClient; +use language::{LanguageConfig, LanguageName, LanguageQueries, LanguageRegistry, LoadedLanguage}; +use lsp::LanguageServerName; +use node_runtime::NodeRuntime; + +use crate::{ + extension_lsp_adapter::ExtensionLspAdapter, + wasm_host::{WasmExtension, WasmHost}, + ExtensionRegistrationHooks, +}; + +pub struct HeadlessExtensionStore { + pub registration_hooks: Arc, + pub fs: Arc, + pub extension_dir: PathBuf, + pub wasm_host: Arc, + pub loaded_extensions: HashMap, Arc>, + pub loaded_languages: HashMap, Vec>, + pub loaded_language_servers: HashMap, Vec<(LanguageServerName, LanguageName)>>, +} + +#[derive(Clone, Debug)] +pub struct ExtensionVersion { + pub id: String, + pub version: String, + pub dev: bool, +} + +impl HeadlessExtensionStore { + pub fn new( + fs: Arc, + http_client: Arc, + languages: Arc, + extension_dir: PathBuf, + node_runtime: NodeRuntime, + cx: &mut AppContext, + ) -> Model { + let registration_hooks = Arc::new(HeadlessRegistrationHooks::new(languages.clone())); + cx.new_model(|cx| Self { + registration_hooks: registration_hooks.clone(), + fs: fs.clone(), + wasm_host: WasmHost::new( + fs.clone(), + http_client.clone(), + node_runtime, + registration_hooks, + extension_dir.join("work"), + cx, + ), + extension_dir, + loaded_extensions: Default::default(), + loaded_languages: Default::default(), + loaded_language_servers: Default::default(), + }) + } + + pub fn sync_extensions( + &mut self, + extensions: Vec, + cx: &ModelContext, + ) -> Task>> { + let on_client = HashSet::from_iter(extensions.iter().map(|e| e.id.as_str())); + let to_remove: Vec> = self + .loaded_extensions + .keys() + .filter(|id| !on_client.contains(id.as_ref())) + .cloned() + .collect(); + let to_load: Vec = extensions + .into_iter() + .filter(|e| { + if e.dev { + return true; + } + !self + .loaded_extensions + .get(e.id.as_str()) + .is_some_and(|loaded| loaded.as_ref() == e.version.as_str()) + }) + .collect(); + + cx.spawn(|this, mut cx| async move { + let mut missing = Vec::new(); + + for extension_id in to_remove { + log::info!("removing extension: {}", extension_id); + this.update(&mut cx, |this, cx| { + this.uninstall_extension(&extension_id, cx) + })? + .await?; + } + + for extension in to_load { + if let Err(e) = Self::load_extension(this.clone(), extension.clone(), &mut cx).await + { + log::info!("failed to load extension: {}, {:?}", extension.id, e); + missing.push(extension) + } else if extension.dev { + missing.push(extension) + } + } + + Ok(missing) + }) + } + + pub async fn load_extension( + this: WeakModel, + extension: ExtensionVersion, + cx: &mut AsyncAppContext, + ) -> Result<()> { + let (fs, wasm_host, extension_dir) = this.update(cx, |this, _cx| { + this.loaded_extensions.insert( + extension.id.clone().into(), + extension.version.clone().into(), + ); + ( + this.fs.clone(), + this.wasm_host.clone(), + this.extension_dir.join(&extension.id), + ) + })?; + + let manifest = Arc::new(ExtensionManifest::load(fs.clone(), &extension_dir).await?); + + debug_assert!(!manifest.languages.is_empty() || !manifest.language_servers.is_empty()); + + if manifest.version.as_ref() != extension.version.as_str() { + anyhow::bail!( + "mismatched versions: ({}) != ({})", + manifest.version, + extension.version + ) + } + + for language_path in &manifest.languages { + let language_path = extension_dir.join(language_path); + let config = fs.load(&language_path.join("config.toml")).await?; + let mut config = ::toml::from_str::(&config)?; + + this.update(cx, |this, _cx| { + this.loaded_languages + .entry(manifest.id.clone()) + .or_default() + .push(config.name.clone()); + + config.grammar = None; + + this.registration_hooks.register_language( + config.name.clone(), + None, + config.matcher.clone(), + Arc::new(move || { + Ok(LoadedLanguage { + config: config.clone(), + queries: LanguageQueries::default(), + context_provider: None, + toolchain_provider: None, + }) + }), + ); + })?; + } + + if manifest.language_servers.is_empty() { + return Ok(()); + } + + let wasm_extension: Arc = + Arc::new(WasmExtension::load(extension_dir, &manifest, wasm_host.clone(), &cx).await?); + + for (language_server_name, language_server_config) in &manifest.language_servers { + for language in language_server_config.languages() { + this.update(cx, |this, _cx| { + this.loaded_language_servers + .entry(manifest.id.clone()) + .or_default() + .push((language_server_name.clone(), language.clone())); + this.registration_hooks.register_lsp_adapter( + language.clone(), + ExtensionLspAdapter { + extension: wasm_extension.clone(), + language_server_id: language_server_name.clone(), + language_name: language, + }, + ); + })?; + } + } + + Ok(()) + } + + fn uninstall_extension( + &mut self, + extension_id: &Arc, + cx: &mut ModelContext, + ) -> Task> { + self.loaded_extensions.remove(extension_id); + let languages_to_remove = self + .loaded_languages + .remove(extension_id) + .unwrap_or_default(); + self.registration_hooks + .remove_languages(&languages_to_remove, &[]); + for (language_server_name, language) in self + .loaded_language_servers + .remove(extension_id) + .unwrap_or_default() + { + self.registration_hooks + .remove_lsp_adapter(&language, &language_server_name); + } + + let path = self.extension_dir.join(&extension_id.to_string()); + let fs = self.fs.clone(); + cx.spawn(|_, _| async move { + fs.remove_dir( + &path, + RemoveOptions { + recursive: true, + ignore_if_not_exists: true, + }, + ) + .await + }) + } + + pub fn install_extension( + &mut self, + extension: ExtensionVersion, + tmp_path: PathBuf, + cx: &mut ModelContext, + ) -> Task> { + let path = self.extension_dir.join(&extension.id); + let fs = self.fs.clone(); + + cx.spawn(|this, mut cx| async move { + if fs.is_dir(&path).await { + this.update(&mut cx, |this, cx| { + this.uninstall_extension(&extension.id.clone().into(), cx) + })? + .await?; + } + + fs.rename(&tmp_path, &path, RenameOptions::default()) + .await?; + + Self::load_extension(this, extension, &mut cx).await + }) + } + + pub async fn handle_sync_extensions( + extension_store: Model, + envelope: TypedEnvelope, + mut cx: AsyncAppContext, + ) -> Result { + let requested_extensions = + envelope + .payload + .extensions + .into_iter() + .map(|p| ExtensionVersion { + id: p.id, + version: p.version, + dev: p.dev, + }); + let missing_extensions = extension_store + .update(&mut cx, |extension_store, cx| { + extension_store.sync_extensions(requested_extensions.collect(), cx) + })? + .await?; + + Ok(proto::SyncExtensionsResponse { + missing_extensions: missing_extensions + .into_iter() + .map(|e| proto::Extension { + id: e.id, + version: e.version, + dev: e.dev, + }) + .collect(), + tmp_dir: paths::remote_extensions_uploads_dir() + .to_string_lossy() + .to_string(), + }) + } + + pub async fn handle_install_extension( + extensions: Model, + envelope: TypedEnvelope, + mut cx: AsyncAppContext, + ) -> Result { + let extension = envelope + .payload + .extension + .with_context(|| anyhow!("Invalid InstallExtension request"))?; + + extensions + .update(&mut cx, |extensions, cx| { + extensions.install_extension( + ExtensionVersion { + id: extension.id, + version: extension.version, + dev: extension.dev, + }, + PathBuf::from(envelope.payload.tmp_dir), + cx, + ) + })? + .await?; + + Ok(proto::Ack {}) + } +} + +struct HeadlessRegistrationHooks { + language_registry: Arc, +} + +impl HeadlessRegistrationHooks { + fn new(language_registry: Arc) -> Self { + Self { language_registry } + } +} + +impl ExtensionRegistrationHooks for HeadlessRegistrationHooks { + fn register_language( + &self, + language: LanguageName, + _grammar: Option>, + matcher: language::LanguageMatcher, + load: Arc Result + 'static + Send + Sync>, + ) { + log::info!("registering language: {:?}", language); + self.language_registry + .register_language(language, None, matcher, load) + } + fn register_lsp_adapter(&self, language: LanguageName, adapter: ExtensionLspAdapter) { + log::info!("registering lsp adapter {:?}", language); + self.language_registry + .register_lsp_adapter(language, Arc::new(adapter) as _); + } + + fn register_wasm_grammars(&self, grammars: Vec<(Arc, PathBuf)>) { + self.language_registry.register_wasm_grammars(grammars) + } + + fn remove_lsp_adapter(&self, language: &LanguageName, server_name: &LanguageServerName) { + self.language_registry + .remove_lsp_adapter(language, server_name) + } + + fn remove_languages( + &self, + languages_to_remove: &[LanguageName], + _grammars_to_remove: &[Arc], + ) { + self.language_registry + .remove_languages(languages_to_remove, &[]) + } + + fn update_lsp_status( + &self, + server_name: LanguageServerName, + status: language::LanguageServerBinaryStatus, + ) { + self.language_registry + .update_lsp_status(server_name, status) + } +} diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index dcd62751a7..b9540238f9 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -295,9 +295,13 @@ message Envelope { GetPanicFilesResponse get_panic_files_response = 281; CancelLanguageServerWork cancel_language_server_work = 282; - + LspExtOpenDocs lsp_ext_open_docs = 283; - LspExtOpenDocsResponse lsp_ext_open_docs_response = 284; // current max + LspExtOpenDocsResponse lsp_ext_open_docs_response = 284; + + SyncExtensions sync_extensions = 285; + SyncExtensionsResponse sync_extensions_response = 286; + InstallExtension install_extension = 287; // current max } reserved 87 to 88; @@ -2544,3 +2548,23 @@ message CancelLanguageServerWork { optional string token = 2; } } + +message Extension { + string id = 1; + string version = 2; + bool dev = 3; +} + +message SyncExtensions { + repeated Extension extensions = 1; +} + +message SyncExtensionsResponse { + string tmp_dir = 1; + repeated Extension missing_extensions = 2; +} + +message InstallExtension { + Extension extension = 1; + string tmp_dir = 2; +} diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 2ec9f8bf55..0810a561b9 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -368,6 +368,9 @@ messages!( (GetPanicFiles, Background), (GetPanicFilesResponse, Background), (CancelLanguageServerWork, Foreground), + (SyncExtensions, Background), + (SyncExtensionsResponse, Background), + (InstallExtension, Background), ); request_messages!( @@ -491,6 +494,8 @@ request_messages!( (GetPathMetadata, GetPathMetadataResponse), (GetPanicFiles, GetPanicFilesResponse), (CancelLanguageServerWork, Ack), + (SyncExtensions, SyncExtensionsResponse), + (InstallExtension, Ack), ); entity_messages!( diff --git a/crates/recent_projects/Cargo.toml b/crates/recent_projects/Cargo.toml index 827afff7c0..336ced57a8 100644 --- a/crates/recent_projects/Cargo.toml +++ b/crates/recent_projects/Cargo.toml @@ -17,6 +17,7 @@ anyhow.workspace = true auto_update.workspace = true release_channel.workspace = true editor.workspace = true +extension_host.workspace = true file_finder.workspace = true futures.workspace = true fuzzy.workspace = true diff --git a/crates/recent_projects/src/ssh_connections.rs b/crates/recent_projects/src/ssh_connections.rs index e70b68d374..a9aeacadd8 100644 --- a/crates/recent_projects/src/ssh_connections.rs +++ b/crates/recent_projects/src/ssh_connections.rs @@ -4,6 +4,7 @@ use std::{path::PathBuf, sync::Arc, time::Duration}; use anyhow::{anyhow, Result}; use auto_update::AutoUpdater; use editor::Editor; +use extension_host::ExtensionStore; use futures::channel::oneshot; use gpui::{ percentage, Animation, AnimationExt, AnyWindowHandle, AsyncAppContext, DismissEvent, @@ -630,6 +631,15 @@ pub async fn open_ssh_project( } } + window + .update(cx, |workspace, cx| { + if let Some(client) = workspace.project().read(cx).ssh_client().clone() { + ExtensionStore::global(cx) + .update(cx, |store, cx| store.register_ssh_client(client, cx)); + } + }) + .ok(); + break; } diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index 87a58cb050..d8c852c019 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -1269,6 +1269,7 @@ impl RemoteConnection for SshRemoteConnection { .map(|port| vec!["-P".to_string(), port.to_string()]) .unwrap_or_default(), ) + .arg("-C") .arg("-r") .arg(&src_path) .arg(format!( diff --git a/crates/remote_server/Cargo.toml b/crates/remote_server/Cargo.toml index 73e52895df..d46fb8df56 100644 --- a/crates/remote_server/Cargo.toml +++ b/crates/remote_server/Cargo.toml @@ -29,6 +29,7 @@ chrono.workspace = true clap.workspace = true client.workspace = true env_logger.workspace = true +extension_host.workspace = true fs.workspace = true futures.workspace = true git.workspace = true diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 74416f6ed9..28cd6e115c 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -1,4 +1,5 @@ use anyhow::{anyhow, Result}; +use extension_host::headless_host::HeadlessExtensionStore; use fs::Fs; use gpui::{AppContext, AsyncAppContext, Context as _, Model, ModelContext, PromptLevel}; use http_client::HttpClient; @@ -37,6 +38,7 @@ pub struct HeadlessProject { pub settings_observer: Model, pub next_entry_id: Arc, pub languages: Arc, + pub extensions: Model, } pub struct HeadlessAppState { @@ -147,6 +149,15 @@ impl HeadlessProject { ) .detach(); + let extensions = HeadlessExtensionStore::new( + fs.clone(), + http_client.clone(), + languages.clone(), + paths::remote_extensions_dir().to_path_buf(), + node_runtime, + cx, + ); + let client: AnyProtoClient = session.clone().into(); session.subscribe_to_entity(SSH_PROJECT_ID, &worktree_store); @@ -173,6 +184,15 @@ impl HeadlessProject { client.add_model_request_handler(BufferStore::handle_update_buffer); client.add_model_message_handler(BufferStore::handle_close_buffer); + client.add_request_handler( + extensions.clone().downgrade(), + HeadlessExtensionStore::handle_sync_extensions, + ); + client.add_request_handler( + extensions.clone().downgrade(), + HeadlessExtensionStore::handle_install_extension, + ); + BufferStore::init(&client); WorktreeStore::init(&client); SettingsObserver::init(&client); @@ -190,6 +210,7 @@ impl HeadlessProject { task_store, next_entry_id: Default::default(), languages, + extensions, } } From 37a59d6b2e22e76d1c3c86df9f3ae7c1e8633dee Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 20 Nov 2024 19:21:22 -0700 Subject: [PATCH 30/68] vim: Fix : on welcome screen (#20937) Release Notes: - vim: Fixed `:` on the welcome screen --- assets/keymaps/vim.json | 2 +- crates/welcome/src/welcome.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 10b2009511..1be3e8c9c1 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -577,7 +577,7 @@ } }, { - "context": "EmptyPane || SharedScreen || MarkdownPreview || KeyContextView", + "context": "EmptyPane || SharedScreen || MarkdownPreview || KeyContextView || Welcome", "use_layout_keys": true, "bindings": { ":": "command_palette::Toggle", diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index 89f12aa37e..0d1e1c24d1 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -73,6 +73,7 @@ impl Render for WelcomePage { h_flex() .size_full() .bg(cx.theme().colors().editor_background) + .key_context("Welcome") .track_focus(&self.focus_handle(cx)) .child( v_flex() From e062f30d9ea264662137a96f7d769deb8af8670e Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 20 Nov 2024 20:29:47 -0700 Subject: [PATCH 31/68] Rename ime_key -> key_char and update behavior (#20953) As part of the recent changes to keyboard support, ime_key is no longer populated by the IME; but instead by the keyboard. As part of #20877 I changed some code to assume that falling back to key was ok, but this was not ok; instead we need to populate this more similarly to how it was done before #20336. The alternative fix could be to instead of simulating these events in our own code to push a fake native event back to the platform input handler. Closes #ISSUE Release Notes: - Fixed a bug where tapping `shift` coudl type "shift" if you had a binding on "shift shift" --- crates/gpui/examples/input.rs | 4 +- crates/gpui/src/platform/keystroke.rs | 37 ++++++++++--------- crates/gpui/src/platform/linux/platform.rs | 6 +-- .../gpui/src/platform/linux/wayland/client.rs | 6 +-- .../gpui/src/platform/linux/wayland/window.rs | 4 +- crates/gpui/src/platform/linux/x11/client.rs | 16 ++++---- crates/gpui/src/platform/linux/x11/window.rs | 4 +- crates/gpui/src/platform/mac/events.rs | 26 +++++++------ crates/gpui/src/platform/mac/window.rs | 21 +++++------ crates/gpui/src/platform/windows/events.rs | 14 +++---- crates/gpui/src/window.rs | 12 ++---- .../markdown_preview/src/markdown_renderer.rs | 2 +- crates/terminal/src/mappings/keys.rs | 2 +- crates/vim/src/digraph.rs | 2 +- 14 files changed, 77 insertions(+), 79 deletions(-) diff --git a/crates/gpui/examples/input.rs b/crates/gpui/examples/input.rs index d52697c43f..29014946cb 100644 --- a/crates/gpui/examples/input.rs +++ b/crates/gpui/examples/input.rs @@ -581,8 +581,8 @@ impl Render for InputExample { format!( "{:} {}", ks.unparse(), - if let Some(ime_key) = ks.ime_key.as_ref() { - format!("-> {:?}", ime_key) + if let Some(key_char) = ks.key_char.as_ref() { + format!("-> {:?}", key_char) } else { "".to_owned() } diff --git a/crates/gpui/src/platform/keystroke.rs b/crates/gpui/src/platform/keystroke.rs index 20a12a691b..af1e5179db 100644 --- a/crates/gpui/src/platform/keystroke.rs +++ b/crates/gpui/src/platform/keystroke.rs @@ -12,14 +12,15 @@ pub struct Keystroke { /// e.g. for option-s, key is "s" pub key: String, - /// ime_key is the character inserted by the IME engine when that key was pressed. - /// e.g. for option-s, ime_key is "ß" - pub ime_key: Option, + /// key_char is the character that could have been typed when + /// this binding was pressed. + /// e.g. for s this is "s", for option-s "ß", and cmd-s None + pub key_char: Option, } impl Keystroke { /// When matching a key we cannot know whether the user intended to type - /// the ime_key or the key itself. On some non-US keyboards keys we use in our + /// the key_char or the key itself. On some non-US keyboards keys we use in our /// bindings are behind option (for example `$` is typed `alt-ç` on a Czech keyboard), /// and on some keyboards the IME handler converts a sequence of keys into a /// specific character (for example `"` is typed as `" space` on a brazilian keyboard). @@ -27,10 +28,10 @@ impl Keystroke { /// This method assumes that `self` was typed and `target' is in the keymap, and checks /// both possibilities for self against the target. pub(crate) fn should_match(&self, target: &Keystroke) -> bool { - if let Some(ime_key) = self - .ime_key + if let Some(key_char) = self + .key_char .as_ref() - .filter(|ime_key| ime_key != &&self.key) + .filter(|key_char| key_char != &&self.key) { let ime_modifiers = Modifiers { control: self.modifiers.control, @@ -38,7 +39,7 @@ impl Keystroke { ..Default::default() }; - if &target.key == ime_key && target.modifiers == ime_modifiers { + if &target.key == key_char && target.modifiers == ime_modifiers { return true; } } @@ -47,9 +48,9 @@ impl Keystroke { } /// key syntax is: - /// [ctrl-][alt-][shift-][cmd-][fn-]key[->ime_key] - /// ime_key syntax is only used for generating test events, - /// when matching a key with an ime_key set will be matched without it. + /// [ctrl-][alt-][shift-][cmd-][fn-]key[->key_char] + /// key_char syntax is only used for generating test events, + /// when matching a key with an key_char set will be matched without it. pub fn parse(source: &str) -> anyhow::Result { let mut control = false; let mut alt = false; @@ -57,7 +58,7 @@ impl Keystroke { let mut platform = false; let mut function = false; let mut key = None; - let mut ime_key = None; + let mut key_char = None; let mut components = source.split('-').peekable(); while let Some(component) = components.next() { @@ -74,7 +75,7 @@ impl Keystroke { break; } else if next.len() > 1 && next.starts_with('>') { key = Some(String::from(component)); - ime_key = Some(String::from(&next[1..])); + key_char = Some(String::from(&next[1..])); components.next(); } else { return Err(anyhow!("Invalid keystroke `{}`", source)); @@ -118,7 +119,7 @@ impl Keystroke { function, }, key, - ime_key, + key_char: key_char, }) } @@ -154,7 +155,7 @@ impl Keystroke { /// Returns true if this keystroke left /// the ime system in an incomplete state. pub fn is_ime_in_progress(&self) -> bool { - self.ime_key.is_none() + self.key_char.is_none() && (is_printable_key(&self.key) || self.key.is_empty()) && !(self.modifiers.platform || self.modifiers.control @@ -162,17 +163,17 @@ impl Keystroke { || self.modifiers.alt) } - /// Returns a new keystroke with the ime_key filled. + /// Returns a new keystroke with the key_char filled. /// This is used for dispatch_keystroke where we want users to /// be able to simulate typing "space", etc. pub fn with_simulated_ime(mut self) -> Self { - if self.ime_key.is_none() + if self.key_char.is_none() && !self.modifiers.platform && !self.modifiers.control && !self.modifiers.function && !self.modifiers.alt { - self.ime_key = match self.key.as_str() { + self.key_char = match self.key.as_str() { "space" => Some(" ".into()), "tab" => Some("\t".into()), "enter" => Some("\n".into()), diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index f778ebc074..650ed70af8 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -742,14 +742,14 @@ impl Keystroke { } } - // Ignore control characters (and DEL) for the purposes of ime_key - let ime_key = + // Ignore control characters (and DEL) for the purposes of key_char + let key_char = (key_utf32 >= 32 && key_utf32 != 127 && !key_utf8.is_empty()).then_some(key_utf8); Keystroke { modifiers, key, - ime_key, + key_char, } } diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/crates/gpui/src/platform/linux/wayland/client.rs index ab87bb2024..e193201957 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/crates/gpui/src/platform/linux/wayland/client.rs @@ -1208,7 +1208,7 @@ impl Dispatch for WaylandClientStatePtr { compose.feed(keysym); match compose.status() { xkb::Status::Composing => { - keystroke.ime_key = None; + keystroke.key_char = None; state.pre_edit_text = compose.utf8().or(Keystroke::underlying_dead_key(keysym)); let pre_edit = @@ -1220,7 +1220,7 @@ impl Dispatch for WaylandClientStatePtr { xkb::Status::Composed => { state.pre_edit_text.take(); - keystroke.ime_key = compose.utf8(); + keystroke.key_char = compose.utf8(); if let Some(keysym) = compose.keysym() { keystroke.key = xkb::keysym_get_name(keysym); } @@ -1340,7 +1340,7 @@ impl Dispatch for WaylandClientStatePtr { keystroke: Keystroke { modifiers: Modifiers::default(), key: commit_text.clone(), - ime_key: Some(commit_text), + key_char: Some(commit_text), }, is_held: false, })); diff --git a/crates/gpui/src/platform/linux/wayland/window.rs b/crates/gpui/src/platform/linux/wayland/window.rs index 8d4516b3f3..55ba4f6004 100644 --- a/crates/gpui/src/platform/linux/wayland/window.rs +++ b/crates/gpui/src/platform/linux/wayland/window.rs @@ -687,11 +687,11 @@ impl WaylandWindowStatePtr { } } if let PlatformInput::KeyDown(event) = input { - if let Some(ime_key) = &event.keystroke.ime_key { + if let Some(key_char) = &event.keystroke.key_char { let mut state = self.state.borrow_mut(); if let Some(mut input_handler) = state.input_handler.take() { drop(state); - input_handler.replace_text_in_range(None, ime_key); + input_handler.replace_text_in_range(None, key_char); self.state.borrow_mut().input_handler = Some(input_handler); } } diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index 82ef39fc6b..f6c3af0348 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -178,7 +178,7 @@ pub struct X11ClientState { pub(crate) compose_state: Option, pub(crate) pre_edit_text: Option, pub(crate) composing: bool, - pub(crate) pre_ime_key_down: Option, + pub(crate) pre_key_char_down: Option, pub(crate) cursor_handle: cursor::Handle, pub(crate) cursor_styles: HashMap, pub(crate) cursor_cache: HashMap, @@ -446,7 +446,7 @@ impl X11Client { compose_state, pre_edit_text: None, - pre_ime_key_down: None, + pre_key_char_down: None, composing: false, cursor_handle, @@ -858,7 +858,7 @@ impl X11Client { let modifiers = modifiers_from_state(event.state); state.modifiers = modifiers; - state.pre_ime_key_down.take(); + state.pre_key_char_down.take(); let keystroke = { let code = event.detail.into(); let xkb_state = state.previous_xkb_state.clone(); @@ -880,13 +880,13 @@ impl X11Client { match compose_state.status() { xkbc::Status::Composed => { state.pre_edit_text.take(); - keystroke.ime_key = compose_state.utf8(); + keystroke.key_char = compose_state.utf8(); if let Some(keysym) = compose_state.keysym() { keystroke.key = xkbc::keysym_get_name(keysym); } } xkbc::Status::Composing => { - keystroke.ime_key = None; + keystroke.key_char = None; state.pre_edit_text = compose_state .utf8() .or(crate::Keystroke::underlying_dead_key(keysym)); @@ -1156,7 +1156,7 @@ impl X11Client { match event { Event::KeyPress(event) | Event::KeyRelease(event) => { let mut state = self.0.borrow_mut(); - state.pre_ime_key_down = Some(Keystroke::from_xkb( + state.pre_key_char_down = Some(Keystroke::from_xkb( &state.xkb, state.modifiers, event.detail.into(), @@ -1187,11 +1187,11 @@ impl X11Client { fn xim_handle_commit(&self, window: xproto::Window, text: String) -> Option<()> { let window = self.get_window(window).unwrap(); let mut state = self.0.borrow_mut(); - let keystroke = state.pre_ime_key_down.take(); + let keystroke = state.pre_key_char_down.take(); state.composing = false; drop(state); if let Some(mut keystroke) = keystroke { - keystroke.ime_key = Some(text.clone()); + keystroke.key_char = Some(text.clone()); window.handle_input(PlatformInput::KeyDown(crate::KeyDownEvent { keystroke, is_held: false, diff --git a/crates/gpui/src/platform/linux/x11/window.rs b/crates/gpui/src/platform/linux/x11/window.rs index 15712233c2..4df1b50f3f 100644 --- a/crates/gpui/src/platform/linux/x11/window.rs +++ b/crates/gpui/src/platform/linux/x11/window.rs @@ -846,9 +846,9 @@ impl X11WindowStatePtr { if let PlatformInput::KeyDown(event) = input { let mut state = self.state.borrow_mut(); if let Some(mut input_handler) = state.input_handler.take() { - if let Some(ime_key) = &event.keystroke.ime_key { + if let Some(key_char) = &event.keystroke.key_char { drop(state); - input_handler.replace_text_in_range(None, ime_key); + input_handler.replace_text_in_range(None, key_char); state = self.state.borrow_mut(); } state.input_handler = Some(input_handler); diff --git a/crates/gpui/src/platform/mac/events.rs b/crates/gpui/src/platform/mac/events.rs index 51716cccb4..f715dba562 100644 --- a/crates/gpui/src/platform/mac/events.rs +++ b/crates/gpui/src/platform/mac/events.rs @@ -245,7 +245,7 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke { .charactersIgnoringModifiers() .to_str() .to_string(); - let mut ime_key = None; + let mut key_char = None; let first_char = characters.chars().next().map(|ch| ch as u16); let modifiers = native_event.modifierFlags(); @@ -261,13 +261,19 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke { #[allow(non_upper_case_globals)] let key = match first_char { Some(SPACE_KEY) => { - ime_key = Some(" ".to_string()); + key_char = Some(" ".to_string()); "space".to_string() } + Some(TAB_KEY) => { + key_char = Some("\t".to_string()); + "tab".to_string() + } + Some(ENTER_KEY) | Some(NUMPAD_ENTER_KEY) => { + key_char = Some("\n".to_string()); + "enter".to_string() + } Some(BACKSPACE_KEY) => "backspace".to_string(), - Some(ENTER_KEY) | Some(NUMPAD_ENTER_KEY) => "enter".to_string(), Some(ESCAPE_KEY) => "escape".to_string(), - Some(TAB_KEY) => "tab".to_string(), Some(SHIFT_TAB_KEY) => "tab".to_string(), Some(NSUpArrowFunctionKey) => "up".to_string(), Some(NSDownArrowFunctionKey) => "down".to_string(), @@ -348,7 +354,7 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke { chars_ignoring_modifiers }; - if always_use_cmd_layout || alt { + if !control && !command && !function { let mut mods = NO_MOD; if shift { mods |= SHIFT_MOD; @@ -356,11 +362,9 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke { if alt { mods |= OPTION_MOD; } - let alt_key = chars_for_modified_key(native_event.keyCode(), mods); - if alt_key != key { - ime_key = Some(alt_key); - } - }; + + key_char = Some(chars_for_modified_key(native_event.keyCode(), mods)); + } key } @@ -375,7 +379,7 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke { function, }, key, - ime_key, + key_char, } } diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index e5a04191a3..abb532980a 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -1283,18 +1283,17 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent: } if event.is_held { - let handled = with_input_handler(&this, |input_handler| { - if !input_handler.apple_press_and_hold_enabled() { - input_handler.replace_text_in_range( - None, - &event.keystroke.ime_key.unwrap_or(event.keystroke.key), - ); + if let Some(key_char) = event.keystroke.key_char.as_ref() { + let handled = with_input_handler(&this, |input_handler| { + if !input_handler.apple_press_and_hold_enabled() { + input_handler.replace_text_in_range(None, &key_char); + return YES; + } + NO + }); + if handled == Some(YES) { return YES; } - NO - }); - if handled == Some(YES) { - return YES; } } @@ -1437,7 +1436,7 @@ extern "C" fn cancel_operation(this: &Object, _sel: Sel, _sender: id) { let keystroke = Keystroke { modifiers: Default::default(), key: ".".into(), - ime_key: None, + key_char: None, }; let event = PlatformInput::KeyDown(KeyDownEvent { keystroke: keystroke.clone(), diff --git a/crates/gpui/src/platform/windows/events.rs b/crates/gpui/src/platform/windows/events.rs index 92adf6c7cb..5f45d260d9 100644 --- a/crates/gpui/src/platform/windows/events.rs +++ b/crates/gpui/src/platform/windows/events.rs @@ -386,7 +386,7 @@ fn handle_char_msg( return Some(1); }; drop(lock); - let ime_key = keystroke.ime_key.clone(); + let key_char = keystroke.key_char.clone(); let event = KeyDownEvent { keystroke, is_held: lparam.0 & (0x1 << 30) > 0, @@ -397,7 +397,7 @@ fn handle_char_msg( if dispatch_event_result.default_prevented || !dispatch_event_result.propagate { return Some(0); } - let Some(ime_char) = ime_key else { + let Some(ime_char) = key_char else { return Some(1); }; with_input_handler(&state_ptr, |input_handler| { @@ -1172,7 +1172,7 @@ fn parse_syskeydown_msg_keystroke(wparam: WPARAM) -> Option { Some(Keystroke { modifiers, key, - ime_key: None, + key_char: None, }) } @@ -1220,7 +1220,7 @@ fn parse_keydown_msg_keystroke(wparam: WPARAM) -> Option { return Some(KeystrokeOrModifier::Keystroke(Keystroke { modifiers, key: format!("f{}", offset + 1), - ime_key: None, + key_char: None, })); }; return None; @@ -1231,7 +1231,7 @@ fn parse_keydown_msg_keystroke(wparam: WPARAM) -> Option { Some(KeystrokeOrModifier::Keystroke(Keystroke { modifiers, key, - ime_key: None, + key_char: None, })) } @@ -1253,7 +1253,7 @@ fn parse_char_msg_keystroke(wparam: WPARAM) -> Option { Some(Keystroke { modifiers, key, - ime_key: Some(first_char.to_string()), + key_char: Some(first_char.to_string()), }) } } @@ -1327,7 +1327,7 @@ fn basic_vkcode_to_string(code: u16, modifiers: Modifiers) -> Option Some(Keystroke { modifiers, key, - ime_key: None, + key_char: None, }) } diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index ec1fd601ec..e4fa74f981 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -3038,7 +3038,7 @@ impl<'a> WindowContext<'a> { return true; } - if let Some(input) = keystroke.with_simulated_ime().ime_key { + if let Some(input) = keystroke.key_char { if let Some(mut input_handler) = self.window.platform_window.take_input_handler() { input_handler.dispatch_input(&input, self); self.window.platform_window.set_input_handler(input_handler); @@ -3267,7 +3267,7 @@ impl<'a> WindowContext<'a> { if let Some(key) = key { keystroke = Some(Keystroke { key: key.to_string(), - ime_key: None, + key_char: None, modifiers: Modifiers::default(), }); } @@ -3482,13 +3482,7 @@ impl<'a> WindowContext<'a> { if !self.propagate_event { continue 'replay; } - if let Some(input) = replay - .keystroke - .with_simulated_ime() - .ime_key - .as_ref() - .cloned() - { + if let Some(input) = replay.keystroke.key_char.as_ref().cloned() { if let Some(mut input_handler) = self.window.platform_window.take_input_handler() { input_handler.dispatch_input(&input, self); self.window.platform_window.set_input_handler(input_handler) diff --git a/crates/markdown_preview/src/markdown_renderer.rs b/crates/markdown_preview/src/markdown_renderer.rs index f38e1c49b5..37ca5636a6 100644 --- a/crates/markdown_preview/src/markdown_renderer.rs +++ b/crates/markdown_preview/src/markdown_renderer.rs @@ -206,7 +206,7 @@ fn render_markdown_list_item( let secondary_modifier = Keystroke { key: "".to_string(), modifiers: Modifiers::secondary_key(), - ime_key: None, + key_char: None, }; Tooltip::text( format!("{}-click to toggle the checkbox", secondary_modifier), diff --git a/crates/terminal/src/mappings/keys.rs b/crates/terminal/src/mappings/keys.rs index 2d4fe4c62e..1efc1f17d2 100644 --- a/crates/terminal/src/mappings/keys.rs +++ b/crates/terminal/src/mappings/keys.rs @@ -343,7 +343,7 @@ mod test { function: false, }, key: "🖖🏻".to_string(), //2 char string - ime_key: None, + key_char: None, }; assert_eq!(to_esc_str(&ks, &TermMode::NONE, false), None); } diff --git a/crates/vim/src/digraph.rs b/crates/vim/src/digraph.rs index 4c09dd3e33..dcccc8b5cd 100644 --- a/crates/vim/src/digraph.rs +++ b/crates/vim/src/digraph.rs @@ -83,7 +83,7 @@ impl Vim { cx: &mut ViewContext, ) { // handled by handle_literal_input - if keystroke_event.keystroke.ime_key.is_some() { + if keystroke_event.keystroke.key_char.is_some() { return; }; From 7285cdb95541c5b287311bfd9a4ec84bf9d88a10 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 20 Nov 2024 21:24:31 -0700 Subject: [PATCH 32/68] Drop platform lock when setting menu (#20962) Turns out setting the menu (sometimes) calls `selected_range` on the input handler. https://zed-industries.slack.com/archives/C04S6T1T7TQ/p1732160078058279 Release Notes: - Fixed a panic when reloading keymaps --- crates/gpui/src/platform/mac/platform.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index faf9329734..28f427af1b 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -844,7 +844,9 @@ impl Platform for MacPlatform { let app: id = msg_send![APP_CLASS, sharedApplication]; let mut state = self.0.lock(); let actions = &mut state.menu_actions; - app.setMainMenu_(self.create_menu_bar(menus, NSWindow::delegate(app), actions, keymap)); + let menu = self.create_menu_bar(menus, NSWindow::delegate(app), actions, keymap); + drop(state); + app.setMainMenu_(menu); } } From ebaa270bafbd8b4ca504436c5178c68dc81618e7 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 20 Nov 2024 22:04:26 -0700 Subject: [PATCH 33/68] Clip UTF-16 offsets in text for range (#20968) When launching the Pinyin keyboard, macOS will sometimes try to peek one character back in the string. This caused a panic if the preceding character was an emoji. The docs say "don't assume the range is valid", so now we don't. Release Notes: - (macOS) Fixed a panic when using the Pinyin keyboard with emojis --- crates/editor/src/editor.rs | 15 +++++---- crates/gpui/examples/input.rs | 35 +++++++++++++++++++- crates/gpui/src/input.rs | 14 +++++--- crates/gpui/src/platform.rs | 10 ++++-- crates/gpui/src/platform/mac/window.rs | 11 ++++-- crates/terminal_view/src/terminal_element.rs | 1 + 6 files changed, 70 insertions(+), 16 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 1435681587..cc450c573f 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -14428,15 +14428,16 @@ impl ViewInputHandler for Editor { fn text_for_range( &mut self, range_utf16: Range, + adjusted_range: &mut Option>, cx: &mut ViewContext, ) -> Option { - Some( - self.buffer - .read(cx) - .read(cx) - .text_for_range(OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end)) - .collect(), - ) + let snapshot = self.buffer.read(cx).read(cx); + let start = snapshot.clip_offset_utf16(OffsetUtf16(range_utf16.start), Bias::Left); + let end = snapshot.clip_offset_utf16(OffsetUtf16(range_utf16.end), Bias::Right); + if (start.0..end.0) != range_utf16 { + adjusted_range.replace(start.0..end.0); + } + Some(snapshot.text_for_range(start..end).collect()) } fn selected_text_range( diff --git a/crates/gpui/examples/input.rs b/crates/gpui/examples/input.rs index 29014946cb..1a49688a8f 100644 --- a/crates/gpui/examples/input.rs +++ b/crates/gpui/examples/input.rs @@ -15,7 +15,10 @@ actions!( SelectAll, Home, End, - ShowCharacterPalette + ShowCharacterPalette, + Paste, + Cut, + Copy, ] ); @@ -107,6 +110,28 @@ impl TextInput { cx.show_character_palette(); } + fn paste(&mut self, _: &Paste, cx: &mut ViewContext) { + if let Some(text) = cx.read_from_clipboard().and_then(|item| item.text()) { + self.replace_text_in_range(None, &text.replace("\n", " "), cx); + } + } + + fn copy(&mut self, _: &Copy, cx: &mut ViewContext) { + if !self.selected_range.is_empty() { + cx.write_to_clipboard(ClipboardItem::new_string( + (&self.content[self.selected_range.clone()]).to_string(), + )); + } + } + fn cut(&mut self, _: &Copy, cx: &mut ViewContext) { + if !self.selected_range.is_empty() { + cx.write_to_clipboard(ClipboardItem::new_string( + (&self.content[self.selected_range.clone()]).to_string(), + )); + self.replace_text_in_range(None, "", cx) + } + } + fn move_to(&mut self, offset: usize, cx: &mut ViewContext) { self.selected_range = offset..offset; cx.notify() @@ -219,9 +244,11 @@ impl ViewInputHandler for TextInput { fn text_for_range( &mut self, range_utf16: Range, + actual_range: &mut Option>, _cx: &mut ViewContext, ) -> Option { let range = self.range_from_utf16(&range_utf16); + actual_range.replace(self.range_to_utf16(&range)); Some(self.content[range].to_string()) } @@ -497,6 +524,9 @@ impl Render for TextInput { .on_action(cx.listener(Self::home)) .on_action(cx.listener(Self::end)) .on_action(cx.listener(Self::show_character_palette)) + .on_action(cx.listener(Self::paste)) + .on_action(cx.listener(Self::cut)) + .on_action(cx.listener(Self::copy)) .on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down)) .on_mouse_up(MouseButton::Left, cx.listener(Self::on_mouse_up)) .on_mouse_up_out(MouseButton::Left, cx.listener(Self::on_mouse_up)) @@ -602,6 +632,9 @@ fn main() { KeyBinding::new("shift-left", SelectLeft, None), KeyBinding::new("shift-right", SelectRight, None), KeyBinding::new("cmd-a", SelectAll, None), + KeyBinding::new("cmd-v", Paste, None), + KeyBinding::new("cmd-c", Copy, None), + KeyBinding::new("cmd-x", Cut, None), KeyBinding::new("home", Home, None), KeyBinding::new("end", End, None), KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, None), diff --git a/crates/gpui/src/input.rs b/crates/gpui/src/input.rs index 161401ecc6..2fb27ac7fc 100644 --- a/crates/gpui/src/input.rs +++ b/crates/gpui/src/input.rs @@ -9,8 +9,12 @@ use std::ops::Range; /// See [`InputHandler`] for details on how to implement each method. pub trait ViewInputHandler: 'static + Sized { /// See [`InputHandler::text_for_range`] for details - fn text_for_range(&mut self, range: Range, cx: &mut ViewContext) - -> Option; + fn text_for_range( + &mut self, + range: Range, + adjusted_range: &mut Option>, + cx: &mut ViewContext, + ) -> Option; /// See [`InputHandler::selected_text_range`] for details fn selected_text_range( @@ -89,10 +93,12 @@ impl InputHandler for ElementInputHandler { fn text_for_range( &mut self, range_utf16: Range, + adjusted_range: &mut Option>, cx: &mut WindowContext, ) -> Option { - self.view - .update(cx, |view, cx| view.text_for_range(range_utf16, cx)) + self.view.update(cx, |view, cx| { + view.text_for_range(range_utf16, adjusted_range, cx) + }) } fn replace_text_in_range( diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index d9016afb68..76a575724f 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -643,9 +643,13 @@ impl PlatformInputHandler { } #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))] - fn text_for_range(&mut self, range_utf16: Range) -> Option { + fn text_for_range( + &mut self, + range_utf16: Range, + adjusted: &mut Option>, + ) -> Option { self.cx - .update(|cx| self.handler.text_for_range(range_utf16, cx)) + .update(|cx| self.handler.text_for_range(range_utf16, adjusted, cx)) .ok() .flatten() } @@ -712,6 +716,7 @@ impl PlatformInputHandler { /// A struct representing a selection in a text buffer, in UTF16 characters. /// This is different from a range because the head may be before the tail. +#[derive(Debug)] pub struct UTF16Selection { /// The range of text in the document this selection corresponds to /// in UTF16 characters. @@ -749,6 +754,7 @@ pub trait InputHandler: 'static { fn text_for_range( &mut self, range_utf16: Range, + adjusted_range: &mut Option>, cx: &mut WindowContext, ) -> Option; diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index abb532980a..ce9a4c05bf 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -38,6 +38,7 @@ use std::{ cell::Cell, ffi::{c_void, CStr}, mem, + ops::Range, path::PathBuf, ptr::{self, NonNull}, rc::Rc, @@ -1754,15 +1755,21 @@ extern "C" fn attributed_substring_for_proposed_range( this: &Object, _: Sel, range: NSRange, - _actual_range: *mut c_void, + actual_range: *mut c_void, ) -> id { with_input_handler(this, |input_handler| { let range = range.to_range()?; if range.is_empty() { return None; } + let mut adjusted: Option> = None; - let selected_text = input_handler.text_for_range(range.clone())?; + let selected_text = input_handler.text_for_range(range.clone(), &mut adjusted)?; + if let Some(adjusted) = adjusted { + if adjusted != range { + unsafe { (actual_range as *mut NSRange).write(NSRange::from(adjusted)) }; + } + } unsafe { let string: id = msg_send![class!(NSAttributedString), alloc]; let string: id = msg_send![string, initWithString: ns_string(&selected_text)]; diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index bc4f58a5ef..9d5eb7d410 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -1001,6 +1001,7 @@ impl InputHandler for TerminalInputHandler { fn text_for_range( &mut self, _: std::ops::Range, + _: &mut Option>, _: &mut WindowContext, ) -> Option { None From 6ab4b469845184770b19cf2271b30c51f3823cba Mon Sep 17 00:00:00 2001 From: Adam Richardson <38476863+AdamWRichardson@users.noreply.github.com> Date: Thu, 21 Nov 2024 07:48:13 +0000 Subject: [PATCH 34/68] rope: Minor optimization for tab indices (#20911) This is a follow up on https://github.com/zed-industries/zed/pull/20289 and optimises the tabs by replacing branches with an XOR. I saw this after watching the latest zed decoded episode so thank you for those videos! Release Notes: - N/A --- crates/rope/src/chunk.rs | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/crates/rope/src/chunk.rs b/crates/rope/src/chunk.rs index c158d2429e..5c2b9b87c3 100644 --- a/crates/rope/src/chunk.rs +++ b/crates/rope/src/chunk.rs @@ -504,8 +504,6 @@ impl<'a> ChunkSlice<'a> { #[inline(always)] pub fn tabs(&self) -> Tabs { Tabs { - byte_offset: 0, - char_offset: 0, tabs: self.tabs, chars: self.chars, } @@ -513,8 +511,6 @@ impl<'a> ChunkSlice<'a> { } pub struct Tabs { - byte_offset: usize, - char_offset: usize, tabs: u128, chars: u128, } @@ -536,21 +532,14 @@ impl Iterator for Tabs { let tab_offset = self.tabs.trailing_zeros() as usize; let chars_mask = (1 << tab_offset) - 1; let char_offset = (self.chars & chars_mask).count_ones() as usize; - self.byte_offset += tab_offset; - self.char_offset += char_offset; - let position = TabPosition { - byte_offset: self.byte_offset, - char_offset: self.char_offset, - }; - self.byte_offset += 1; - self.char_offset += 1; - if self.byte_offset == MAX_BASE { - self.tabs = 0; - } else { - self.tabs >>= tab_offset + 1; - self.chars >>= tab_offset + 1; - } + // Since tabs are 1 byte the tab offset is the same as the byte offset + let position = TabPosition { + byte_offset: tab_offset, + char_offset: char_offset, + }; + // Remove the tab we've just seen + self.tabs ^= 1 << tab_offset; Some(position) } From 75c545aa1e7a9cb01febf2f6dc00536c7271ff2f Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 21 Nov 2024 13:27:25 +0100 Subject: [PATCH 35/68] toolchains: Expose raw JSON representation of a toolchain (#20721) Closes #ISSUE Release Notes: - N/A --- crates/language/src/toolchain.rs | 2 ++ crates/languages/src/python.rs | 3 ++- crates/project/src/toolchain_store.rs | 28 ++++++++++++++++++--------- crates/proto/proto/zed.proto | 1 + crates/workspace/src/persistence.rs | 23 +++++++++++++--------- 5 files changed, 38 insertions(+), 19 deletions(-) diff --git a/crates/language/src/toolchain.rs b/crates/language/src/toolchain.rs index cd9a3bc403..d77690c1f7 100644 --- a/crates/language/src/toolchain.rs +++ b/crates/language/src/toolchain.rs @@ -20,6 +20,8 @@ pub struct Toolchain { pub name: SharedString, pub path: SharedString, pub language_name: LanguageName, + /// Full toolchain data (including language-specific details) + pub as_json: serde_json::Value, } #[async_trait(?Send)] diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index a5fe479627..3db79dd29f 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -591,8 +591,9 @@ impl ToolchainLister for PythonToolchainProvider { .into(); Some(Toolchain { name, - path: toolchain.executable?.to_str()?.to_owned().into(), + path: toolchain.executable.as_ref()?.to_str()?.to_owned().into(), language_name: LanguageName::new("Python"), + as_json: serde_json::to_value(toolchain).ok()?, }) }) .collect(); diff --git a/crates/project/src/toolchain_store.rs b/crates/project/src/toolchain_store.rs index c601ff8f12..4d4c32d745 100644 --- a/crates/project/src/toolchain_store.rs +++ b/crates/project/src/toolchain_store.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::{str::FromStr, sync::Arc}; use anyhow::{bail, Result}; @@ -119,6 +119,7 @@ impl ToolchainStore { let toolchain = Toolchain { name: toolchain.name.into(), path: toolchain.path.into(), + as_json: serde_json::Value::from_str(&toolchain.raw_json)?, language_name, }; let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); @@ -144,6 +145,7 @@ impl ToolchainStore { toolchain: toolchain.map(|toolchain| proto::Toolchain { name: toolchain.name.into(), path: toolchain.path.into(), + raw_json: toolchain.as_json.to_string(), }), }) } @@ -182,6 +184,7 @@ impl ToolchainStore { .map(|toolchain| proto::Toolchain { name: toolchain.name.to_string(), path: toolchain.path.to_string(), + raw_json: toolchain.as_json.to_string(), }) .collect::>() } else { @@ -352,6 +355,7 @@ impl RemoteToolchainStore { toolchain: Some(proto::Toolchain { name: toolchain.name.into(), path: toolchain.path.into(), + raw_json: toolchain.as_json.to_string(), }), }) .await @@ -383,10 +387,13 @@ impl RemoteToolchainStore { let toolchains = response .toolchains .into_iter() - .map(|toolchain| Toolchain { - language_name: language_name.clone(), - name: toolchain.name.into(), - path: toolchain.path.into(), + .filter_map(|toolchain| { + Some(Toolchain { + language_name: language_name.clone(), + name: toolchain.name.into(), + path: toolchain.path.into(), + as_json: serde_json::Value::from_str(&toolchain.raw_json).ok()?, + }) }) .collect(); let groups = response @@ -421,10 +428,13 @@ impl RemoteToolchainStore { .await .log_err()?; - response.toolchain.map(|toolchain| Toolchain { - language_name: language_name.clone(), - name: toolchain.name.into(), - path: toolchain.path.into(), + response.toolchain.and_then(|toolchain| { + Some(Toolchain { + language_name: language_name.clone(), + name: toolchain.name.into(), + path: toolchain.path.into(), + as_json: serde_json::Value::from_str(&toolchain.raw_json).ok()?, + }) }) }) } diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index b9540238f9..178d88ad26 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -2473,6 +2473,7 @@ message ListToolchains { message Toolchain { string name = 1; string path = 2; + string raw_json = 3; } message ToolchainGroup { diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 925d56a921..82de2bc684 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -1,6 +1,6 @@ pub mod model; -use std::path::Path; +use std::{path::Path, str::FromStr}; use anyhow::{anyhow, bail, Context, Result}; use client::DevServerProjectId; @@ -380,6 +380,9 @@ define_connection! { PRIMARY KEY (workspace_id, worktree_id, language_name) ); ), + sql!( + ALTER TABLE toolchains ADD COLUMN raw_json TEXT DEFAULT "{}"; + ), ]; } @@ -1080,18 +1083,19 @@ impl WorkspaceDb { self.write(move |this| { let mut select = this .select_bound(sql!( - SELECT name, path FROM toolchains WHERE workspace_id = ? AND language_name = ? AND worktree_id = ? + SELECT name, path, raw_json FROM toolchains WHERE workspace_id = ? AND language_name = ? AND worktree_id = ? )) .context("Preparing insertion")?; - let toolchain: Vec<(String, String)> = + let toolchain: Vec<(String, String, String)> = select((workspace_id, language_name.0.to_owned(), worktree_id.to_usize()))?; - Ok(toolchain.into_iter().next().map(|(name, path)| Toolchain { + Ok(toolchain.into_iter().next().and_then(|(name, path, raw_json)| Some(Toolchain { name: name.into(), path: path.into(), language_name, - })) + as_json: serde_json::Value::from_str(&raw_json).ok()? + }))) }) .await } @@ -1103,18 +1107,19 @@ impl WorkspaceDb { self.write(move |this| { let mut select = this .select_bound(sql!( - SELECT name, path, worktree_id, language_name FROM toolchains WHERE workspace_id = ? + SELECT name, path, worktree_id, language_name, raw_json FROM toolchains WHERE workspace_id = ? )) .context("Preparing insertion")?; - let toolchain: Vec<(String, String, u64, String)> = + let toolchain: Vec<(String, String, u64, String, String)> = select(workspace_id)?; - Ok(toolchain.into_iter().map(|(name, path, worktree_id, language_name)| (Toolchain { + Ok(toolchain.into_iter().filter_map(|(name, path, worktree_id, language_name, raw_json)| Some((Toolchain { name: name.into(), path: path.into(), language_name: LanguageName::new(&language_name), - }, WorktreeId::from_proto(worktree_id))).collect()) + as_json: serde_json::Value::from_str(&raw_json).ok()? + }, WorktreeId::from_proto(worktree_id)))).collect()) }) .await } From 0b373d43dcc8b25ecd277df7f32773dfccad530b Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 21 Nov 2024 15:57:22 +0100 Subject: [PATCH 36/68] toolchains: Use language-specific terms in UI (#20985) Closes #ISSUE Release Notes: - N/A --- crates/language/src/toolchain.rs | 2 ++ crates/languages/src/python.rs | 18 +++++++++++++--- crates/picker/src/picker.rs | 13 ++++++++++++ crates/project/src/project.rs | 13 ++++++++++++ .../src/active_toolchain.rs | 21 +++++++++++++++---- .../src/toolchain_selector.rs | 18 ++++++++++++++-- 6 files changed, 76 insertions(+), 9 deletions(-) diff --git a/crates/language/src/toolchain.rs b/crates/language/src/toolchain.rs index d77690c1f7..fe8936db08 100644 --- a/crates/language/src/toolchain.rs +++ b/crates/language/src/toolchain.rs @@ -31,6 +31,8 @@ pub trait ToolchainLister: Send + Sync { worktree_root: PathBuf, project_env: Option>, ) -> ToolchainList; + // Returns a term which we should use in UI to refer to a toolchain. + fn term(&self) -> SharedString; } #[async_trait(?Send)] diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 3db79dd29f..df158b9c7d 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -2,8 +2,8 @@ use anyhow::ensure; use anyhow::{anyhow, Result}; use async_trait::async_trait; use collections::HashMap; -use gpui::AsyncAppContext; use gpui::{AppContext, Task}; +use gpui::{AsyncAppContext, SharedString}; use language::language_settings::language_settings; use language::LanguageName; use language::LanguageToolchainStore; @@ -498,8 +498,17 @@ fn python_module_name_from_relative_path(relative_path: &str) -> String { .to_string() } -#[derive(Default)] -pub(crate) struct PythonToolchainProvider {} +pub(crate) struct PythonToolchainProvider { + term: SharedString, +} + +impl Default for PythonToolchainProvider { + fn default() -> Self { + Self { + term: SharedString::new_static("Virtual Environment"), + } + } +} static ENV_PRIORITY_LIST: &'static [PythonEnvironmentKind] = &[ // Prioritize non-Conda environments. @@ -604,6 +613,9 @@ impl ToolchainLister for PythonToolchainProvider { groups: Default::default(), } } + fn term(&self) -> SharedString { + self.term.clone() + } } pub struct EnvironmentApi<'a> { diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index 119c412b48..1cdb5af1af 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -425,6 +425,19 @@ impl Picker { self.cancel(&menu::Cancel, cx); } + pub fn refresh_placeholder(&mut self, cx: &mut WindowContext<'_>) { + match &self.head { + Head::Editor(view) => { + let placeholder = self.delegate.placeholder_text(cx); + view.update(cx, |this, cx| { + this.set_placeholder_text(placeholder, cx); + cx.notify(); + }); + } + Head::Empty(_) => {} + } + } + pub fn refresh(&mut self, cx: &mut ViewContext) { let query = self.query(cx); self.update_matches(query, cx); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 2b18659b7d..61a700e5d6 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2464,6 +2464,19 @@ impl Project { Task::ready(None) } } + + pub async fn toolchain_term( + languages: Arc, + language_name: LanguageName, + ) -> Option { + languages + .language_for_name(&language_name.0) + .await + .ok()? + .toolchain_lister() + .map(|lister| lister.term()) + } + pub fn activate_toolchain( &self, worktree_id: WorktreeId, diff --git a/crates/toolchain_selector/src/active_toolchain.rs b/crates/toolchain_selector/src/active_toolchain.rs index e2d0b2c808..c49deed02c 100644 --- a/crates/toolchain_selector/src/active_toolchain.rs +++ b/crates/toolchain_selector/src/active_toolchain.rs @@ -4,14 +4,15 @@ use gpui::{ ViewContext, WeakModel, WeakView, }; use language::{Buffer, BufferEvent, LanguageName, Toolchain}; -use project::WorktreeId; -use ui::{Button, ButtonCommon, Clickable, FluentBuilder, LabelSize, Tooltip}; +use project::{Project, WorktreeId}; +use ui::{Button, ButtonCommon, Clickable, FluentBuilder, LabelSize, SharedString, Tooltip}; use workspace::{item::ItemHandle, StatusItemView, Workspace}; use crate::ToolchainSelector; pub struct ActiveToolchain { active_toolchain: Option, + term: SharedString, workspace: WeakView, active_buffer: Option<(WorktreeId, WeakModel, Subscription)>, _update_toolchain_task: Task>, @@ -22,6 +23,7 @@ impl ActiveToolchain { Self { active_toolchain: None, active_buffer: None, + term: SharedString::new_static("Toolchain"), workspace: workspace.weak_handle(), _update_toolchain_task: Self::spawn_tracker_task(cx), @@ -44,7 +46,17 @@ impl ActiveToolchain { .update(&mut cx, |this, _| Some(this.language()?.name())) .ok() .flatten()?; - + let term = workspace + .update(&mut cx, |workspace, cx| { + let languages = workspace.project().read(cx).languages(); + Project::toolchain_term(languages.clone(), language_name.clone()) + }) + .ok()? + .await?; + let _ = this.update(&mut cx, |this, cx| { + this.term = term; + cx.notify(); + }); let worktree_id = active_file .update(&mut cx, |this, cx| Some(this.file()?.worktree_id(cx))) .ok() @@ -133,6 +145,7 @@ impl ActiveToolchain { impl Render for ActiveToolchain { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { div().when_some(self.active_toolchain.as_ref(), |el, active_toolchain| { + let term = self.term.clone(); el.child( Button::new("change-toolchain", active_toolchain.name.clone()) .label_size(LabelSize::Small) @@ -143,7 +156,7 @@ impl Render for ActiveToolchain { }); } })) - .tooltip(|cx| Tooltip::text("Select Toolchain", cx)), + .tooltip(move |cx| Tooltip::text(format!("Select {}", &term), cx)), ) }) } diff --git a/crates/toolchain_selector/src/toolchain_selector.rs b/crates/toolchain_selector/src/toolchain_selector.rs index 8a3368f816..4c31d600ba 100644 --- a/crates/toolchain_selector/src/toolchain_selector.rs +++ b/crates/toolchain_selector/src/toolchain_selector.rs @@ -126,6 +126,7 @@ pub struct ToolchainSelectorDelegate { workspace: WeakView, worktree_id: WorktreeId, worktree_abs_path_root: Arc, + placeholder_text: Arc, _fetch_candidates_task: Task>, } @@ -144,6 +145,17 @@ impl ToolchainSelectorDelegate { let _fetch_candidates_task = cx.spawn({ let project = project.clone(); move |this, mut cx| async move { + let term = project + .update(&mut cx, |this, _| { + Project::toolchain_term(this.languages().clone(), language_name.clone()) + }) + .ok()? + .await?; + let placeholder_text = format!("Select a {}…", term.to_lowercase()).into(); + let _ = this.update(&mut cx, move |this, cx| { + this.delegate.placeholder_text = placeholder_text; + this.refresh_placeholder(cx); + }); let available_toolchains = project .update(&mut cx, |this, cx| { this.available_toolchains(worktree_id, language_name, cx) @@ -153,6 +165,7 @@ impl ToolchainSelectorDelegate { let _ = this.update(&mut cx, move |this, cx| { this.delegate.candidates = available_toolchains; + if let Some(active_toolchain) = active_toolchain { if let Some(position) = this .delegate @@ -170,7 +183,7 @@ impl ToolchainSelectorDelegate { Some(()) } }); - + let placeholder_text = "Select a toolchain…".to_string().into(); Self { toolchain_selector: language_selector, candidates: Default::default(), @@ -179,6 +192,7 @@ impl ToolchainSelectorDelegate { workspace, worktree_id, worktree_abs_path_root, + placeholder_text, _fetch_candidates_task, } } @@ -196,7 +210,7 @@ impl PickerDelegate for ToolchainSelectorDelegate { type ListItem = ListItem; fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc { - "Select a toolchain...".into() + self.placeholder_text.clone() } fn match_count(&self) -> usize { From 74223c1b009662840de04fde6bfcc9ba4780fddf Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 21 Nov 2024 09:05:00 -0700 Subject: [PATCH 37/68] vim: Fix shortcuts that require shift+punct (#20990) Fixes a bug I introduced in #20953 Release Notes: - N/A --- crates/gpui/src/platform/mac/events.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/crates/gpui/src/platform/mac/events.rs b/crates/gpui/src/platform/mac/events.rs index f715dba562..e1aae9db39 100644 --- a/crates/gpui/src/platform/mac/events.rs +++ b/crates/gpui/src/platform/mac/events.rs @@ -341,6 +341,18 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke { chars_ignoring_modifiers = chars_with_cmd; } + if !control && !command && !function { + let mut mods = NO_MOD; + if shift { + mods |= SHIFT_MOD; + } + if alt { + mods |= OPTION_MOD; + } + + key_char = Some(chars_for_modified_key(native_event.keyCode(), mods)); + } + let mut key = if shift && chars_ignoring_modifiers .chars() @@ -354,18 +366,6 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke { chars_ignoring_modifiers }; - if !control && !command && !function { - let mut mods = NO_MOD; - if shift { - mods |= SHIFT_MOD; - } - if alt { - mods |= OPTION_MOD; - } - - key_char = Some(chars_for_modified_key(native_event.keyCode(), mods)); - } - key } }; From 395e25be256b77d5465af015641ead849b766947 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 21 Nov 2024 10:18:54 -0700 Subject: [PATCH 38/68] Fix keybindings on a Spanish ISO keyboard (#20995) Co-Authored-By: Peter Also reformatted the mappings to be easier to read/edit by hand. Release Notes: - Fixed keyboard shortcuts on Spanish ISO keyboards --------- Co-authored-by: Peter --- crates/settings/src/key_equivalents.rs | 1520 +++++++++++++++++++++--- 1 file changed, 1370 insertions(+), 150 deletions(-) diff --git a/crates/settings/src/key_equivalents.rs b/crates/settings/src/key_equivalents.rs index 1c68f48db4..4c5ae9e065 100644 --- a/crates/settings/src/key_equivalents.rs +++ b/crates/settings/src/key_equivalents.rs @@ -26,157 +26,1377 @@ use collections::HashMap; // From there I used multi-cursor to produce this match statement. #[cfg(target_os = "macos")] pub fn get_key_equivalents(layout: &str) -> Option> { - let (from, to) = match layout { - "com.apple.keylayout.Welsh" => ("#", "£"), - "com.apple.keylayout.Turkmen" => ("qc]Q`|[XV\\^v~Cx}{", "äçöÄžŞňÜÝş№ýŽÇüÖŇ"), - "com.apple.keylayout.Turkish-QWERTY-PC" => ( - "$\\|`'[}^=.#{*+:/~;)(@<,&]>\"", - "+,;<ığÜ&.ç^Ğ(:Ş*>ş=)'Öö/üÇI", - ), - "com.apple.keylayout.Sami-PC" => ( - "}*x\"w[~^/@`]{|<)>W(\\X=Qq&':;", - "Æ(čŊšøŽ&´\"žæØĐ;=:Š)đČ`Áá/ŋÅå", - ), - "com.apple.keylayout.LatinAmerican" => { - ("[^~>`(<\\@{;*&/):]|='}\"", "{&>:<);¿\"[ñ(/'=Ñ}¡*´]¨") - } - "com.apple.keylayout.IrishExtended" => ("#", "£"), - "com.apple.keylayout.Icelandic" => ("[}=:/'){(*&;^|`\"\\>]<~@", "æ´*Ð'ö=Æ)(/ð&Þ<Öþ:´;>\""), - "com.apple.keylayout.German-DIN-2137" => { - ("}~/<^>{`:\\)&=[]@|;#'\"(*", "Ä>ß;&:Ö<Ü#=/*öä\"'ü§´`)(") - } - "com.apple.keylayout.FinnishSami-PC" => { - (")=*\"\\[@{:>';/<|~(]}^`&", "=`(ˆ@ö\"ÖÅ:¨å´;*>)äÄ& { - ("];{`:'*<~=/}\\|&[\"($^)>@", "äåÖ<Ũ(;>`´Ä'*/öˆ)€&=:\"") - } - "com.apple.keylayout.Faroese" => ("}\";/$>^@~`:&[*){|]=(\\<'", "ÐØæ´€:&\"><Æ/å(=Å*ð`)';ø"), - "com.apple.keylayout.Croatian-PC" => { - ("{@~;<=>(&*['|]\":/}^`)\\", "Š\">č;*:)/(šćŽđĆČ'Đ&<=ž") - } - "com.apple.keylayout.Croatian" => ("{@;<~=>(&*['|]\":}^)\\`", "Š\"č;>*:)'(šćŽđĆČĐ&=ž<"), - "com.apple.keylayout.Azeri" => (":{W?./\"[}<]|,>';w", "IÖÜ,ş.ƏöĞÇğ/çŞəıü"), - "com.apple.keylayout.Albanian" => ("\\'~;:|<>`\"@", "ë@>çÇË;:<'\""), - "com.apple.keylayout.SwissFrench" => ( - ":@&'~^)$;\"][\\/#={!|*+`<(>}", - "ü\"/^>&=çè`àé$'*¨ö+£(!<;):ä", - ), - "com.apple.keylayout.Swedish" => ("(]\\\"~$`^{|/>*:;<)&=[}'@", ")ä'^>€<&Ö*´:(Åå;=/`öĨ\""), - "com.apple.keylayout.Swedish-Pro" => { - ("/^*`'{|)$>&<[\\;(~\"}@]:=", "´&(<¨Ö*=€:/;ö'å)>^Ä\"äÅ`") - } - "com.apple.keylayout.Spanish" => ("|!\\<{[:;@`/~].'>}\"^", "\"¡'¿Ññº´!<.>;ç`Ç:¨/"), - "com.apple.keylayout.Spanish-ISO" => ( - "|~`]/:)(<&^>*;#}\"{.\\['@", - "\"><;.º=)¿/&Ç(´·not found¨Ñç'ñ`\"", - ), - "com.apple.keylayout.Portuguese" => (")`/'^\"<];>[:{@}(&*=~", "=<'´&`;~º:çªÇ\"^)/(*>"), - "com.apple.keylayout.Italian" => ( - "*7};8:!5%(1&4]^\\6)32>.à32", - ), - "com.apple.keylayout.Italian-Pro" => { - ("/:@[]'\\=){;|#<\"(*^&`}>~", "'é\"òàìù*=ç解;^)(&/<°:>") - } - "com.apple.keylayout.Irish" => ("#", "£"), - "com.apple.keylayout.German" => ("=`#'}:)/\"^&]*{;|[<(>~@\\", "*<§´ÄÜ=ß`&/ä(Öü'ö;):>\"#"), - "com.apple.keylayout.French" => ( - "*}7;8:!5%(1&4]\\^6)32>.ç32", - ), - "com.apple.keylayout.French-numerical" => ( - "|!52;][>&@\"%'{)<~7.1/^(}*8#0$9`6\\3:4", - "£1(é)$^/72%5ù¨0.>è;&:69*8!3à4ç<§`\"°'", - ), - "com.apple.keylayout.French-PC" => ( - "!&\"_$}/72>8]#:31)*<%4;6\\-{['@(0|5.`9~^", - "17%°4£:èé/_$3§\"&08.5'!-*)¨^ù29àμ(;<ç>6", - ), - "com.apple.keylayout.Finnish" => ("/^*`)'{|$>&<[\\~;(\"}@]:=", "´&(<=¨Ö*€:/;ö'>å)^Ä\"äÅ`"), - "com.apple.keylayout.Danish" => ("=[;'`{}|>]*^(&@~)<\\/$\":", "`æå¨<ÆØ*:ø(&)/\">=;'´€^Å"), - "com.apple.keylayout.Canadian-CSA" => ("\\?']/><[{}|~`\"", "àÉèçé\"'^¨ÇÀÙùÈ"), - "com.apple.keylayout.British" => ("#", "£"), - "com.apple.keylayout.Brazilian-ABNT2" => ("\"|~?`'/^\\", "`^\"Ç'´ç¨~"), - "com.apple.keylayout.Belgian" => ( - "`3/*<\\8>7#&96@);024(|'1\":$[~5.%^}]{!", - "<\":8.`!/è37ç§20)àé'9£ù&%°4^>(;56*$¨1", - ), - "com.apple.keylayout.Austrian" => ("/^*`'{|)>&<[\\;(~\"}@]:=#", "ß&(<´Ö'=:/;ö#ü)>`Ä\"äÜ*§"), - "com.apple.keylayout.Slovak-QWERTY" => ( - "):9;63'\"]^/+@~>`? ( - "!$`10&:#4^*~{%5')}6/\"[8]97?;<@23>(+", - "14ň+é7\"3č68ŇÚ5ť§0Äž'!úáäíýˇô?2ľš:9%", - ), - "com.apple.keylayout.Polish" => ( - "&)|?,%:;^}]_{!+#(*`/[~<\"$.>'@=\\", - ":\"$Ż.+Łł=)(柧]!/_<żó>śę?,ńą%[;", - ), - "com.apple.keylayout.Lithuanian" => ("+#&=!%1*@73^584$26", "ŽĘŲžĄĮąŪČųęŠįūėĖčš"), - "com.apple.keylayout.Hungarian" => ( - "}(*@\"{=/|;>'[`<~\\!$&0#:]^)+", - "Ú)(\"ÁŐóüŰé:áőíÜÍű'!=ö+Éú/ÖÓ", - ), - "com.apple.keylayout.Hungarian-QWERTY" => ( - "=]#>@/&<`0')~(\\!:*;$\"+^{|}[", - "óú+:\"ü=ÜíöáÖÍ)ű'É(é!ÁÓ/ŐŰÚő", - ), - "com.apple.keylayout.Czech-QWERTY" => ( - "9>0[2()\"}@]46%5;#8{*7^~+!3?&'<$/1`:", - "í:éúě90!(2)čž5řů3áÚ8ý6`%1šˇ7§?4'+¨\"", - ), - "com.apple.keylayout.Maltese" => ("[`}{#]~", "ġżĦĠ£ħŻ"), - "com.apple.keylayout.Turkish" => ( - "|}(#>&^-/`$%@]~*,[\"<_.{:'\\)", - "ÜI%\"Ç)/ş.<'(*ı>_öğ-ÖŞçĞ$,ü:", - ), - "com.apple.keylayout.Turkish-Standard" => { - ("|}(#>=&^`@]~*,;[\"<.{:'\\)", "ÜI)^;*'&ö\"ıÖ(.çğŞ:,ĞÇşü=") - } - "com.apple.keylayout.NorwegianSami-PC" => { - ("\"}~<`&>':{@*^|\\)=([]/;", "ˆÆ>; { - (";\\@>&'<]\"|(=}^)`[~:*{", "čž\":'ć;đĆŽ)*Đ&=<š>Č(Š") - } - "com.apple.keylayout.Slovenian" => ("]`^@)&\":'*=<{;}(~>\\|[", "đ<&\"='ĆČć(*;ŠčĐ)>:žŽš"), - "com.apple.keylayout.SwedishSami-PC" => { - ("@=<^|`>){'&\"}]~[/:*\\(;", "\"`;&*<:=Ö¨/ˆÄä>ö´Å(@)å") - } - "com.apple.keylayout.SwissGerman" => ( - "={#:\\}!(+]/<\";$'`*[>&^~@)|", - "¨é*è$à+)!ä';`üç^<(ö:/&>\"=£", - ), - "com.apple.keylayout.Hawaiian" => ("'", "ʻ"), - "com.apple.keylayout.NorthernSami" => ( - ":/[<{X\"wQx\\(;~>W}`*@])'^|=q&", - "Å´ø;ØČŊšÁčđ)åŽ:ŠÆž(\"æ=ŋ&Đ`á/", - ), - "com.apple.keylayout.USInternational-PC" => ("^~", "ˆ˜"), - "com.apple.keylayout.NorwegianExtended" => ("^~", "ˆ˜"), - "com.apple.keylayout.Norwegian" => ("`'~\"\\*|=/@)[:}&><]{(^;", "<¨>^@(*`´\"=øÅÆ/:;æØ)&å"), - "com.apple.keylayout.ABC-QWERTZ" => { - ("\"}~<`>'&#:{@*^|\\)=(]/;[", "`Ä>;<:´/§ÜÖ\"(&'#=*)äßüö") - } - "com.apple.keylayout.ABC-AZERTY" => ( - ">[$61%@7|)&8\":}593(.4^8:ùà", - ), - "com.apple.keylayout.Czech" => ( - "(7*#193620?/{)@~!$8+;:%4\">`^]&5}[<'", - "9ý83+íšžěéˇ'Ú02`14á%ů\"5č!:¨6)7ř(ú?§", - ), - "com.apple.keylayout.Brazilian-Pro" => ("^~", "ˆ˜"), - _ => { - return None; - } - }; - debug_assert!(from.chars().count() == to.chars().count()); + let mappings: &[(char, char)] = match layout { + "com.apple.keylayout.ABC-AZERTY" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('.', ';'), + ('/', ':'), + ('0', 'à'), + ('1', '&'), + ('2', 'é'), + ('3', '"'), + ('4', '\''), + ('5', '('), + ('6', '§'), + ('7', 'è'), + ('8', '!'), + ('9', 'ç'), + (':', '°'), + (';', ')'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', '^'), + ('\'', 'ù'), + ('\\', '`'), + (']', '$'), + ('^', '6'), + ('`', '<'), + ('{', '¨'), + ('|', '£'), + ('}', '*'), + ('~', '>'), + ], + "com.apple.keylayout.ABC-QWERTZ" => &[ + ('"', '`'), + ('#', '§'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', 'ß'), + (':', 'Ü'), + (';', 'ü'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '´'), + ('\\', '#'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '\''), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.Albanian" => &[ + ('"', '\''), + (':', 'Ç'), + (';', 'ç'), + ('<', ';'), + ('>', ':'), + ('@', '"'), + ('\'', '@'), + ('\\', 'ë'), + ('`', '<'), + ('|', 'Ë'), + ('~', '>'), + ], + "com.apple.keylayout.Austrian" => &[ + ('"', '`'), + ('#', '§'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', 'ß'), + (':', 'Ü'), + (';', 'ü'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '´'), + ('\\', '#'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '\''), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.Azeri" => &[ + ('"', 'Ə'), + (',', 'ç'), + ('.', 'ş'), + ('/', '.'), + (':', 'I'), + (';', 'ı'), + ('<', 'Ç'), + ('>', 'Ş'), + ('?', ','), + ('W', 'Ü'), + ('[', 'ö'), + ('\'', 'ə'), + (']', 'ğ'), + ('w', 'ü'), + ('{', 'Ö'), + ('|', '/'), + ('}', 'Ğ'), + ], + "com.apple.keylayout.Belgian" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('.', ';'), + ('/', ':'), + ('0', 'à'), + ('1', '&'), + ('2', 'é'), + ('3', '"'), + ('4', '\''), + ('5', '('), + ('6', '§'), + ('7', 'è'), + ('8', '!'), + ('9', 'ç'), + (':', '°'), + (';', ')'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', '^'), + ('\'', 'ù'), + ('\\', '`'), + (']', '$'), + ('^', '6'), + ('`', '<'), + ('{', '¨'), + ('|', '£'), + ('}', '*'), + ('~', '>'), + ], + "com.apple.keylayout.Brazilian-ABNT2" => &[ + ('"', '`'), + ('/', 'ç'), + ('?', 'Ç'), + ('\'', '´'), + ('\\', '~'), + ('^', '¨'), + ('`', '\''), + ('|', '^'), + ('~', '"'), + ], + "com.apple.keylayout.Brazilian-Pro" => &[('^', 'ˆ'), ('~', '˜')], + "com.apple.keylayout.British" => &[('#', '£')], + "com.apple.keylayout.Canadian-CSA" => &[ + ('"', 'È'), + ('/', 'é'), + ('<', '\''), + ('>', '"'), + ('?', 'É'), + ('[', '^'), + ('\'', 'è'), + ('\\', 'à'), + (']', 'ç'), + ('`', 'ù'), + ('{', '¨'), + ('|', 'À'), + ('}', 'Ç'), + ('~', 'Ù'), + ], + "com.apple.keylayout.Croatian" => &[ + ('"', 'Ć'), + ('&', '\''), + ('(', ')'), + (')', '='), + ('*', '('), + (':', 'Č'), + (';', 'č'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'š'), + ('\'', 'ć'), + ('\\', 'ž'), + (']', 'đ'), + ('^', '&'), + ('`', '<'), + ('{', 'Š'), + ('|', 'Ž'), + ('}', 'Đ'), + ('~', '>'), + ], + "com.apple.keylayout.Croatian-PC" => &[ + ('"', 'Ć'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '\''), + (':', 'Č'), + (';', 'č'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'š'), + ('\'', 'ć'), + ('\\', 'ž'), + (']', 'đ'), + ('^', '&'), + ('`', '<'), + ('{', 'Š'), + ('|', 'Ž'), + ('}', 'Đ'), + ('~', '>'), + ], + "com.apple.keylayout.Czech" => &[ + ('!', '1'), + ('"', '!'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('+', '%'), + ('/', '\''), + ('0', 'é'), + ('1', '+'), + ('2', 'ě'), + ('3', 'š'), + ('4', 'č'), + ('5', 'ř'), + ('6', 'ž'), + ('7', 'ý'), + ('8', 'á'), + ('9', 'í'), + (':', '"'), + (';', 'ů'), + ('<', '?'), + ('>', ':'), + ('?', 'ˇ'), + ('@', '2'), + ('[', 'ú'), + ('\'', '§'), + (']', ')'), + ('^', '6'), + ('`', '¨'), + ('{', 'Ú'), + ('}', '('), + ('~', '`'), + ], + "com.apple.keylayout.Czech-QWERTY" => &[ + ('!', '1'), + ('"', '!'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('+', '%'), + ('/', '\''), + ('0', 'é'), + ('1', '+'), + ('2', 'ě'), + ('3', 'š'), + ('4', 'č'), + ('5', 'ř'), + ('6', 'ž'), + ('7', 'ý'), + ('8', 'á'), + ('9', 'í'), + (':', '"'), + (';', 'ů'), + ('<', '?'), + ('>', ':'), + ('?', 'ˇ'), + ('@', '2'), + ('[', 'ú'), + ('\'', '§'), + (']', ')'), + ('^', '6'), + ('`', '¨'), + ('{', 'Ú'), + ('}', '('), + ('~', '`'), + ], + "com.apple.keylayout.Danish" => &[ + ('"', '^'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'æ'), + ('\'', '¨'), + ('\\', '\''), + (']', 'ø'), + ('^', '&'), + ('`', '<'), + ('{', 'Æ'), + ('|', '*'), + ('}', 'Ø'), + ('~', '>'), + ], + "com.apple.keylayout.Faroese" => &[ + ('"', 'Ø'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Æ'), + (';', 'æ'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'å'), + ('\'', 'ø'), + ('\\', '\''), + (']', 'ð'), + ('^', '&'), + ('`', '<'), + ('{', 'Å'), + ('|', '*'), + ('}', 'Ð'), + ('~', '>'), + ], + "com.apple.keylayout.Finnish" => &[ + ('"', '^'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '\''), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.FinnishExtended" => &[ + ('"', 'ˆ'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '\''), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.FinnishSami-PC" => &[ + ('"', 'ˆ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '@'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.French" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('.', ';'), + ('/', ':'), + ('0', 'à'), + ('1', '&'), + ('2', 'é'), + ('3', '"'), + ('4', '\''), + ('5', '('), + ('6', '§'), + ('7', 'è'), + ('8', '!'), + ('9', 'ç'), + (':', '°'), + (';', ')'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', '^'), + ('\'', 'ù'), + ('\\', '`'), + (']', '$'), + ('^', '6'), + ('`', '<'), + ('{', '¨'), + ('|', '£'), + ('}', '*'), + ('~', '>'), + ], + "com.apple.keylayout.French-PC" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('-', ')'), + ('.', ';'), + ('/', ':'), + ('0', 'à'), + ('1', '&'), + ('2', 'é'), + ('3', '"'), + ('4', '\''), + ('5', '('), + ('6', '-'), + ('7', 'è'), + ('8', '_'), + ('9', 'ç'), + (':', '§'), + (';', '!'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', '^'), + ('\'', 'ù'), + ('\\', '*'), + (']', '$'), + ('^', '6'), + ('_', '°'), + ('`', '<'), + ('{', '¨'), + ('|', 'μ'), + ('}', '£'), + ('~', '>'), + ], + "com.apple.keylayout.French-numerical" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('.', ';'), + ('/', ':'), + ('0', 'à'), + ('1', '&'), + ('2', 'é'), + ('3', '"'), + ('4', '\''), + ('5', '('), + ('6', '§'), + ('7', 'è'), + ('8', '!'), + ('9', 'ç'), + (':', '°'), + (';', ')'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', '^'), + ('\'', 'ù'), + ('\\', '`'), + (']', '$'), + ('^', '6'), + ('`', '<'), + ('{', '¨'), + ('|', '£'), + ('}', '*'), + ('~', '>'), + ], + "com.apple.keylayout.German" => &[ + ('"', '`'), + ('#', '§'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', 'ß'), + (':', 'Ü'), + (';', 'ü'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '´'), + ('\\', '#'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '\''), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.German-DIN-2137" => &[ + ('"', '`'), + ('#', '§'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', 'ß'), + (':', 'Ü'), + (';', 'ü'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '´'), + ('\\', '#'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '\''), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.Hawaiian" => &[('\'', 'ʻ')], + "com.apple.keylayout.Hungarian" => &[ + ('!', '\''), + ('"', 'Á'), + ('#', '+'), + ('$', '!'), + ('&', '='), + ('(', ')'), + (')', 'Ö'), + ('*', '('), + ('+', 'Ó'), + ('/', 'ü'), + ('0', 'ö'), + (':', 'É'), + (';', 'é'), + ('<', 'Ü'), + ('=', 'ó'), + ('>', ':'), + ('@', '"'), + ('[', 'ő'), + ('\'', 'á'), + ('\\', 'ű'), + (']', 'ú'), + ('^', '/'), + ('`', 'í'), + ('{', 'Ő'), + ('|', 'Ű'), + ('}', 'Ú'), + ('~', 'Í'), + ], + "com.apple.keylayout.Hungarian-QWERTY" => &[ + ('!', '\''), + ('"', 'Á'), + ('#', '+'), + ('$', '!'), + ('&', '='), + ('(', ')'), + (')', 'Ö'), + ('*', '('), + ('+', 'Ó'), + ('/', 'ü'), + ('0', 'ö'), + (':', 'É'), + (';', 'é'), + ('<', 'Ü'), + ('=', 'ó'), + ('>', ':'), + ('@', '"'), + ('[', 'ő'), + ('\'', 'á'), + ('\\', 'ű'), + (']', 'ú'), + ('^', '/'), + ('`', 'í'), + ('{', 'Ő'), + ('|', 'Ű'), + ('}', 'Ú'), + ('~', 'Í'), + ], + "com.apple.keylayout.Icelandic" => &[ + ('"', 'Ö'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '\''), + (':', 'Ð'), + (';', 'ð'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'æ'), + ('\'', 'ö'), + ('\\', 'þ'), + (']', '´'), + ('^', '&'), + ('`', '<'), + ('{', 'Æ'), + ('|', 'Þ'), + ('}', '´'), + ('~', '>'), + ], + "com.apple.keylayout.Irish" => &[('#', '£')], + "com.apple.keylayout.IrishExtended" => &[('#', '£')], + "com.apple.keylayout.Italian" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + (',', ';'), + ('.', ':'), + ('/', ','), + ('0', 'é'), + ('1', '&'), + ('2', '"'), + ('3', '\''), + ('4', '('), + ('5', 'ç'), + ('6', 'è'), + ('7', ')'), + ('8', '£'), + ('9', 'à'), + (':', '!'), + (';', 'ò'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', 'ì'), + ('\'', 'ù'), + ('\\', '§'), + (']', '$'), + ('^', '6'), + ('`', '<'), + ('{', '^'), + ('|', '°'), + ('}', '*'), + ('~', '>'), + ], + "com.apple.keylayout.Italian-Pro" => &[ + ('"', '^'), + ('#', '£'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '\''), + (':', 'é'), + (';', 'è'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ò'), + ('\'', 'ì'), + ('\\', 'ù'), + (']', 'à'), + ('^', '&'), + ('`', '<'), + ('{', 'ç'), + ('|', '§'), + ('}', '°'), + ('~', '>'), + ], + "com.apple.keylayout.LatinAmerican" => &[ + ('"', '¨'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '\''), + (':', 'Ñ'), + (';', 'ñ'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', '{'), + ('\'', '´'), + ('\\', '¿'), + (']', '}'), + ('^', '&'), + ('`', '<'), + ('{', '['), + ('|', '¡'), + ('}', ']'), + ('~', '>'), + ], + "com.apple.keylayout.Lithuanian" => &[ + ('!', 'Ą'), + ('#', 'Ę'), + ('$', 'Ė'), + ('%', 'Į'), + ('&', 'Ų'), + ('*', 'Ū'), + ('+', 'Ž'), + ('1', 'ą'), + ('2', 'č'), + ('3', 'ę'), + ('4', 'ė'), + ('5', 'į'), + ('6', 'š'), + ('7', 'ų'), + ('8', 'ū'), + ('=', 'ž'), + ('@', 'Č'), + ('^', 'Š'), + ], + "com.apple.keylayout.Maltese" => &[ + ('#', '£'), + ('[', 'ġ'), + (']', 'ħ'), + ('`', 'ż'), + ('{', 'Ġ'), + ('}', 'Ħ'), + ('~', 'Ż'), + ], + "com.apple.keylayout.NorthernSami" => &[ + ('"', 'Ŋ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('Q', 'Á'), + ('W', 'Š'), + ('X', 'Č'), + ('[', 'ø'), + ('\'', 'ŋ'), + ('\\', 'đ'), + (']', 'æ'), + ('^', '&'), + ('`', 'ž'), + ('q', 'á'), + ('w', 'š'), + ('x', 'č'), + ('{', 'Ø'), + ('|', 'Đ'), + ('}', 'Æ'), + ('~', 'Ž'), + ], + "com.apple.keylayout.Norwegian" => &[ + ('"', '^'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ø'), + ('\'', '¨'), + ('\\', '@'), + (']', 'æ'), + ('^', '&'), + ('`', '<'), + ('{', 'Ø'), + ('|', '*'), + ('}', 'Æ'), + ('~', '>'), + ], + "com.apple.keylayout.NorwegianExtended" => &[('^', 'ˆ'), ('~', '˜')], + "com.apple.keylayout.NorwegianSami-PC" => &[ + ('"', 'ˆ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ø'), + ('\'', '¨'), + ('\\', '@'), + (']', 'æ'), + ('^', '&'), + ('`', '<'), + ('{', 'Ø'), + ('|', '*'), + ('}', 'Æ'), + ('~', '>'), + ], + "com.apple.keylayout.Polish" => &[ + ('!', '§'), + ('"', 'ę'), + ('#', '!'), + ('$', '?'), + ('%', '+'), + ('&', ':'), + ('(', '/'), + (')', '"'), + ('*', '_'), + ('+', ']'), + (',', '.'), + ('.', ','), + ('/', 'ż'), + (':', 'Ł'), + (';', 'ł'), + ('<', 'ś'), + ('=', '['), + ('>', 'ń'), + ('?', 'Ż'), + ('@', '%'), + ('[', 'ó'), + ('\'', 'ą'), + ('\\', ';'), + (']', '('), + ('^', '='), + ('_', 'ć'), + ('`', '<'), + ('{', 'ź'), + ('|', '$'), + ('}', ')'), + ('~', '>'), + ], + "com.apple.keylayout.Portuguese" => &[ + ('"', '`'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '\''), + (':', 'ª'), + (';', 'º'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ç'), + ('\'', '´'), + (']', '~'), + ('^', '&'), + ('`', '<'), + ('{', 'Ç'), + ('}', '^'), + ('~', '>'), + ], + "com.apple.keylayout.Sami-PC" => &[ + ('"', 'Ŋ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('Q', 'Á'), + ('W', 'Š'), + ('X', 'Č'), + ('[', 'ø'), + ('\'', 'ŋ'), + ('\\', 'đ'), + (']', 'æ'), + ('^', '&'), + ('`', 'ž'), + ('q', 'á'), + ('w', 'š'), + ('x', 'č'), + ('{', 'Ø'), + ('|', 'Đ'), + ('}', 'Æ'), + ('~', 'Ž'), + ], + "com.apple.keylayout.Serbian-Latin" => &[ + ('"', 'Ć'), + ('&', '\''), + ('(', ')'), + (')', '='), + ('*', '('), + (':', 'Č'), + (';', 'č'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'š'), + ('\'', 'ć'), + ('\\', 'ž'), + (']', 'đ'), + ('^', '&'), + ('`', '<'), + ('{', 'Š'), + ('|', 'Ž'), + ('}', 'Đ'), + ('~', '>'), + ], + "com.apple.keylayout.Slovak" => &[ + ('!', '1'), + ('"', '!'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('+', '%'), + ('/', '\''), + ('0', 'é'), + ('1', '+'), + ('2', 'ľ'), + ('3', 'š'), + ('4', 'č'), + ('5', 'ť'), + ('6', 'ž'), + ('7', 'ý'), + ('8', 'á'), + ('9', 'í'), + (':', '"'), + (';', 'ô'), + ('<', '?'), + ('>', ':'), + ('?', 'ˇ'), + ('@', '2'), + ('[', 'ú'), + ('\'', '§'), + (']', 'ä'), + ('^', '6'), + ('`', 'ň'), + ('{', 'Ú'), + ('}', 'Ä'), + ('~', 'Ň'), + ], + "com.apple.keylayout.Slovak-QWERTY" => &[ + ('!', '1'), + ('"', '!'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('+', '%'), + ('/', '\''), + ('0', 'é'), + ('1', '+'), + ('2', 'ľ'), + ('3', 'š'), + ('4', 'č'), + ('5', 'ť'), + ('6', 'ž'), + ('7', 'ý'), + ('8', 'á'), + ('9', 'í'), + (':', '"'), + (';', 'ô'), + ('<', '?'), + ('>', ':'), + ('?', 'ˇ'), + ('@', '2'), + ('[', 'ú'), + ('\'', '§'), + (']', 'ä'), + ('^', '6'), + ('`', 'ň'), + ('{', 'Ú'), + ('}', 'Ä'), + ('~', 'Ň'), + ], + "com.apple.keylayout.Slovenian" => &[ + ('"', 'Ć'), + ('&', '\''), + ('(', ')'), + (')', '='), + ('*', '('), + (':', 'Č'), + (';', 'č'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'š'), + ('\'', 'ć'), + ('\\', 'ž'), + (']', 'đ'), + ('^', '&'), + ('`', '<'), + ('{', 'Š'), + ('|', 'Ž'), + ('}', 'Đ'), + ('~', '>'), + ], + "com.apple.keylayout.Spanish" => &[ + ('!', '¡'), + ('"', '¨'), + ('.', 'ç'), + ('/', '.'), + (':', 'º'), + (';', '´'), + ('<', '¿'), + ('>', 'Ç'), + ('@', '!'), + ('[', 'ñ'), + ('\'', '`'), + ('\\', '\''), + (']', ';'), + ('^', '/'), + ('`', '<'), + ('{', 'Ñ'), + ('|', '"'), + ('}', ':'), + ('~', '>'), + ], + "com.apple.keylayout.Spanish-ISO" => &[ + ('"', '¨'), + ('#', '·'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('.', 'ç'), + ('/', '.'), + (':', 'º'), + (';', '´'), + ('<', '¿'), + ('>', 'Ç'), + ('@', '"'), + ('[', 'ñ'), + ('\'', '`'), + ('\\', '\''), + (']', ';'), + ('^', '&'), + ('`', '<'), + ('{', 'Ñ'), + ('|', '"'), + ('}', '`'), + ('~', '>'), + ], + "com.apple.keylayout.Swedish" => &[ + ('"', '^'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '\''), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.Swedish-Pro" => &[ + ('"', '^'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '\''), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.SwedishSami-PC" => &[ + ('"', 'ˆ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '@'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.SwissFrench" => &[ + ('!', '+'), + ('"', '`'), + ('#', '*'), + ('$', 'ç'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('+', '!'), + ('/', '\''), + (':', 'ü'), + (';', 'è'), + ('<', ';'), + ('=', '¨'), + ('>', ':'), + ('@', '"'), + ('[', 'é'), + ('\'', '^'), + ('\\', '$'), + (']', 'à'), + ('^', '&'), + ('`', '<'), + ('{', 'ö'), + ('|', '£'), + ('}', 'ä'), + ('~', '>'), + ], + "com.apple.keylayout.SwissGerman" => &[ + ('!', '+'), + ('"', '`'), + ('#', '*'), + ('$', 'ç'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('+', '!'), + ('/', '\''), + (':', 'è'), + (';', 'ü'), + ('<', ';'), + ('=', '¨'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '^'), + ('\\', '$'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'é'), + ('|', '£'), + ('}', 'à'), + ('~', '>'), + ], + "com.apple.keylayout.Turkish" => &[ + ('"', '-'), + ('#', '"'), + ('$', '\''), + ('%', '('), + ('&', ')'), + ('(', '%'), + (')', ':'), + ('*', '_'), + (',', 'ö'), + ('-', 'ş'), + ('.', 'ç'), + ('/', '.'), + (':', '$'), + ('<', 'Ö'), + ('>', 'Ç'), + ('@', '*'), + ('[', 'ğ'), + ('\'', ','), + ('\\', 'ü'), + (']', 'ı'), + ('^', '/'), + ('_', 'Ş'), + ('`', '<'), + ('{', 'Ğ'), + ('|', 'Ü'), + ('}', 'I'), + ('~', '>'), + ], + "com.apple.keylayout.Turkish-QWERTY-PC" => &[ + ('"', 'I'), + ('#', '^'), + ('$', '+'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('+', ':'), + (',', 'ö'), + ('.', 'ç'), + ('/', '*'), + (':', 'Ş'), + (';', 'ş'), + ('<', 'Ö'), + ('=', '.'), + ('>', 'Ç'), + ('@', '\''), + ('[', 'ğ'), + ('\'', 'ı'), + ('\\', ','), + (']', 'ü'), + ('^', '&'), + ('`', '<'), + ('{', 'Ğ'), + ('|', ';'), + ('}', 'Ü'), + ('~', '>'), + ], + "com.apple.keylayout.Turkish-Standard" => &[ + ('"', 'Ş'), + ('#', '^'), + ('&', '\''), + ('(', ')'), + (')', '='), + ('*', '('), + (',', '.'), + ('.', ','), + (':', 'Ç'), + (';', 'ç'), + ('<', ':'), + ('=', '*'), + ('>', ';'), + ('@', '"'), + ('[', 'ğ'), + ('\'', 'ş'), + ('\\', 'ü'), + (']', 'ı'), + ('^', '&'), + ('`', 'ö'), + ('{', 'Ğ'), + ('|', 'Ü'), + ('}', 'I'), + ('~', 'Ö'), + ], + "com.apple.keylayout.Turkmen" => &[ + ('C', 'Ç'), + ('Q', 'Ä'), + ('V', 'Ý'), + ('X', 'Ü'), + ('[', 'ň'), + ('\\', 'ş'), + (']', 'ö'), + ('^', '№'), + ('`', 'ž'), + ('c', 'ç'), + ('q', 'ä'), + ('v', 'ý'), + ('x', 'ü'), + ('{', 'Ň'), + ('|', 'Ş'), + ('}', 'Ö'), + ('~', 'Ž'), + ], + "com.apple.keylayout.USInternational-PC" => &[('^', 'ˆ'), ('~', '˜')], + "com.apple.keylayout.Welsh" => &[('#', '£')], - Some(HashMap::from_iter(from.chars().zip(to.chars()))) + _ => return None, + }; + + Some(HashMap::from_iter(mappings.into_iter().cloned())) } #[cfg(not(target_os = "macos"))] From 5ff49db92fd3e804f080596433f56fc42b78c887 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 21 Nov 2024 19:57:09 +0200 Subject: [PATCH 39/68] Only show breadcrumbs for terminals when there's a title (#20997) Closes https://github.com/zed-industries/zed/issues/20475 Release Notes: - Fixed terminal title and breadcrumbs behavior --------- Co-authored-by: Thorsten Ball --- Cargo.lock | 1 + assets/settings/default.json | 8 ++++++-- crates/diagnostics/src/diagnostics.rs | 2 +- crates/editor/src/items.rs | 2 +- crates/image_viewer/src/image_viewer.rs | 2 +- crates/search/src/project_search.rs | 2 +- crates/terminal/src/terminal_settings.rs | 10 +++++++--- crates/terminal_view/Cargo.toml | 3 ++- crates/terminal_view/src/terminal_panel.rs | 8 ++++++-- crates/terminal_view/src/terminal_view.rs | 10 +++++----- crates/workspace/src/item.rs | 4 ++-- docs/src/configuring-zed.md | 14 ++++++++++---- 12 files changed, 43 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 49c4a10efc..ddf89ba3cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12282,6 +12282,7 @@ name = "terminal_view" version = "0.1.0" dependencies = [ "anyhow", + "breadcrumbs", "client", "collections", "db", diff --git a/assets/settings/default.json b/assets/settings/default.json index d654082e24..819cdcfff6 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -847,8 +847,12 @@ } }, "toolbar": { - // Whether to display the terminal title in its toolbar. - "title": true + // Whether to display the terminal title in its toolbar's breadcrumbs. + // Only shown if the terminal title is not empty. + // + // The shell running in the terminal needs to be configured to emit the title. + // Example: `echo -e "\e]2;New Title\007";` + "breadcrumbs": true } // Set the terminal's font size. If this option is not included, // the terminal will default to matching the buffer's font size. diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 6f20b91689..bd0af230ab 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -776,7 +776,7 @@ impl Item for ProjectDiagnosticsEditor { } } - fn breadcrumb_location(&self) -> ToolbarItemLocation { + fn breadcrumb_location(&self, _: &AppContext) -> ToolbarItemLocation { ToolbarItemLocation::PrimaryLeft } diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index d3914f6772..bd54d2c376 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -841,7 +841,7 @@ impl Item for Editor { self.pixel_position_of_newest_cursor } - fn breadcrumb_location(&self) -> ToolbarItemLocation { + fn breadcrumb_location(&self, _: &AppContext) -> ToolbarItemLocation { if self.show_breadcrumbs { ToolbarItemLocation::PrimaryLeft } else { diff --git a/crates/image_viewer/src/image_viewer.rs b/crates/image_viewer/src/image_viewer.rs index 5e58cc49fb..1d03e77e76 100644 --- a/crates/image_viewer/src/image_viewer.rs +++ b/crates/image_viewer/src/image_viewer.rs @@ -116,7 +116,7 @@ impl Item for ImageView { .map(Icon::from_path) } - fn breadcrumb_location(&self) -> ToolbarItemLocation { + fn breadcrumb_location(&self, _: &AppContext) -> ToolbarItemLocation { ToolbarItemLocation::PrimaryLeft } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 1f4492d992..8430fd1f37 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -536,7 +536,7 @@ impl Item for ProjectSearchView { } } - fn breadcrumb_location(&self) -> ToolbarItemLocation { + fn breadcrumb_location(&self, _: &AppContext) -> ToolbarItemLocation { if self.has_matches() { ToolbarItemLocation::Secondary } else { diff --git a/crates/terminal/src/terminal_settings.rs b/crates/terminal/src/terminal_settings.rs index e48e23b141..842f00ad9f 100644 --- a/crates/terminal/src/terminal_settings.rs +++ b/crates/terminal/src/terminal_settings.rs @@ -21,7 +21,7 @@ pub enum TerminalDockPosition { #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] pub struct Toolbar { - pub title: bool, + pub breadcrumbs: bool, } #[derive(Debug, Deserialize)] @@ -286,10 +286,14 @@ pub enum WorkingDirectory { // Toolbar related settings #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] pub struct ToolbarContent { - /// Whether to display the terminal title in its toolbar. + /// Whether to display the terminal title in breadcrumbs inside the terminal pane. + /// Only shown if the terminal title is not empty. + /// + /// The shell running in the terminal needs to be configured to emit the title. + /// Example: `echo -e "\e]2;New Title\007";` /// /// Default: true - pub title: Option, + pub breadcrumbs: Option, } #[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] diff --git a/crates/terminal_view/Cargo.toml b/crates/terminal_view/Cargo.toml index 64b979cdd6..e57d9d1fc6 100644 --- a/crates/terminal_view/Cargo.toml +++ b/crates/terminal_view/Cargo.toml @@ -14,8 +14,9 @@ doctest = false [dependencies] anyhow.workspace = true -db.workspace = true +breadcrumbs.workspace = true collections.workspace = true +db.workspace = true dirs.workspace = true editor.workspace = true futures.workspace = true diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 2ca7561bdb..ee10e924f4 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -1,6 +1,7 @@ use std::{ops::ControlFlow, path::PathBuf, sync::Arc}; use crate::{default_working_directory, TerminalView}; +use breadcrumbs::Breadcrumbs; use collections::{HashMap, HashSet}; use db::kvp::KEY_VALUE_STORE; use futures::future::join_all; @@ -138,8 +139,11 @@ impl TerminalPanel { ControlFlow::Break(()) }); let buffer_search_bar = cx.new_view(search::BufferSearchBar::new); - pane.toolbar() - .update(cx, |toolbar, cx| toolbar.add_item(buffer_search_bar, cx)); + let breadcrumbs = cx.new_view(|_| Breadcrumbs::new()); + pane.toolbar().update(cx, |toolbar, cx| { + toolbar.add_item(buffer_search_bar, cx); + toolbar.add_item(breadcrumbs, cx); + }); pane }); let subscriptions = vec![ diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 21d20599b9..ad0c7f520d 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -109,7 +109,7 @@ pub struct TerminalView { blink_epoch: usize, can_navigate_to_selected_word: bool, workspace_id: Option, - show_title: bool, + show_breadcrumbs: bool, block_below_cursor: Option>, scroll_top: Pixels, _subscriptions: Vec, @@ -189,7 +189,7 @@ impl TerminalView { blink_epoch: 0, can_navigate_to_selected_word: false, workspace_id, - show_title: TerminalSettings::get_global(cx).toolbar.title, + show_breadcrumbs: TerminalSettings::get_global(cx).toolbar.breadcrumbs, block_below_cursor: None, scroll_top: Pixels::ZERO, _subscriptions: vec![ @@ -259,7 +259,7 @@ impl TerminalView { fn settings_changed(&mut self, cx: &mut ViewContext) { let settings = TerminalSettings::get_global(cx); - self.show_title = settings.toolbar.title; + self.show_breadcrumbs = settings.toolbar.breadcrumbs; let new_cursor_shape = settings.cursor_shape.unwrap_or_default(); let old_cursor_shape = self.cursor_shape; @@ -1145,8 +1145,8 @@ impl Item for TerminalView { Some(Box::new(handle.clone())) } - fn breadcrumb_location(&self) -> ToolbarItemLocation { - if self.show_title { + fn breadcrumb_location(&self, cx: &AppContext) -> ToolbarItemLocation { + if self.show_breadcrumbs && !self.terminal().read(cx).breadcrumb_text.trim().is_empty() { ToolbarItemLocation::PrimaryLeft } else { ToolbarItemLocation::Hidden diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 5f14b9ba62..a7bf90dd17 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -278,7 +278,7 @@ pub trait Item: FocusableView + EventEmitter { None } - fn breadcrumb_location(&self) -> ToolbarItemLocation { + fn breadcrumb_location(&self, _: &AppContext) -> ToolbarItemLocation { ToolbarItemLocation::Hidden } @@ -827,7 +827,7 @@ impl ItemHandle for View { } fn breadcrumb_location(&self, cx: &AppContext) -> ToolbarItemLocation { - self.read(cx).breadcrumb_location() + self.read(cx).breadcrumb_location(cx) } fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option> { diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index b4da7901a1..4991ff1119 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -1628,7 +1628,7 @@ List of `integer` column numbers "button": false, "shell": {}, "toolbar": { - "title": true + "breadcrumbs": true }, "working_directory": "current_project_directory" } @@ -1946,7 +1946,7 @@ Disable with: ## Terminal: Toolbar -- Description: Whether or not to show various elements in the terminal toolbar. It only affects terminals placed in the editor pane. +- Description: Whether or not to show various elements in the terminal toolbar. - Setting: `toolbar` - Default: @@ -1954,7 +1954,7 @@ Disable with: { "terminal": { "toolbar": { - "title": true + "breadcrumbs": true } } } @@ -1962,7 +1962,13 @@ Disable with: **Options** -At the moment, only the `title` option is available, it controls displaying of the terminal title that can be changed via `PROMPT_COMMAND`. If the title is hidden, the terminal toolbar is not displayed. +At the moment, only the `breadcrumbs` option is available, it controls displaying of the terminal title that can be changed via `PROMPT_COMMAND`. + +If the terminal title is empty, the breadcrumbs won't be shown. + +The shell running in the terminal needs to be configured to emit the title. + +Example command to set the title: `echo -e "\e]2;New Title\007";` ### Terminal: Button From 571c7d4f6645528c0bf1d2bcacfd623676c69ee7 Mon Sep 17 00:00:00 2001 From: Nils Koch Date: Thu, 21 Nov 2024 18:03:40 +0000 Subject: [PATCH 40/68] Improve project_panel diagnostic icon knockout colors (#20760) Closes #20572 Release Notes: - N/A cc @danilo-leal @WeetHet --- crates/project_panel/src/project_panel.rs | 46 +++++++++++++---------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 9432d1e6d5..5ad2c2d12e 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -101,6 +101,7 @@ pub struct ProjectPanel { // We keep track of the mouse down state on entries so we don't flash the UI // in case a user clicks to open a file. mouse_down: bool, + hovered_entries: HashSet, } #[derive(Clone, Debug)] @@ -139,6 +140,7 @@ struct EntryDetails { is_marked: bool, is_editing: bool, is_processing: bool, + is_hovered: bool, is_cut: bool, filename_text_color: Color, diagnostic_severity: Option, @@ -256,7 +258,7 @@ fn get_item_color(cx: &ViewContext) -> ItemColors { ItemColors { default: colors.surface_background, - hover: colors.element_active, + hover: colors.ghost_element_hover, drag_over: colors.drop_target_background, marked_active: colors.ghost_element_selected, } @@ -380,6 +382,7 @@ impl ProjectPanel { diagnostics: Default::default(), scroll_handle, mouse_down: false, + hovered_entries: Default::default(), }; this.update_visible_entries(None, cx); @@ -2465,6 +2468,7 @@ impl ProjectPanel { is_expanded, is_selected: self.selection == Some(selection), is_marked, + is_hovered: self.hovered_entries.contains(&entry.id), is_editing: false, is_processing: false, is_cut: self @@ -2594,6 +2598,7 @@ impl ProjectPanel { let is_active = self .selection .map_or(false, |selection| selection.entry_id == entry_id); + let is_hovered = details.is_hovered; let width = self.size(cx); let file_name = details.filename.clone(); @@ -2626,6 +2631,14 @@ impl ProjectPanel { marked_selections: selections, }; + let (bg_color, border_color) = match (is_hovered, is_marked || is_active, self.mouse_down) { + (true, _, true) => (item_colors.marked_active, item_colors.hover), + (true, false, false) => (item_colors.hover, item_colors.hover), + (true, true, false) => (item_colors.hover, item_colors.marked_active), + (false, true, _) => (item_colors.marked_active, item_colors.marked_active), + _ => (item_colors.default, item_colors.default), + }; + div() .id(entry_id.to_proto() as usize) .when(is_local, |div| { @@ -2703,6 +2716,14 @@ impl ProjectPanel { cx.propagate(); }), ) + .on_hover(cx.listener(move |this, hover, cx| { + if *hover { + this.hovered_entries.insert(entry_id); + } else { + this.hovered_entries.remove(&entry_id); + } + cx.notify(); + })) .on_click(cx.listener(move |this, event: &gpui::ClickEvent, cx| { if event.down.button == MouseButton::Right || event.down.first_mouse || show_editor { @@ -2763,11 +2784,13 @@ impl ProjectPanel { } })) .cursor_pointer() + .bg(bg_color) + .border_color(border_color) .child( ListItem::new(entry_id.to_proto() as usize) .indent_level(depth) .indent_step_size(px(settings.indent_size)) - .selected(is_marked || is_active) + .selectable(false) .when_some(canonical_path, |this, path| { this.end_slot::( div() @@ -2807,11 +2830,7 @@ impl ProjectPanel { } else { IconDecorationKind::Dot }, - if is_marked || is_active { - item_colors.marked_active - } else { - item_colors.default - }, + bg_color, cx, ) .color(decoration_color.color(cx)) @@ -2924,19 +2943,6 @@ impl ProjectPanel { .border_1() .border_r_2() .rounded_none() - .hover(|style| { - if is_active { - style - } else { - style.bg(item_colors.hover).border_color(item_colors.hover) - } - }) - .when(is_marked || is_active, |this| { - this.when(is_marked, |this| { - this.bg(item_colors.marked_active) - .border_color(item_colors.marked_active) - }) - }) .when( !self.mouse_down && is_active && self.focus_handle.contains_focused(cx), |this| this.border_color(Color::Selected.color(cx)), From 268ac4c0476f56453639aba36715f8042e542815 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Thu, 21 Nov 2024 18:10:25 +0000 Subject: [PATCH 41/68] Implement readline/emacs/macos style ctrl-k cut and ctrl-y yank (#21003) - Added support for ctrl-k / ctrl-y alternate cut/yank buffer on macos. Co-authored-by: Conrad Irwin --- assets/keymaps/default-macos.json | 3 ++- crates/editor/src/actions.rs | 2 ++ crates/editor/src/editor.rs | 42 ++++++++++++++++++++++++++----- crates/editor/src/element.rs | 2 ++ 4 files changed, 42 insertions(+), 7 deletions(-) diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 82edba3305..5b416db9b2 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -49,8 +49,9 @@ "ctrl-d": "editor::Delete", "tab": "editor::Tab", "shift-tab": "editor::TabPrev", - "ctrl-k": "editor::CutToEndOfLine", "ctrl-t": "editor::Transpose", + "ctrl-k": "editor::KillRingCut", + "ctrl-y": "editor::KillRingYank", "cmd-k q": "editor::Rewrap", "cmd-k cmd-q": "editor::Rewrap", "cmd-backspace": "editor::DeleteToBeginningOfLine", diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index dcfc291968..5b11b18bc2 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -271,6 +271,8 @@ gpui::actions!( Hover, Indent, JoinLines, + KillRingCut, + KillRingYank, LineDown, LineUp, MoveDown, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index cc450c573f..b31938bcfd 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -74,7 +74,7 @@ use gpui::{ div, impl_actions, point, prelude::*, px, relative, size, uniform_list, Action, AnyElement, AppContext, AsyncWindowContext, AvailableSpace, BackgroundExecutor, Bounds, ClipboardEntry, ClipboardItem, Context, DispatchPhase, ElementId, EventEmitter, FocusHandle, FocusOutEvent, - FocusableView, FontId, FontWeight, HighlightStyle, Hsla, InteractiveText, KeyContext, + FocusableView, FontId, FontWeight, Global, HighlightStyle, Hsla, InteractiveText, KeyContext, ListSizingBehavior, Model, ModelContext, MouseButton, PaintQuad, ParentElement, Pixels, Render, ScrollStrategy, SharedString, Size, StrikethroughStyle, Styled, StyledText, Subscription, Task, TextStyle, TextStyleRefinement, UTF16Selection, UnderlineStyle, UniformListScrollHandle, View, @@ -7364,7 +7364,7 @@ impl Editor { .update(cx, |buffer, cx| buffer.edit(edits, None, cx)); } - pub fn cut(&mut self, _: &Cut, cx: &mut ViewContext) { + pub fn cut_common(&mut self, cx: &mut ViewContext) -> ClipboardItem { let mut text = String::new(); let buffer = self.buffer.read(cx).snapshot(cx); let mut selections = self.selections.all::(cx); @@ -7408,11 +7408,38 @@ impl Editor { s.select(selections); }); this.insert("", cx); - cx.write_to_clipboard(ClipboardItem::new_string_with_json_metadata( - text, - clipboard_selections, - )); }); + ClipboardItem::new_string_with_json_metadata(text, clipboard_selections) + } + + pub fn cut(&mut self, _: &Cut, cx: &mut ViewContext) { + let item = self.cut_common(cx); + cx.write_to_clipboard(item); + } + + pub fn kill_ring_cut(&mut self, _: &KillRingCut, cx: &mut ViewContext) { + self.change_selections(None, cx, |s| { + s.move_with(|snapshot, sel| { + if sel.is_empty() { + sel.end = DisplayPoint::new(sel.end.row(), snapshot.line_len(sel.end.row())) + } + }); + }); + let item = self.cut_common(cx); + cx.set_global(KillRing(item)) + } + + pub fn kill_ring_yank(&mut self, _: &KillRingYank, cx: &mut ViewContext) { + let (text, metadata) = if let Some(KillRing(item)) = cx.try_global() { + if let Some(ClipboardEntry::String(kill_ring)) = item.entries().first() { + (kill_ring.text().to_string(), kill_ring.metadata_json()) + } else { + return; + } + } else { + return; + }; + self.do_paste(&text, metadata, false, cx); } pub fn copy(&mut self, _: &Copy, cx: &mut ViewContext) { @@ -15145,4 +15172,7 @@ fn check_multiline_range(buffer: &Buffer, range: Range) -> Range { } } +pub struct KillRing(ClipboardItem); +impl Global for KillRing {} + const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 6e4538ae6d..0c403022a3 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -217,6 +217,8 @@ impl EditorElement { register_action(view, cx, Editor::transpose); register_action(view, cx, Editor::rewrap); register_action(view, cx, Editor::cut); + register_action(view, cx, Editor::kill_ring_cut); + register_action(view, cx, Editor::kill_ring_yank); register_action(view, cx, Editor::copy); register_action(view, cx, Editor::paste); register_action(view, cx, Editor::undo); From c16dfc1a39f481c9fb7db0b158522fcebbe142fc Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 21 Nov 2024 13:37:34 -0500 Subject: [PATCH 42/68] title_bar: Remove dependency on `command_palette` (#21006) This PR removes the `title_bar` crate's dependency on the `command_palette`. The `command_palette::Toggle` action now resides at `zed_actions::command_palette::Toggle`. Release Notes: - N/A --- Cargo.lock | 1 - crates/command_palette/src/command_palette.rs | 6 ++---- crates/title_bar/Cargo.toml | 1 - crates/title_bar/src/application_menu.rs | 5 ++++- crates/zed/src/zed/app_menus.rs | 2 +- crates/zed_actions/src/lib.rs | 6 ++++++ 6 files changed, 13 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ddf89ba3cd..f416381225 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12607,7 +12607,6 @@ dependencies = [ "call", "client", "collections", - "command_palette", "editor", "feature_flags", "feedback", diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 21dd06e81c..11bc6848fe 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -11,7 +11,7 @@ use command_palette_hooks::{ }; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - actions, Action, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Global, + Action, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Global, ParentElement, Render, Styled, Task, UpdateGlobal, View, ViewContext, VisualContext, WeakView, }; use picker::{Picker, PickerDelegate}; @@ -21,9 +21,7 @@ use settings::Settings; use ui::{h_flex, prelude::*, v_flex, HighlightedLabel, KeyBinding, ListItem, ListItemSpacing}; use util::ResultExt; use workspace::{ModalView, Workspace, WorkspaceSettings}; -use zed_actions::OpenZedUrl; - -actions!(command_palette, [Toggle]); +use zed_actions::{command_palette::Toggle, OpenZedUrl}; pub fn init(cx: &mut AppContext) { client::init_settings(cx); diff --git a/crates/title_bar/Cargo.toml b/crates/title_bar/Cargo.toml index 05bd1be502..809915b4dc 100644 --- a/crates/title_bar/Cargo.toml +++ b/crates/title_bar/Cargo.toml @@ -31,7 +31,6 @@ test-support = [ auto_update.workspace = true call.workspace = true client.workspace = true -command_palette.workspace = true feedback.workspace = true feature_flags.workspace = true gpui.workspace = true diff --git a/crates/title_bar/src/application_menu.rs b/crates/title_bar/src/application_menu.rs index c3994f81d7..3d5a774e8f 100644 --- a/crates/title_bar/src/application_menu.rs +++ b/crates/title_bar/src/application_menu.rs @@ -18,7 +18,10 @@ impl Render for ApplicationMenu { .menu(move |cx| { ContextMenu::build(cx, move |menu, cx| { menu.header("Workspace") - .action("Open Command Palette", Box::new(command_palette::Toggle)) + .action( + "Open Command Palette", + Box::new(zed_actions::command_palette::Toggle), + ) .when_some(cx.focused(), |menu, focused| menu.context(focused)) .custom_row(move |cx| { h_flex() diff --git a/crates/zed/src/zed/app_menus.rs b/crates/zed/src/zed/app_menus.rs index 824704fca5..4a2f351627 100644 --- a/crates/zed/src/zed/app_menus.rs +++ b/crates/zed/src/zed/app_menus.rs @@ -146,7 +146,7 @@ pub fn app_menus() -> Vec { MenuItem::action("Back", workspace::GoBack), MenuItem::action("Forward", workspace::GoForward), MenuItem::separator(), - MenuItem::action("Command Palette...", command_palette::Toggle), + MenuItem::action("Command Palette...", zed_actions::command_palette::Toggle), MenuItem::separator(), MenuItem::action("Go to File...", workspace::ToggleFileFinder::default()), // MenuItem::action("Go to Symbol in Project", project_symbols::Toggle), diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index 53f5b202a8..b777f03646 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -44,6 +44,12 @@ actions!( ] ); +pub mod command_palette { + use gpui::actions; + + actions!(command_palette, [Toggle]); +} + #[derive(Clone, Default, Deserialize, PartialEq)] pub struct InlineAssist { pub prompt: Option, From 02447a8552a7c48e3dc5fbb54e15ee400e41ab2a Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 21 Nov 2024 11:55:22 -0700 Subject: [PATCH 43/68] Use our own git clone in draft release notes (#20956) It turns out that messing with the git repo created by the github action is tricky, so we'll just clone our own. On my machine, a shallow tree-less clone takes <500ms Release Notes: - N/A --- script/create-draft-release | 2 +- script/draft-release-notes | 70 ++++++++++++++++++++++--------------- 2 files changed, 42 insertions(+), 30 deletions(-) diff --git a/script/create-draft-release b/script/create-draft-release index e72c6d141c..95b1a1450a 100755 --- a/script/create-draft-release +++ b/script/create-draft-release @@ -5,4 +5,4 @@ if [[ "$GITHUB_REF_NAME" == *"-pre" ]]; then preview="-p" fi -gh release create -d "$GITHUB_REF_NAME" -F "$1" $preview +gh release create -t "$GITHUB_REF_NAME" -d "$GITHUB_REF_NAME" -F "$1" $preview diff --git a/script/draft-release-notes b/script/draft-release-notes index eeb53bbb22..1ef276718d 100755 --- a/script/draft-release-notes +++ b/script/draft-release-notes @@ -19,24 +19,45 @@ async function main() { process.exit(1); } - let priorVersion = [parts[0], parts[1], parts[2] - 1].join("."); - let suffix = ""; - - if (channel == "preview") { - suffix = "-pre"; - if (parts[2] == 0) { - priorVersion = [parts[0], parts[1] - 1, 0].join("."); - } - } else if (!ensureTag(`v${priorVersion}`)) { - console.log("Copy the release notes from preview."); + // currently we can only draft notes for patch releases. + if (parts[2] == 0) { process.exit(0); } + let priorVersion = [parts[0], parts[1], parts[2] - 1].join("."); + let suffix = channel == "preview" ? "-pre" : ""; let [tag, priorTag] = [`v${version}${suffix}`, `v${priorVersion}${suffix}`]; - if (!ensureTag(tag) || !ensureTag(priorTag)) { - console.log("Could not draft release notes, missing a tag:", tag, priorTag); - process.exit(0); + try { + execFileSync("rm", ["-rf", "target/shallow_clone"]); + execFileSync("git", [ + "clone", + "https://github.com/zed-industries/zed", + "target/shallow_clone", + "--filter=tree:0", + "--no-checkout", + "--branch", + tag, + "--depth", + 100, + ]); + execFileSync("git", [ + "-C", + "target/shallow_clone", + "rev-parse", + "--verify", + tag, + ]); + execFileSync("git", [ + "-C", + "target/shallow_clone", + "rev-parse", + "--verify", + priorTag, + ]); + } catch (e) { + console.error(e.stderr.toString()); + process.exit(1); } const newCommits = getCommits(priorTag, tag); @@ -69,7 +90,13 @@ async function main() { function getCommits(oldTag, newTag) { const pullRequestNumbers = execFileSync( "git", - ["log", `${oldTag}..${newTag}`, "--format=DIVIDER\n%H|||%B"], + [ + "-C", + "target/shallow_clone", + "log", + `${oldTag}..${newTag}`, + "--format=DIVIDER\n%H|||%B", + ], { encoding: "utf8" }, ) .replace(/\r\n/g, "\n") @@ -99,18 +126,3 @@ function getCommits(oldTag, newTag) { return pullRequestNumbers; } - -function ensureTag(tag) { - try { - execFileSync("git", ["rev-parse", "--verify", tag]); - return true; - } catch (e) { - try { - execFileSync("git"[("fetch", "origin", "--shallow-exclude", tag)]); - execFileSync("git"[("fetch", "origin", "--deepen", "1")]); - return true; - } catch (e) { - return false; - } - } -} From 841d3221b34fa5786d85fb7c2cd7e9cc3ae51c15 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 21 Nov 2024 11:59:02 -0700 Subject: [PATCH 44/68] Auto release preview patch releases (#20886) This should make the process of releasing patch releases to preview less toilful Release Notes: - N/A --- .github/workflows/ci.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 43af9309fe..8f2f08aa1a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -404,3 +404,16 @@ jobs: target/release/zed-linux-aarch64.tar.gz env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + auto-release-preview: + name: Auto release preview + if: ${{ startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre') }} + needs: [bundle-mac, bundle-linux, bundle-linux-aarch64] + runs-on: + - self-hosted + - bundle + steps: + - name: gh release + run: gh release edit $GITHUB_REF_NAME --draft=false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From f62ccf9c8a68a70353460d23586c557e89f7ec9b Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 21 Nov 2024 14:11:57 -0500 Subject: [PATCH 45/68] Extract `auto_update_ui` crate (#21008) This PR extracts an `auto_update_ui` crate out of the `auto_update` crate. This allows `auto_update` to not depend on heavier crates like `editor`, which in turn allows other downstream crates to start building sooner. Release Notes: - N/A --- Cargo.lock | 26 ++- Cargo.toml | 2 + crates/auto_update/Cargo.toml | 5 - crates/auto_update/src/auto_update.rs | 170 ++---------------- crates/auto_update_ui/Cargo.toml | 28 +++ crates/auto_update_ui/LICENSE-GPL | 1 + crates/auto_update_ui/src/auto_update_ui.rs | 147 +++++++++++++++ .../src/update_notification.rs | 0 crates/zed/Cargo.toml | 1 + crates/zed/src/main.rs | 1 + crates/zed/src/zed.rs | 2 +- 11 files changed, 217 insertions(+), 166 deletions(-) create mode 100644 crates/auto_update_ui/Cargo.toml create mode 120000 crates/auto_update_ui/LICENSE-GPL create mode 100644 crates/auto_update_ui/src/auto_update_ui.rs rename crates/{auto_update => auto_update_ui}/src/update_notification.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index f416381225..8888754a33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1014,26 +1014,41 @@ dependencies = [ "anyhow", "client", "db", - "editor", "gpui", "http_client", "log", - "markdown_preview", - "menu", "paths", "release_channel", "schemars", "serde", - "serde_derive", "serde_json", "settings", "smol", "tempfile", - "util", "which 6.0.3", "workspace", ] +[[package]] +name = "auto_update_ui" +version = "0.1.0" +dependencies = [ + "anyhow", + "auto_update", + "client", + "editor", + "gpui", + "http_client", + "markdown_preview", + "menu", + "release_channel", + "serde", + "serde_json", + "smol", + "util", + "workspace", +] + [[package]] name = "autocfg" version = "1.4.0" @@ -15464,6 +15479,7 @@ dependencies = [ "async-watch", "audio", "auto_update", + "auto_update_ui", "backtrace", "breadcrumbs", "call", diff --git a/Cargo.toml b/Cargo.toml index a5555864d1..b1feec52ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ members = [ "crates/assistant_tool", "crates/audio", "crates/auto_update", + "crates/auto_update_ui", "crates/breadcrumbs", "crates/call", "crates/channel", @@ -187,6 +188,7 @@ assistant_slash_command = { path = "crates/assistant_slash_command" } assistant_tool = { path = "crates/assistant_tool" } audio = { path = "crates/audio" } auto_update = { path = "crates/auto_update" } +auto_update_ui = { path = "crates/auto_update_ui" } breadcrumbs = { path = "crates/breadcrumbs" } call = { path = "crates/call" } channel = { path = "crates/channel" } diff --git a/crates/auto_update/Cargo.toml b/crates/auto_update/Cargo.toml index d47a9f9ae0..fa46b04a78 100644 --- a/crates/auto_update/Cargo.toml +++ b/crates/auto_update/Cargo.toml @@ -16,21 +16,16 @@ doctest = false anyhow.workspace = true client.workspace = true db.workspace = true -editor.workspace = true gpui.workspace = true http_client.workspace = true log.workspace = true -markdown_preview.workspace = true -menu.workspace = true paths.workspace = true release_channel.workspace = true schemars.workspace = true serde.workspace = true -serde_derive.workspace = true serde_json.workspace = true settings.workspace = true smol.workspace = true tempfile.workspace = true -util.workspace = true which.workspace = true workspace.workspace = true diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index 6d95daecb7..0f9999b918 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -1,27 +1,19 @@ -mod update_notification; - use anyhow::{anyhow, Context, Result}; use client::{Client, TelemetrySettings}; use db::kvp::KEY_VALUE_STORE; use db::RELEASE_CHANNEL; -use editor::{Editor, MultiBuffer}; use gpui::{ actions, AppContext, AsyncAppContext, Context as _, Global, Model, ModelContext, - SemanticVersion, SharedString, Task, View, ViewContext, VisualContext, WindowContext, + SemanticVersion, Task, WindowContext, }; - -use markdown_preview::markdown_preview_view::{MarkdownPreviewMode, MarkdownPreviewView}; -use paths::remote_servers_dir; -use schemars::JsonSchema; -use serde::Deserialize; -use serde_derive::Serialize; -use smol::{fs, io::AsyncReadExt}; - -use settings::{Settings, SettingsSources, SettingsStore}; -use smol::{fs::File, process::Command}; - use http_client::{AsyncBody, HttpClient, HttpClientWithUrl}; -use release_channel::{AppCommitSha, AppVersion, ReleaseChannel}; +use paths::remote_servers_dir; +use release_channel::{AppCommitSha, ReleaseChannel}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use settings::{Settings, SettingsSources, SettingsStore}; +use smol::{fs, io::AsyncReadExt}; +use smol::{fs::File, process::Command}; use std::{ env::{ self, @@ -32,24 +24,13 @@ use std::{ sync::Arc, time::Duration, }; -use update_notification::UpdateNotification; -use util::ResultExt; use which::which; -use workspace::notifications::NotificationId; use workspace::Workspace; const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification"; const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60); -actions!( - auto_update, - [ - Check, - DismissErrorMessage, - ViewReleaseNotes, - ViewReleaseNotesLocally - ] -); +actions!(auto_update, [Check, DismissErrorMessage, ViewReleaseNotes,]); #[derive(Serialize)] struct UpdateRequestBody { @@ -146,12 +127,6 @@ struct GlobalAutoUpdate(Option>); impl Global for GlobalAutoUpdate {} -#[derive(Deserialize)] -struct ReleaseNotesBody { - title: String, - release_notes: String, -} - pub fn init(http_client: Arc, cx: &mut AppContext) { AutoUpdateSetting::register(cx); @@ -161,10 +136,6 @@ pub fn init(http_client: Arc, cx: &mut AppContext) { workspace.register_action(|_, action, cx| { view_release_notes(action, cx); }); - - workspace.register_action(|workspace, _: &ViewReleaseNotesLocally, cx| { - view_release_notes_locally(workspace, cx); - }); }) .detach(); @@ -264,121 +235,6 @@ pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) -> Option<( None } -fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext) { - let release_channel = ReleaseChannel::global(cx); - - let url = match release_channel { - ReleaseChannel::Nightly => Some("https://github.com/zed-industries/zed/commits/nightly/"), - ReleaseChannel::Dev => Some("https://github.com/zed-industries/zed/commits/main/"), - _ => None, - }; - - if let Some(url) = url { - cx.open_url(url); - return; - } - - let version = AppVersion::global(cx).to_string(); - - let client = client::Client::global(cx).http_client(); - let url = client.build_url(&format!( - "/api/release_notes/v2/{}/{}", - release_channel.dev_name(), - version - )); - - let markdown = workspace - .app_state() - .languages - .language_for_name("Markdown"); - - workspace - .with_local_workspace(cx, move |_, cx| { - cx.spawn(|workspace, mut cx| async move { - let markdown = markdown.await.log_err(); - let response = client.get(&url, Default::default(), true).await; - let Some(mut response) = response.log_err() else { - return; - }; - - let mut body = Vec::new(); - response.body_mut().read_to_end(&mut body).await.ok(); - - let body: serde_json::Result = - serde_json::from_slice(body.as_slice()); - - if let Ok(body) = body { - workspace - .update(&mut cx, |workspace, cx| { - let project = workspace.project().clone(); - let buffer = project.update(cx, |project, cx| { - project.create_local_buffer("", markdown, cx) - }); - buffer.update(cx, |buffer, cx| { - buffer.edit([(0..0, body.release_notes)], None, cx) - }); - let language_registry = project.read(cx).languages().clone(); - - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); - - let tab_description = SharedString::from(body.title.to_string()); - let editor = cx.new_view(|cx| { - Editor::for_multibuffer(buffer, Some(project), true, cx) - }); - let workspace_handle = workspace.weak_handle(); - let view: View = MarkdownPreviewView::new( - MarkdownPreviewMode::Default, - editor, - workspace_handle, - language_registry, - Some(tab_description), - cx, - ); - workspace.add_item_to_active_pane( - Box::new(view.clone()), - None, - true, - cx, - ); - cx.notify(); - }) - .log_err(); - } - }) - .detach(); - }) - .detach(); -} - -pub fn notify_of_any_new_update(cx: &mut ViewContext) -> Option<()> { - let updater = AutoUpdater::get(cx)?; - let version = updater.read(cx).current_version; - let should_show_notification = updater.read(cx).should_show_update_notification(cx); - - cx.spawn(|workspace, mut cx| async move { - let should_show_notification = should_show_notification.await?; - if should_show_notification { - workspace.update(&mut cx, |workspace, cx| { - let workspace_handle = workspace.weak_handle(); - workspace.show_notification( - NotificationId::unique::(), - cx, - |cx| cx.new_view(|_| UpdateNotification::new(version, workspace_handle)), - ); - updater.update(cx, |updater, cx| { - updater - .set_should_show_update_notification(false, cx) - .detach_and_log_err(cx); - }); - })?; - } - anyhow::Ok(()) - }) - .detach(); - - None -} - impl AutoUpdater { pub fn get(cx: &mut AppContext) -> Option> { cx.default_global::().0.clone() @@ -423,6 +279,10 @@ impl AutoUpdater { })); } + pub fn current_version(&self) -> SemanticVersion { + self.current_version + } + pub fn status(&self) -> AutoUpdateStatus { self.status.clone() } @@ -646,7 +506,7 @@ impl AutoUpdater { Ok(()) } - fn set_should_show_update_notification( + pub fn set_should_show_update_notification( &self, should_show: bool, cx: &AppContext, @@ -668,7 +528,7 @@ impl AutoUpdater { }) } - fn should_show_update_notification(&self, cx: &AppContext) -> Task> { + pub fn should_show_update_notification(&self, cx: &AppContext) -> Task> { cx.background_executor().spawn(async move { Ok(KEY_VALUE_STORE .read_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)? diff --git a/crates/auto_update_ui/Cargo.toml b/crates/auto_update_ui/Cargo.toml new file mode 100644 index 0000000000..1d62d295b7 --- /dev/null +++ b/crates/auto_update_ui/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "auto_update_ui" +version = "0.1.0" +edition = "2021" +publish = false +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/auto_update_ui.rs" + +[dependencies] +anyhow.workspace = true +auto_update.workspace = true +client.workspace = true +editor.workspace = true +gpui.workspace = true +http_client.workspace = true +markdown_preview.workspace = true +menu.workspace = true +release_channel.workspace = true +serde.workspace = true +serde_json.workspace = true +smol.workspace = true +util.workspace = true +workspace.workspace = true diff --git a/crates/auto_update_ui/LICENSE-GPL b/crates/auto_update_ui/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/auto_update_ui/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/auto_update_ui/src/auto_update_ui.rs b/crates/auto_update_ui/src/auto_update_ui.rs new file mode 100644 index 0000000000..9114375e88 --- /dev/null +++ b/crates/auto_update_ui/src/auto_update_ui.rs @@ -0,0 +1,147 @@ +mod update_notification; + +use auto_update::AutoUpdater; +use editor::{Editor, MultiBuffer}; +use gpui::{actions, prelude::*, AppContext, SharedString, View, ViewContext}; +use http_client::HttpClient; +use markdown_preview::markdown_preview_view::{MarkdownPreviewMode, MarkdownPreviewView}; +use release_channel::{AppVersion, ReleaseChannel}; +use serde::Deserialize; +use smol::io::AsyncReadExt; +use util::ResultExt as _; +use workspace::notifications::NotificationId; +use workspace::Workspace; + +use crate::update_notification::UpdateNotification; + +actions!(auto_update, [ViewReleaseNotesLocally]); + +pub fn init(cx: &mut AppContext) { + cx.observe_new_views(|workspace: &mut Workspace, _cx| { + workspace.register_action(|workspace, _: &ViewReleaseNotesLocally, cx| { + view_release_notes_locally(workspace, cx); + }); + }) + .detach(); +} + +#[derive(Deserialize)] +struct ReleaseNotesBody { + title: String, + release_notes: String, +} + +fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext) { + let release_channel = ReleaseChannel::global(cx); + + let url = match release_channel { + ReleaseChannel::Nightly => Some("https://github.com/zed-industries/zed/commits/nightly/"), + ReleaseChannel::Dev => Some("https://github.com/zed-industries/zed/commits/main/"), + _ => None, + }; + + if let Some(url) = url { + cx.open_url(url); + return; + } + + let version = AppVersion::global(cx).to_string(); + + let client = client::Client::global(cx).http_client(); + let url = client.build_url(&format!( + "/api/release_notes/v2/{}/{}", + release_channel.dev_name(), + version + )); + + let markdown = workspace + .app_state() + .languages + .language_for_name("Markdown"); + + workspace + .with_local_workspace(cx, move |_, cx| { + cx.spawn(|workspace, mut cx| async move { + let markdown = markdown.await.log_err(); + let response = client.get(&url, Default::default(), true).await; + let Some(mut response) = response.log_err() else { + return; + }; + + let mut body = Vec::new(); + response.body_mut().read_to_end(&mut body).await.ok(); + + let body: serde_json::Result = + serde_json::from_slice(body.as_slice()); + + if let Ok(body) = body { + workspace + .update(&mut cx, |workspace, cx| { + let project = workspace.project().clone(); + let buffer = project.update(cx, |project, cx| { + project.create_local_buffer("", markdown, cx) + }); + buffer.update(cx, |buffer, cx| { + buffer.edit([(0..0, body.release_notes)], None, cx) + }); + let language_registry = project.read(cx).languages().clone(); + + let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); + + let tab_description = SharedString::from(body.title.to_string()); + let editor = cx.new_view(|cx| { + Editor::for_multibuffer(buffer, Some(project), true, cx) + }); + let workspace_handle = workspace.weak_handle(); + let view: View = MarkdownPreviewView::new( + MarkdownPreviewMode::Default, + editor, + workspace_handle, + language_registry, + Some(tab_description), + cx, + ); + workspace.add_item_to_active_pane( + Box::new(view.clone()), + None, + true, + cx, + ); + cx.notify(); + }) + .log_err(); + } + }) + .detach(); + }) + .detach(); +} + +pub fn notify_of_any_new_update(cx: &mut ViewContext) -> Option<()> { + let updater = AutoUpdater::get(cx)?; + let version = updater.read(cx).current_version(); + let should_show_notification = updater.read(cx).should_show_update_notification(cx); + + cx.spawn(|workspace, mut cx| async move { + let should_show_notification = should_show_notification.await?; + if should_show_notification { + workspace.update(&mut cx, |workspace, cx| { + let workspace_handle = workspace.weak_handle(); + workspace.show_notification( + NotificationId::unique::(), + cx, + |cx| cx.new_view(|_| UpdateNotification::new(version, workspace_handle)), + ); + updater.update(cx, |updater, cx| { + updater + .set_should_show_update_notification(false, cx) + .detach_and_log_err(cx); + }); + })?; + } + anyhow::Ok(()) + }) + .detach(); + + None +} diff --git a/crates/auto_update/src/update_notification.rs b/crates/auto_update_ui/src/update_notification.rs similarity index 100% rename from crates/auto_update/src/update_notification.rs rename to crates/auto_update_ui/src/update_notification.rs diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 8d12d7b9f9..b55ebce2b9 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -23,6 +23,7 @@ assistant.workspace = true async-watch.workspace = true audio.workspace = true auto_update.workspace = true +auto_update_ui.workspace = true backtrace = "0.3" breadcrumbs.workspace = true call.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 9dbe00c617..f7aabb2626 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -367,6 +367,7 @@ fn main() { AppState::set_global(Arc::downgrade(&app_state), cx); auto_update::init(client.http_client(), cx); + auto_update_ui::init(cx); reliability::init( client.http_client(), system_id.as_ref().map(|id| id.to_string()), diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 73ecd00192..909afc207d 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -223,7 +223,7 @@ pub fn initialize_workspace( status_bar.add_right_item(cursor_position, cx); }); - auto_update::notify_of_any_new_update(cx); + auto_update_ui::notify_of_any_new_update(cx); let handle = cx.view().downgrade(); cx.on_window_should_close(move |cx| { From 6b2f1cc54341163f9f441d39ff067eeb50c0bd13 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 21 Nov 2024 14:33:58 -0500 Subject: [PATCH 46/68] title_bar: Remove dependency on `theme_selector` (#21009) This PR removes the `title_bar` crate's dependency on the `theme_selector`. The `theme_selector::Toggle` action now resides at `zed_actions::theme_selector::Toggle`. Release Notes: - N/A --- Cargo.lock | 2 +- crates/extensions_ui/src/extensions_ui.rs | 2 +- crates/theme_selector/Cargo.toml | 1 + crates/theme_selector/src/theme_selector.rs | 13 +++---------- crates/title_bar/Cargo.toml | 1 - crates/title_bar/src/title_bar.rs | 10 ++++++++-- crates/zed/src/zed/app_menus.rs | 5 ++++- crates/zed_actions/src/lib.rs | 13 +++++++++++++ 8 files changed, 31 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8888754a33..2bb4c4e0c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12410,6 +12410,7 @@ dependencies = [ "ui", "util", "workspace", + "zed_actions", ] [[package]] @@ -12637,7 +12638,6 @@ dependencies = [ "smallvec", "story", "theme", - "theme_selector", "tree-sitter-md", "ui", "util", diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs index 01e2b1dd66..1586f3546e 100644 --- a/crates/extensions_ui/src/extensions_ui.rs +++ b/crates/extensions_ui/src/extensions_ui.rs @@ -257,7 +257,7 @@ impl ExtensionsPage { .update(cx, |workspace, cx| { theme_selector::toggle( workspace, - &theme_selector::Toggle { + &zed_actions::theme_selector::Toggle { themes_filter: Some(themes), }, cx, diff --git a/crates/theme_selector/Cargo.toml b/crates/theme_selector/Cargo.toml index ec7e9aa877..dc0d5f3ac4 100644 --- a/crates/theme_selector/Cargo.toml +++ b/crates/theme_selector/Cargo.toml @@ -25,5 +25,6 @@ theme.workspace = true ui.workspace = true util.workspace = true workspace.workspace = true +zed_actions.workspace = true [dev-dependencies] diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index d0763c2793..e09ad40bf4 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -2,25 +2,18 @@ use client::telemetry::Telemetry; use fs::Fs; use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; use gpui::{ - actions, impl_actions, AppContext, DismissEvent, EventEmitter, FocusableView, Render, - UpdateGlobal, View, ViewContext, VisualContext, WeakView, + actions, AppContext, DismissEvent, EventEmitter, FocusableView, Render, UpdateGlobal, View, + ViewContext, VisualContext, WeakView, }; use picker::{Picker, PickerDelegate}; -use serde::Deserialize; use settings::{update_settings_file, SettingsStore}; use std::sync::Arc; use theme::{Appearance, Theme, ThemeMeta, ThemeRegistry, ThemeSettings}; use ui::{prelude::*, v_flex, ListItem, ListItemSpacing}; use util::ResultExt; use workspace::{ui::HighlightedLabel, ModalView, Workspace}; +use zed_actions::theme_selector::Toggle; -#[derive(PartialEq, Clone, Default, Debug, Deserialize)] -pub struct Toggle { - /// A list of theme names to filter the theme selector down to. - pub themes_filter: Option>, -} - -impl_actions!(theme_selector, [Toggle]); actions!(theme_selector, [Reload]); pub fn init(cx: &mut AppContext) { diff --git a/crates/title_bar/Cargo.toml b/crates/title_bar/Cargo.toml index 809915b4dc..75cb49b5a8 100644 --- a/crates/title_bar/Cargo.toml +++ b/crates/title_bar/Cargo.toml @@ -42,7 +42,6 @@ serde.workspace = true smallvec.workspace = true story = { workspace = true, optional = true } theme.workspace = true -theme_selector.workspace = true ui.workspace = true util.workspace = true vcs_menu.workspace = true diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index bcf13a5ac7..744f4ce26d 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -579,7 +579,10 @@ impl TitleBar { }) .action("Settings", zed_actions::OpenSettings.boxed_clone()) .action("Key Bindings", Box::new(zed_actions::OpenKeymap)) - .action("Themes…", theme_selector::Toggle::default().boxed_clone()) + .action( + "Themes…", + zed_actions::theme_selector::Toggle::default().boxed_clone(), + ) .action("Extensions", zed_actions::Extensions.boxed_clone()) .separator() .link( @@ -615,7 +618,10 @@ impl TitleBar { ContextMenu::build(cx, |menu, _| { menu.action("Settings", zed_actions::OpenSettings.boxed_clone()) .action("Key Bindings", Box::new(zed_actions::OpenKeymap)) - .action("Themes…", theme_selector::Toggle::default().boxed_clone()) + .action( + "Themes…", + zed_actions::theme_selector::Toggle::default().boxed_clone(), + ) .action("Extensions", zed_actions::Extensions.boxed_clone()) .separator() .link( diff --git a/crates/zed/src/zed/app_menus.rs b/crates/zed/src/zed/app_menus.rs index 4a2f351627..3affa31986 100644 --- a/crates/zed/src/zed/app_menus.rs +++ b/crates/zed/src/zed/app_menus.rs @@ -23,7 +23,10 @@ pub fn app_menus() -> Vec { zed_actions::OpenDefaultKeymap, ), MenuItem::action("Open Project Settings", super::OpenProjectSettings), - MenuItem::action("Select Theme...", theme_selector::Toggle::default()), + MenuItem::action( + "Select Theme...", + zed_actions::theme_selector::Toggle::default(), + ), ], }), MenuItem::separator(), diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index b777f03646..0e62e88fea 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -50,6 +50,19 @@ pub mod command_palette { actions!(command_palette, [Toggle]); } +pub mod theme_selector { + use gpui::impl_actions; + use serde::Deserialize; + + #[derive(PartialEq, Clone, Default, Debug, Deserialize)] + pub struct Toggle { + /// A list of theme names to filter the theme selector down to. + pub themes_filter: Option>, + } + + impl_actions!(theme_selector, [Toggle]); +} + #[derive(Clone, Default, Deserialize, PartialEq)] pub struct InlineAssist { pub prompt: Option, From 4c7b48b35d7e04e82a4551ffa5cafa2b42a7f684 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 21 Nov 2024 14:56:02 -0500 Subject: [PATCH 47/68] title_bar: Remove dependency on `vcs_menu` (#21011) This PR removes the `title_bar` crate's dependency on the `vcs_menu`. The `vcs_menu::OpenRecent` action now resides at `zed_actions::branches::OpenRecent`. Release Notes: - N/A --- Cargo.lock | 2 +- crates/title_bar/Cargo.toml | 1 - crates/title_bar/src/title_bar.rs | 7 +++---- crates/vcs_menu/Cargo.toml | 1 + crates/vcs_menu/src/lib.rs | 10 ++++------ crates/zed_actions/src/lib.rs | 6 ++++++ 6 files changed, 15 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2bb4c4e0c7..f4b1662a01 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12641,7 +12641,6 @@ dependencies = [ "tree-sitter-md", "ui", "util", - "vcs_menu", "windows 0.58.0", "workspace", "zed_actions", @@ -13653,6 +13652,7 @@ dependencies = [ "ui", "util", "workspace", + "zed_actions", ] [[package]] diff --git a/crates/title_bar/Cargo.toml b/crates/title_bar/Cargo.toml index 75cb49b5a8..8433ecad21 100644 --- a/crates/title_bar/Cargo.toml +++ b/crates/title_bar/Cargo.toml @@ -44,7 +44,6 @@ story = { workspace = true, optional = true } theme.workspace = true ui.workspace = true util.workspace = true -vcs_menu.workspace = true workspace.workspace = true zed_actions.workspace = true diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index 744f4ce26d..4e9a99433a 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -27,7 +27,6 @@ use ui::{ IconSize, IconWithIndicator, Indicator, PopoverMenu, Tooltip, }; use util::ResultExt; -use vcs_menu::{BranchList, OpenRecent as ToggleVcsMenu}; use workspace::{notifications::NotifyResultExt, Workspace}; use zed_actions::{OpenBrowser, OpenRecent, OpenRemote}; @@ -442,14 +441,14 @@ impl TitleBar { .tooltip(move |cx| { Tooltip::with_meta( "Recent Branches", - Some(&ToggleVcsMenu), + Some(&zed_actions::branches::OpenRecent), "Local branches only", cx, ) }) .on_click(move |_, cx| { - let _ = workspace.update(cx, |this, cx| { - BranchList::open(this, &Default::default(), cx); + let _ = workspace.update(cx, |_this, cx| { + cx.dispatch_action(zed_actions::branches::OpenRecent.boxed_clone()); }); }), ) diff --git a/crates/vcs_menu/Cargo.toml b/crates/vcs_menu/Cargo.toml index 11de371868..47bf3d8984 100644 --- a/crates/vcs_menu/Cargo.toml +++ b/crates/vcs_menu/Cargo.toml @@ -18,3 +18,4 @@ project.workspace = true ui.workspace = true util.workspace = true workspace.workspace = true +zed_actions.workspace = true diff --git a/crates/vcs_menu/src/lib.rs b/crates/vcs_menu/src/lib.rs index f165c91bfe..f61bad57fa 100644 --- a/crates/vcs_menu/src/lib.rs +++ b/crates/vcs_menu/src/lib.rs @@ -2,10 +2,9 @@ use anyhow::{anyhow, Context, Result}; use fuzzy::{StringMatch, StringMatchCandidate}; use git::repository::Branch; use gpui::{ - actions, rems, AnyElement, AppContext, AsyncAppContext, DismissEvent, EventEmitter, - FocusHandle, FocusableView, InteractiveElement, IntoElement, ParentElement, Render, - SharedString, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, - WindowContext, + rems, AnyElement, AppContext, AsyncAppContext, DismissEvent, EventEmitter, FocusHandle, + FocusableView, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, + Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowContext, }; use picker::{Picker, PickerDelegate}; use project::ProjectPath; @@ -14,8 +13,7 @@ use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing}; use util::ResultExt; use workspace::notifications::DetachAndPromptErr; use workspace::{ModalView, Workspace}; - -actions!(branches, [OpenRecent]); +use zed_actions::branches::OpenRecent; pub fn init(cx: &mut AppContext) { cx.observe_new_views(|workspace: &mut Workspace, _| { diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index 0e62e88fea..848412c2a3 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -44,6 +44,12 @@ actions!( ] ); +pub mod branches { + use gpui::actions; + + actions!(branches, [OpenRecent]); +} + pub mod command_palette { use gpui::actions; From 614b3b979b7373aaa6dee84dfbc824fce1a86ea8 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Thu, 21 Nov 2024 20:03:50 +0000 Subject: [PATCH 48/68] macos: Add default keybind for ctrl-home / ctrl-end (#21007) This matches the default behavior on native macos apps. ctrl-fn-left == ctrl-home == MoveToBeginning ctrl-fn-right == ctrl-end == MoveToEnd --- assets/keymaps/default-macos.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 5b416db9b2..025ba4d69d 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -93,6 +93,8 @@ "ctrl-e": "editor::MoveToEndOfLine", "cmd-up": "editor::MoveToBeginning", "cmd-down": "editor::MoveToEnd", + "ctrl-home": "editor::MoveToBeginning", + "ctrl-end": "editor::MoveToEnd", "shift-up": "editor::SelectUp", "ctrl-shift-p": "editor::SelectUp", "shift-down": "editor::SelectDown", From 2868b67286b60e3b4f9126e3f146a218cfd962c0 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 21 Nov 2024 15:24:04 -0500 Subject: [PATCH 49/68] title_bar: Remove dependency on `feedback` (#21013) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR removes the `title_bar` crate's dependency on the `feedback` crate. The `feedback::GiveFeedback` action now resides at `zed_actions::feedback::GiveFeedback`. `title_bar` now no longer depends on `editor` 🥳 Release Notes: - N/A --- Cargo.lock | 3 +-- crates/feedback/Cargo.toml | 3 ++- crates/feedback/src/feedback.rs | 2 -- crates/feedback/src/feedback_modal.rs | 3 ++- crates/title_bar/Cargo.toml | 3 --- crates/title_bar/src/application_menu.rs | 5 ++++- crates/zed/src/zed/app_menus.rs | 2 +- crates/zed_actions/src/lib.rs | 6 ++++++ 8 files changed, 16 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f4b1662a01..9ddbe6dfaa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4347,6 +4347,7 @@ dependencies = [ "urlencoding", "util", "workspace", + "zed_actions", ] [[package]] @@ -12623,9 +12624,7 @@ dependencies = [ "call", "client", "collections", - "editor", "feature_flags", - "feedback", "gpui", "http_client", "notifications", diff --git a/crates/feedback/Cargo.toml b/crates/feedback/Cargo.toml index 0447858ca5..605b572c6c 100644 --- a/crates/feedback/Cargo.toml +++ b/crates/feedback/Cargo.toml @@ -22,8 +22,8 @@ db.workspace = true editor.workspace = true futures.workspace = true gpui.workspace = true -human_bytes = "0.4.1" http_client.workspace = true +human_bytes = "0.4.1" language.workspace = true log.workspace = true menu.workspace = true @@ -39,6 +39,7 @@ ui.workspace = true urlencoding = "2.1.2" util.workspace = true workspace.workspace = true +zed_actions.workspace = true [dev-dependencies] editor = { workspace = true, features = ["test-support"] } diff --git a/crates/feedback/src/feedback.rs b/crates/feedback/src/feedback.rs index 671dea8689..f802a0950d 100644 --- a/crates/feedback/src/feedback.rs +++ b/crates/feedback/src/feedback.rs @@ -5,8 +5,6 @@ use workspace::Workspace; pub mod feedback_modal; -actions!(feedback, [GiveFeedback, SubmitFeedback]); - mod system_specs; actions!( diff --git a/crates/feedback/src/feedback_modal.rs b/crates/feedback/src/feedback_modal.rs index 5270492aee..2c98267ccf 100644 --- a/crates/feedback/src/feedback_modal.rs +++ b/crates/feedback/src/feedback_modal.rs @@ -18,8 +18,9 @@ use serde_derive::Serialize; use ui::{prelude::*, Button, ButtonStyle, IconPosition, Tooltip}; use util::ResultExt; use workspace::{DismissDecision, ModalView, Workspace}; +use zed_actions::feedback::GiveFeedback; -use crate::{system_specs::SystemSpecs, GiveFeedback, OpenZedRepo}; +use crate::{system_specs::SystemSpecs, OpenZedRepo}; // For UI testing purposes const SEND_SUCCESS_IN_DEV_MODE: bool = true; diff --git a/crates/title_bar/Cargo.toml b/crates/title_bar/Cargo.toml index 8433ecad21..0a2878b357 100644 --- a/crates/title_bar/Cargo.toml +++ b/crates/title_bar/Cargo.toml @@ -19,7 +19,6 @@ test-support = [ "call/test-support", "client/test-support", "collections/test-support", - "editor/test-support", "gpui/test-support", "http_client/test-support", "project/test-support", @@ -31,7 +30,6 @@ test-support = [ auto_update.workspace = true call.workspace = true client.workspace = true -feedback.workspace = true feature_flags.workspace = true gpui.workspace = true notifications.workspace = true @@ -54,7 +52,6 @@ windows.workspace = true call = { workspace = true, features = ["test-support"] } client = { workspace = true, features = ["test-support"] } collections = { workspace = true, features = ["test-support"] } -editor = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] } http_client = { workspace = true, features = ["test-support"] } notifications = { workspace = true, features = ["test-support"] } diff --git a/crates/title_bar/src/application_menu.rs b/crates/title_bar/src/application_menu.rs index 3d5a774e8f..ef13655bdb 100644 --- a/crates/title_bar/src/application_menu.rs +++ b/crates/title_bar/src/application_menu.rs @@ -116,7 +116,10 @@ impl Render for ApplicationMenu { url: "https://zed.dev/docs".into(), }), ) - .action("Give Feedback", Box::new(feedback::GiveFeedback)) + .action( + "Give Feedback", + Box::new(zed_actions::feedback::GiveFeedback), + ) .action("Check for Updates", Box::new(auto_update::Check)) .action("View Telemetry", Box::new(zed_actions::OpenTelemetryLog)) .action( diff --git a/crates/zed/src/zed/app_menus.rs b/crates/zed/src/zed/app_menus.rs index 3affa31986..8586df57f2 100644 --- a/crates/zed/src/zed/app_menus.rs +++ b/crates/zed/src/zed/app_menus.rs @@ -179,7 +179,7 @@ pub fn app_menus() -> Vec { MenuItem::action("View Telemetry", zed_actions::OpenTelemetryLog), MenuItem::action("View Dependency Licenses", zed_actions::OpenLicenses), MenuItem::action("Show Welcome", workspace::Welcome), - MenuItem::action("Give Feedback...", feedback::GiveFeedback), + MenuItem::action("Give Feedback...", zed_actions::feedback::GiveFeedback), MenuItem::separator(), MenuItem::action( "Documentation", diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index 848412c2a3..b4bb6d2152 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -56,6 +56,12 @@ pub mod command_palette { actions!(command_palette, [Toggle]); } +pub mod feedback { + use gpui::actions; + + actions!(feedback, [GiveFeedback]); +} + pub mod theme_selector { use gpui::impl_actions; use serde::Deserialize; From 790fdcf737e8103140e654be8162c3f0c48f587c Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 21 Nov 2024 15:48:35 -0500 Subject: [PATCH 50/68] collab_ui: Remove dependency on `vcs_menu` (#21016) This PR removes the `vcs_menu` dependency from `collab_ui`. We were only depending on this to call `vcs_menu::init`, which isn't necessary to do here. Release Notes: - N/A --- Cargo.lock | 2 +- crates/collab_ui/Cargo.toml | 3 +-- crates/collab_ui/src/collab_ui.rs | 1 - crates/zed/Cargo.toml | 5 +++-- crates/zed/src/main.rs | 1 + 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9ddbe6dfaa..07af53a6c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2712,7 +2712,6 @@ dependencies = [ "tree-sitter-md", "ui", "util", - "vcs_menu", "workspace", ] @@ -15574,6 +15573,7 @@ dependencies = [ "urlencoding", "util", "uuid", + "vcs_menu", "vim", "welcome", "windows 0.58.0", diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index cd00e13206..3cc8f25b18 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -58,12 +58,11 @@ settings.workspace = true smallvec.workspace = true story = { workspace = true, optional = true } theme.workspace = true -time_format.workspace = true time.workspace = true +time_format.workspace = true title_bar.workspace = true ui.workspace = true util.workspace = true -vcs_menu.workspace = true workspace.workspace = true [dev-dependencies] diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index 2baaa01490..67c4ad6dad 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -33,7 +33,6 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { notification_panel::init(cx); notifications::init(app_state, cx); title_bar::init(cx); - vcs_menu::init(cx); } fn notification_window_options( diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index b55ebce2b9..0eef53bd9e 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -15,11 +15,11 @@ name = "zed" path = "src/main.rs" [dependencies] -assistant_slash_command.workspace = true activity_indicator.workspace = true anyhow.workspace = true assets.workspace = true assistant.workspace = true +assistant_slash_command.workspace = true async-watch.workspace = true audio.workspace = true auto_update.workspace = true @@ -88,6 +88,7 @@ recent_projects.workspace = true release_channel.workspace = true remote.workspace = true repl.workspace = true +reqwest_client.workspace = true rope.workspace = true search.workspace = true serde.workspace = true @@ -112,11 +113,11 @@ theme_selector.workspace = true time.workspace = true toolchain_selector.workspace = true ui.workspace = true -reqwest_client.workspace = true url.workspace = true urlencoding = "2.1.2" util.workspace = true uuid.workspace = true +vcs_menu.workspace = true vim.workspace = true welcome.workspace = true workspace.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index f7aabb2626..b1b721c7c6 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -462,6 +462,7 @@ fn main() { call::init(app_state.client.clone(), app_state.user_store.clone(), cx); notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx); collab_ui::init(&app_state, cx); + vcs_menu::init(cx); feedback::init(cx); markdown_preview::init(cx); welcome::init(cx); From b102a40e045a96591b86a5f0171a28234b1f1434 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 21 Nov 2024 16:24:38 -0500 Subject: [PATCH 51/68] Extract `VimModeSetting` to its own crate (#21019) This PR extracts the `VimModeSetting` out of the `vim` crate and into its own `vim_mode_setting` crate. A number of crates were depending on the entirety of the `vim` crate just to reference `VimModeSetting`, which was not ideal. Release Notes: - N/A --- Cargo.lock | 15 ++++++-- Cargo.toml | 2 ++ crates/extensions_ui/Cargo.toml | 2 +- crates/extensions_ui/src/extensions_ui.rs | 2 +- crates/vim/Cargo.toml | 1 + crates/vim/src/vim.rs | 25 ++----------- crates/vim_mode_setting/Cargo.toml | 17 +++++++++ crates/vim_mode_setting/LICENSE-GPL | 1 + .../vim_mode_setting/src/vim_mode_setting.rs | 36 +++++++++++++++++++ crates/welcome/Cargo.toml | 2 +- crates/welcome/src/welcome.rs | 2 +- crates/zed/Cargo.toml | 1 + crates/zed/src/zed.rs | 18 +++++----- 13 files changed, 85 insertions(+), 39 deletions(-) create mode 100644 crates/vim_mode_setting/Cargo.toml create mode 120000 crates/vim_mode_setting/LICENSE-GPL create mode 100644 crates/vim_mode_setting/src/vim_mode_setting.rs diff --git a/Cargo.lock b/Cargo.lock index 07af53a6c5..66155eda62 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4236,7 +4236,7 @@ dependencies = [ "theme_selector", "ui", "util", - "vim", + "vim_mode_setting", "wasmtime-wasi", "workspace", "zed_actions", @@ -13697,10 +13697,20 @@ dependencies = [ "tokio", "ui", "util", + "vim_mode_setting", "workspace", "zed_actions", ] +[[package]] +name = "vim_mode_setting" +version = "0.1.0" +dependencies = [ + "anyhow", + "gpui", + "settings", +] + [[package]] name = "vscode_theme" version = "0.2.0" @@ -14415,7 +14425,7 @@ dependencies = [ "theme_selector", "ui", "util", - "vim", + "vim_mode_setting", "workspace", "zed_actions", ] @@ -15575,6 +15585,7 @@ dependencies = [ "uuid", "vcs_menu", "vim", + "vim_mode_setting", "welcome", "windows 0.58.0", "winresource", diff --git a/Cargo.toml b/Cargo.toml index b1feec52ef..58239800da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -129,6 +129,7 @@ members = [ "crates/util", "crates/vcs_menu", "crates/vim", + "crates/vim_mode_setting", "crates/welcome", "crates/workspace", "crates/worktree", @@ -304,6 +305,7 @@ ui_macros = { path = "crates/ui_macros" } util = { path = "crates/util" } vcs_menu = { path = "crates/vcs_menu" } vim = { path = "crates/vim" } +vim_mode_setting = { path = "crates/vim_mode_setting" } welcome = { path = "crates/welcome" } workspace = { path = "crates/workspace" } worktree = { path = "crates/worktree" } diff --git a/crates/extensions_ui/Cargo.toml b/crates/extensions_ui/Cargo.toml index 2ff2f21696..ce345ca2db 100644 --- a/crates/extensions_ui/Cargo.toml +++ b/crates/extensions_ui/Cargo.toml @@ -41,7 +41,7 @@ theme.workspace = true theme_selector.workspace = true ui.workspace = true util.workspace = true -vim.workspace = true +vim_mode_setting.workspace = true wasmtime-wasi.workspace = true workspace.workspace = true zed_actions.workspace = true diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs index 1586f3546e..ac2e147796 100644 --- a/crates/extensions_ui/src/extensions_ui.rs +++ b/crates/extensions_ui/src/extensions_ui.rs @@ -27,7 +27,7 @@ use release_channel::ReleaseChannel; use settings::Settings; use theme::ThemeSettings; use ui::{prelude::*, CheckboxWithLabel, ContextMenu, PopoverMenu, ToggleButton, Tooltip}; -use vim::VimModeSetting; +use vim_mode_setting::VimModeSetting; use workspace::{ item::{Item, ItemEvent}, Workspace, WorkspaceId, diff --git a/crates/vim/Cargo.toml b/crates/vim/Cargo.toml index fddb607c1f..ddf738d067 100644 --- a/crates/vim/Cargo.toml +++ b/crates/vim/Cargo.toml @@ -39,6 +39,7 @@ settings.workspace = true tokio = { version = "1.15", features = ["full"], optional = true } ui.workspace = true util.workspace = true +vim_mode_setting.workspace = true workspace.workspace = true zed_actions.workspace = true diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 77fc7db9d6..dd3bf297cb 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -41,15 +41,11 @@ use state::{Mode, Operator, RecordedSelection, SearchState, VimGlobals}; use std::{mem, ops::Range, sync::Arc}; use surrounds::SurroundsType; use ui::{IntoElement, VisualContext}; +use vim_mode_setting::VimModeSetting; use workspace::{self, Pane, Workspace}; use crate::state::ReplayableAction; -/// Whether or not to enable Vim mode. -/// -/// Default: false -pub struct VimModeSetting(pub bool); - /// An Action to Switch between modes #[derive(Clone, Deserialize, PartialEq)] pub struct SwitchMode(pub Mode); @@ -89,7 +85,7 @@ impl_actions!(vim, [SwitchMode, PushOperator, Number, SelectRegister]); /// Initializes the `vim` crate. pub fn init(cx: &mut AppContext) { - VimModeSetting::register(cx); + vim_mode_setting::init(cx); VimSettings::register(cx); VimGlobals::register(cx); @@ -1122,23 +1118,6 @@ impl Vim { } } -impl Settings for VimModeSetting { - const KEY: Option<&'static str> = Some("vim_mode"); - - type FileContent = Option; - - fn load(sources: SettingsSources, _: &mut AppContext) -> Result { - Ok(Self( - sources - .user - .or(sources.server) - .copied() - .flatten() - .unwrap_or(sources.default.ok_or_else(Self::missing_default)?), - )) - } -} - /// Controls when to use system clipboard. #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] diff --git a/crates/vim_mode_setting/Cargo.toml b/crates/vim_mode_setting/Cargo.toml new file mode 100644 index 0000000000..0c009fdfd6 --- /dev/null +++ b/crates/vim_mode_setting/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "vim_mode_setting" +version = "0.1.0" +edition = "2021" +publish = false +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/vim_mode_setting.rs" + +[dependencies] +anyhow.workspace = true +gpui.workspace = true +settings.workspace = true diff --git a/crates/vim_mode_setting/LICENSE-GPL b/crates/vim_mode_setting/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/vim_mode_setting/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/vim_mode_setting/src/vim_mode_setting.rs b/crates/vim_mode_setting/src/vim_mode_setting.rs new file mode 100644 index 0000000000..072db138df --- /dev/null +++ b/crates/vim_mode_setting/src/vim_mode_setting.rs @@ -0,0 +1,36 @@ +//! Contains the [`VimModeSetting`] used to enable/disable Vim mode. +//! +//! This is in its own crate as we want other crates to be able to enable or +//! disable Vim mode without having to depend on the `vim` crate in its +//! entirety. + +use anyhow::Result; +use gpui::AppContext; +use settings::{Settings, SettingsSources}; + +/// Initializes the `vim_mode_setting` crate. +pub fn init(cx: &mut AppContext) { + VimModeSetting::register(cx); +} + +/// Whether or not to enable Vim mode. +/// +/// Default: false +pub struct VimModeSetting(pub bool); + +impl Settings for VimModeSetting { + const KEY: Option<&'static str> = Some("vim_mode"); + + type FileContent = Option; + + fn load(sources: SettingsSources, _: &mut AppContext) -> Result { + Ok(Self( + sources + .user + .or(sources.server) + .copied() + .flatten() + .unwrap_or(sources.default.ok_or_else(Self::missing_default)?), + )) + } +} diff --git a/crates/welcome/Cargo.toml b/crates/welcome/Cargo.toml index 8ec245290d..26fe379ec6 100644 --- a/crates/welcome/Cargo.toml +++ b/crates/welcome/Cargo.toml @@ -30,7 +30,7 @@ settings.workspace = true theme_selector.workspace = true ui.workspace = true util.workspace = true -vim.workspace = true +vim_mode_setting.workspace = true workspace.workspace = true zed_actions.workspace = true diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index 0d1e1c24d1..e66feec768 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -12,7 +12,7 @@ use gpui::{ use settings::{Settings, SettingsStore}; use std::sync::Arc; use ui::{prelude::*, CheckboxWithLabel}; -use vim::VimModeSetting; +use vim_mode_setting::VimModeSetting; use workspace::{ dock::DockPosition, item::{Item, ItemEvent}, diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 0eef53bd9e..6c447bcabe 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -119,6 +119,7 @@ util.workspace = true uuid.workspace = true vcs_menu.workspace = true vim.workspace = true +vim_mode_setting.workspace = true welcome.workspace = true workspace.workspace = true zed_actions.workspace = true diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 909afc207d..b2dbc087b0 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -9,7 +9,9 @@ mod open_listener; #[cfg(target_os = "windows")] pub(crate) mod windows_only_instance; +use anyhow::Context as _; pub use app_menus::*; +use assets::Assets; use assistant::PromptBuilder; use breadcrumbs::Breadcrumbs; use client::{zed_urls, ZED_URL_SCHEME}; @@ -18,17 +20,15 @@ use command_palette_hooks::CommandPaletteFilter; use editor::ProposedChangesEditorToolbar; use editor::{scroll::Autoscroll, Editor, MultiBuffer}; use feature_flags::FeatureFlagAppExt; +use futures::{channel::mpsc, select_biased, StreamExt}; use gpui::{ actions, point, px, AppContext, AsyncAppContext, Context, FocusableView, MenuItem, PathPromptOptions, PromptLevel, ReadGlobal, Task, TitlebarOptions, View, ViewContext, VisualContext, WindowKind, WindowOptions, }; pub use open_listener::*; - -use anyhow::Context as _; -use assets::Assets; -use futures::{channel::mpsc, select_biased, StreamExt}; use outline_panel::OutlinePanel; +use paths::{local_settings_file_relative_path, local_tasks_file_relative_path}; use project::{DirectoryLister, Item}; use project_panel::ProjectPanel; use quick_action_bar::QuickActionBar; @@ -43,16 +43,14 @@ use settings::{ use std::any::TypeId; use std::path::PathBuf; use std::{borrow::Cow, ops::Deref, path::Path, sync::Arc}; -use theme::ActiveTheme; -use workspace::notifications::NotificationId; -use workspace::CloseIntent; - -use paths::{local_settings_file_relative_path, local_tasks_file_relative_path}; use terminal_view::terminal_panel::{self, TerminalPanel}; +use theme::ActiveTheme; use util::{asset_str, ResultExt}; use uuid::Uuid; -use vim::VimModeSetting; +use vim_mode_setting::VimModeSetting; use welcome::{BaseKeymap, MultibufferHint}; +use workspace::notifications::NotificationId; +use workspace::CloseIntent; use workspace::{ create_and_open_local_file, notifications::simple_message_notification::MessageNotification, open_new, AppState, NewFile, NewWindow, OpenLog, Toast, Workspace, WorkspaceSettings, From af34953bc33ffb085c3c59df1edbbfc105f49409 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 21 Nov 2024 16:48:25 -0500 Subject: [PATCH 52/68] extensions_ui: Remove dependency on `theme_selector` (#21023) This PR removes the dependency on `theme_selector` from `extensions_ui`, as we can just dispatch the action instead. Release Notes: - N/A --- Cargo.lock | 1 - crates/extensions_ui/Cargo.toml | 1 - crates/extensions_ui/src/extensions_ui.rs | 19 +++++++++---------- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 66155eda62..34bccbc8b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4233,7 +4233,6 @@ dependencies = [ "smallvec", "snippet_provider", "theme", - "theme_selector", "ui", "util", "vim_mode_setting", diff --git a/crates/extensions_ui/Cargo.toml b/crates/extensions_ui/Cargo.toml index ce345ca2db..e8de7c3f12 100644 --- a/crates/extensions_ui/Cargo.toml +++ b/crates/extensions_ui/Cargo.toml @@ -38,7 +38,6 @@ settings.workspace = true smallvec.workspace = true snippet_provider.workspace = true theme.workspace = true -theme_selector.workspace = true ui.workspace = true util.workspace = true vim_mode_setting.workspace = true diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs index ac2e147796..077d80b9b8 100644 --- a/crates/extensions_ui/src/extensions_ui.rs +++ b/crates/extensions_ui/src/extensions_ui.rs @@ -17,9 +17,9 @@ use editor::{Editor, EditorElement, EditorStyle}; use extension_host::{ExtensionManifest, ExtensionOperation, ExtensionStore}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ - actions, uniform_list, AppContext, EventEmitter, Flatten, FocusableView, InteractiveElement, - KeyContext, ParentElement, Render, Styled, Task, TextStyle, UniformListScrollHandle, View, - ViewContext, VisualContext, WeakView, WindowContext, + actions, uniform_list, Action, AppContext, EventEmitter, Flatten, FocusableView, + InteractiveElement, KeyContext, ParentElement, Render, Styled, Task, TextStyle, + UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WindowContext, }; use num_format::{Locale, ToFormattedString}; use project::DirectoryLister; @@ -254,14 +254,13 @@ impl ExtensionsPage { .collect::>(); if !themes.is_empty() { workspace - .update(cx, |workspace, cx| { - theme_selector::toggle( - workspace, - &zed_actions::theme_selector::Toggle { + .update(cx, |_workspace, cx| { + cx.dispatch_action( + zed_actions::theme_selector::Toggle { themes_filter: Some(themes), - }, - cx, - ) + } + .boxed_clone(), + ); }) .ok(); } From f74f670865956e95a4e852e248beb1b14df34008 Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Thu, 21 Nov 2024 14:50:38 -0700 Subject: [PATCH 53/68] Fix panics from spawn_local tasks dropped on other threads in remote server (#21022) Closes #21020 Release Notes: - Fixed remote server panic of "local task dropped by a thread that didn't spawn it" --- crates/remote/src/ssh_session.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index d8c852c019..546135c30b 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -1074,7 +1074,7 @@ impl SshRemoteClient { c.connections.insert( opts.clone(), ConnectionPoolEntry::Connecting( - cx.foreground_executor() + cx.background_executor() .spawn({ let connection = connection.clone(); async move { Ok(connection.clone()) } From 72613b7668f2291cfc23d388795fc1eceaa991a1 Mon Sep 17 00:00:00 2001 From: Kyle Kelley Date: Thu, 21 Nov 2024 14:00:19 -0800 Subject: [PATCH 54/68] Implement RunningKernel trait for native and remote kernels (#20934) This PR introduces a unified interface for both native and remote kernels through the `RunningKernel` trait. When either the native kernel or the remote kernels are started, they return a `Box` to make it easier to work with the session. As a bonus of this refactor, I've dropped some of the mpsc channels to instead opt for passing messages directly to `session.route(message)`. There was a lot of simplification of `Session` by moving responsibilities to `NativeRunningKernel`. No release notes yet until this is finalized. * [x] Detect remote kernelspecs from configured remote servers * [x] Launch kernel on demand For now, this allows you to set env vars `JUPYTER_SERVER` and `JUPYTER_TOKEN` to access a remote server. `JUPYTER_SERVER` should be a base path like `http://localhost:8888` or `https://notebooks.gesis.org/binder/jupyter/user/rubydata-binder-w6igpy4l/` Release Notes: - N/A --- Cargo.lock | 1 + crates/repl/Cargo.toml | 2 + crates/repl/src/components/kernel_options.rs | 83 +++++-- crates/repl/src/kernels/mod.rs | 19 +- crates/repl/src/kernels/native_kernel.rs | 148 +++++++++--- crates/repl/src/kernels/remote_kernels.rs | 227 ++++++++++++++++--- crates/repl/src/repl_store.rs | 48 +++- crates/repl/src/session.rs | 180 ++++----------- 8 files changed, 478 insertions(+), 230 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 34bccbc8b4..d950324d2f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9926,6 +9926,7 @@ dependencies = [ "editor", "env_logger 0.11.5", "feature_flags", + "file_icons", "futures 0.3.31", "gpui", "http_client", diff --git a/crates/repl/Cargo.toml b/crates/repl/Cargo.toml index 60e8734771..293a58e762 100644 --- a/crates/repl/Cargo.toml +++ b/crates/repl/Cargo.toml @@ -22,8 +22,10 @@ collections.workspace = true command_palette_hooks.workspace = true editor.workspace = true feature_flags.workspace = true +file_icons.workspace = true futures.workspace = true gpui.workspace = true +http_client.workspace = true image.workspace = true jupyter-websocket-client.workspace = true jupyter-protocol.workspace = true diff --git a/crates/repl/src/components/kernel_options.rs b/crates/repl/src/components/kernel_options.rs index fc0213e54e..8fd9b412ea 100644 --- a/crates/repl/src/components/kernel_options.rs +++ b/crates/repl/src/components/kernel_options.rs @@ -34,6 +34,16 @@ pub struct KernelPickerDelegate { on_select: OnSelect, } +// Helper function to truncate long paths +fn truncate_path(path: &SharedString, max_length: usize) -> SharedString { + if path.len() <= max_length { + path.to_string().into() + } else { + let truncated = path.chars().rev().take(max_length - 3).collect::(); + format!("...{}", truncated.chars().rev().collect::()).into() + } +} + impl KernelSelector { pub fn new(on_select: OnSelect, worktree_id: WorktreeId, trigger: T) -> Self { KernelSelector { @@ -116,11 +126,25 @@ impl PickerDelegate for KernelPickerDelegate { &self, ix: usize, selected: bool, - _cx: &mut ViewContext>, + cx: &mut ViewContext>, ) -> Option { let kernelspec = self.filtered_kernels.get(ix)?; - let is_selected = self.selected_kernelspec.as_ref() == Some(kernelspec); + let icon = kernelspec.icon(cx); + + let (name, kernel_type, path_or_url) = match kernelspec { + KernelSpecification::Jupyter(_) => (kernelspec.name(), "Jupyter", None), + KernelSpecification::PythonEnv(_) => ( + kernelspec.name(), + "Python Env", + Some(truncate_path(&kernelspec.path(), 42)), + ), + KernelSpecification::Remote(_) => ( + kernelspec.name(), + "Remote", + Some(truncate_path(&kernelspec.path(), 42)), + ), + }; Some( ListItem::new(ix) @@ -128,25 +152,46 @@ impl PickerDelegate for KernelPickerDelegate { .spacing(ListItemSpacing::Sparse) .selected(selected) .child( - v_flex() - .min_w(px(600.)) + h_flex() .w_full() - .gap_0p5() + .gap_3() + .child(icon.color(Color::Default).size(IconSize::Medium)) .child( - h_flex() - .w_full() - .gap_1() - .child(Label::new(kernelspec.name()).weight(FontWeight::MEDIUM)) + v_flex() + .flex_grow() + .gap_0p5() .child( - Label::new(kernelspec.language()) - .size(LabelSize::Small) - .color(Color::Muted), + h_flex() + .justify_between() + .child( + div().w_48().text_ellipsis().child( + Label::new(name) + .weight(FontWeight::MEDIUM) + .size(LabelSize::Default), + ), + ) + .when_some(path_or_url.clone(), |flex, path| { + flex.text_ellipsis().child( + Label::new(path) + .size(LabelSize::Small) + .color(Color::Muted), + ) + }), + ) + .child( + h_flex() + .gap_1() + .child( + Label::new(kernelspec.language()) + .size(LabelSize::Small) + .color(Color::Muted), + ) + .child( + Label::new(kernel_type) + .size(LabelSize::Small) + .color(Color::Muted), + ), ), - ) - .child( - Label::new(kernelspec.path()) - .size(LabelSize::XSmall) - .color(Color::Muted), ), ) .when(is_selected, |item| { @@ -199,7 +244,9 @@ impl RenderOnce for KernelSelector { }; let picker_view = cx.new_view(|cx| { - let picker = Picker::uniform_list(delegate, cx).max_height(Some(rems(20.).into())); + let picker = Picker::uniform_list(delegate, cx) + .width(rems(30.)) + .max_height(Some(rems(20.).into())); picker }); diff --git a/crates/repl/src/kernels/mod.rs b/crates/repl/src/kernels/mod.rs index 3fe4c3c12d..47fde97154 100644 --- a/crates/repl/src/kernels/mod.rs +++ b/crates/repl/src/kernels/mod.rs @@ -6,7 +6,7 @@ use futures::{ future::Shared, stream, }; -use gpui::{AppContext, Model, Task}; +use gpui::{AppContext, Model, Task, WindowContext}; use language::LanguageName; pub use native_kernel::*; @@ -16,7 +16,7 @@ pub use remote_kernels::*; use anyhow::Result; use runtimelib::{ExecutionState, JupyterKernelspec, JupyterMessage, KernelInfoReply}; -use ui::SharedString; +use ui::{Icon, IconName, SharedString}; pub type JupyterMessageChannel = stream::SelectAll>; @@ -59,6 +59,19 @@ impl KernelSpecification { Self::Remote(spec) => spec.kernelspec.language.clone(), }) } + + pub fn icon(&self, cx: &AppContext) -> Icon { + let lang_name = match self { + Self::Jupyter(spec) => spec.kernelspec.language.clone(), + Self::PythonEnv(spec) => spec.kernelspec.language.clone(), + Self::Remote(spec) => spec.kernelspec.language.clone(), + }; + + file_icons::FileIcons::get(cx) + .get_type_icon(&lang_name.to_lowercase()) + .map(Icon::from_path) + .unwrap_or(Icon::new(IconName::ReplNeutral)) + } } pub fn python_env_kernel_specifications( @@ -134,7 +147,7 @@ pub trait RunningKernel: Send + Debug { fn set_execution_state(&mut self, state: ExecutionState); fn kernel_info(&self) -> Option<&KernelInfoReply>; fn set_kernel_info(&mut self, info: KernelInfoReply); - fn force_shutdown(&mut self) -> anyhow::Result<()>; + fn force_shutdown(&mut self, cx: &mut WindowContext) -> Task>; } #[derive(Debug, Clone)] diff --git a/crates/repl/src/kernels/native_kernel.rs b/crates/repl/src/kernels/native_kernel.rs index 03a57b34ef..6f7c5d92ee 100644 --- a/crates/repl/src/kernels/native_kernel.rs +++ b/crates/repl/src/kernels/native_kernel.rs @@ -1,10 +1,11 @@ use anyhow::{Context as _, Result}; use futures::{ channel::mpsc::{self}, + io::BufReader, stream::{SelectAll, StreamExt}, - SinkExt as _, + AsyncBufReadExt as _, SinkExt as _, }; -use gpui::{AppContext, EntityId, Task}; +use gpui::{EntityId, Task, View, WindowContext}; use jupyter_protocol::{JupyterMessage, JupyterMessageContent, KernelInfoReply}; use project::Fs; use runtimelib::{dirs, ConnectionInfo, ExecutionState, JupyterKernelspec}; @@ -18,7 +19,9 @@ use std::{ }; use uuid::Uuid; -use super::{JupyterMessageChannel, RunningKernel}; +use crate::Session; + +use super::RunningKernel; #[derive(Debug, Clone)] pub struct LocalKernelSpecification { @@ -83,10 +86,10 @@ async fn peek_ports(ip: IpAddr) -> Result<[u16; 5]> { pub struct NativeRunningKernel { pub process: smol::process::Child, _shell_task: Task>, - _iopub_task: Task>, _control_task: Task>, _routing_task: Task>, connection_path: PathBuf, + _process_status_task: Option>, pub working_directory: PathBuf, pub request_tx: mpsc::Sender, pub execution_state: ExecutionState, @@ -107,8 +110,10 @@ impl NativeRunningKernel { entity_id: EntityId, working_directory: PathBuf, fs: Arc, - cx: &mut AppContext, - ) -> Task> { + // todo: convert to weak view + session: View, + cx: &mut WindowContext, + ) -> Task>> { cx.spawn(|cx| async move { let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); let ports = peek_ports(ip).await?; @@ -136,7 +141,7 @@ impl NativeRunningKernel { let mut cmd = kernel_specification.command(&connection_path)?; - let process = cmd + let mut process = cmd .current_dir(&working_directory) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) @@ -155,8 +160,6 @@ impl NativeRunningKernel { let mut control_socket = runtimelib::create_client_control_connection(&connection_info, &session_id).await?; - let (mut iopub, iosub) = futures::channel::mpsc::channel(100); - let (request_tx, mut request_rx) = futures::channel::mpsc::channel::(100); @@ -164,18 +167,41 @@ impl NativeRunningKernel { let (mut shell_reply_tx, shell_reply_rx) = futures::channel::mpsc::channel(100); let mut messages_rx = SelectAll::new(); - messages_rx.push(iosub); messages_rx.push(control_reply_rx); messages_rx.push(shell_reply_rx); - let iopub_task = cx.background_executor().spawn({ - async move { - while let Ok(message) = iopub_socket.read().await { - iopub.send(message).await?; + cx.spawn({ + let session = session.clone(); + + |mut cx| async move { + while let Some(message) = messages_rx.next().await { + session + .update(&mut cx, |session, cx| { + session.route(&message, cx); + }) + .ok(); } anyhow::Ok(()) } - }); + }) + .detach(); + + // iopub task + cx.spawn({ + let session = session.clone(); + + |mut cx| async move { + while let Ok(message) = iopub_socket.read().await { + session + .update(&mut cx, |session, cx| { + session.route(&message, cx); + }) + .ok(); + } + anyhow::Ok(()) + } + }) + .detach(); let (mut control_request_tx, mut control_request_rx) = futures::channel::mpsc::channel(100); @@ -221,21 +247,74 @@ impl NativeRunningKernel { } }); - anyhow::Ok(( - Self { - process, - request_tx, - working_directory, - _shell_task: shell_task, - _iopub_task: iopub_task, - _control_task: control_task, - _routing_task: routing_task, - connection_path, - execution_state: ExecutionState::Idle, - kernel_info: None, - }, - messages_rx, - )) + let stderr = process.stderr.take(); + + cx.spawn(|mut _cx| async move { + if stderr.is_none() { + return; + } + let reader = BufReader::new(stderr.unwrap()); + let mut lines = reader.lines(); + while let Some(Ok(line)) = lines.next().await { + log::error!("kernel: {}", line); + } + }) + .detach(); + + let stdout = process.stdout.take(); + + cx.spawn(|mut _cx| async move { + if stdout.is_none() { + return; + } + let reader = BufReader::new(stdout.unwrap()); + let mut lines = reader.lines(); + while let Some(Ok(line)) = lines.next().await { + log::info!("kernel: {}", line); + } + }) + .detach(); + + let status = process.status(); + + let process_status_task = cx.spawn(|mut cx| async move { + let error_message = match status.await { + Ok(status) => { + if status.success() { + log::info!("kernel process exited successfully"); + return; + } + + format!("kernel process exited with status: {:?}", status) + } + Err(err) => { + format!("kernel process exited with error: {:?}", err) + } + }; + + log::error!("{}", error_message); + + session + .update(&mut cx, |session, cx| { + session.kernel_errored(error_message, cx); + + cx.notify(); + }) + .ok(); + }); + + anyhow::Ok(Box::new(Self { + process, + request_tx, + working_directory, + _process_status_task: Some(process_status_task), + _shell_task: shell_task, + _control_task: control_task, + _routing_task: routing_task, + connection_path, + execution_state: ExecutionState::Idle, + kernel_info: None, + }) as Box) }) } } @@ -265,14 +344,17 @@ impl RunningKernel for NativeRunningKernel { self.kernel_info = Some(info); } - fn force_shutdown(&mut self) -> anyhow::Result<()> { - match self.process.kill() { + fn force_shutdown(&mut self, _cx: &mut WindowContext) -> Task> { + self._process_status_task.take(); + self.request_tx.close_channel(); + + Task::ready(match self.process.kill() { Ok(_) => Ok(()), Err(error) => Err(anyhow::anyhow!( "Failed to kill the kernel process: {}", error )), - } + }) } } diff --git a/crates/repl/src/kernels/remote_kernels.rs b/crates/repl/src/kernels/remote_kernels.rs index 9d2d5f2810..808a7dbf02 100644 --- a/crates/repl/src/kernels/remote_kernels.rs +++ b/crates/repl/src/kernels/remote_kernels.rs @@ -1,12 +1,21 @@ -use futures::{channel::mpsc, StreamExt as _}; -use gpui::AppContext; +use futures::{channel::mpsc, SinkExt as _}; +use gpui::{Task, View, WindowContext}; +use http_client::{AsyncBody, HttpClient, Request}; use jupyter_protocol::{ExecutionState, JupyterMessage, KernelInfoReply}; -// todo(kyle): figure out if this needs to be different use runtimelib::JupyterKernelspec; +use futures::StreamExt; +use smol::io::AsyncReadExt as _; + +use crate::Session; + use super::RunningKernel; -use jupyter_websocket_client::RemoteServer; -use std::fmt::Debug; +use anyhow::Result; +use jupyter_websocket_client::{ + JupyterWebSocketReader, JupyterWebSocketWriter, KernelLaunchRequest, KernelSpecsResponse, + RemoteServer, +}; +use std::{fmt::Debug, sync::Arc}; #[derive(Debug, Clone)] pub struct RemoteKernelSpecification { @@ -16,6 +25,101 @@ pub struct RemoteKernelSpecification { pub kernelspec: JupyterKernelspec, } +pub async fn launch_remote_kernel( + remote_server: &RemoteServer, + http_client: Arc, + kernel_name: &str, + _path: &str, +) -> Result { + // + let kernel_launch_request = KernelLaunchRequest { + name: kernel_name.to_string(), + // todo: add path to runtimelib + // path, + }; + + let kernel_launch_request = serde_json::to_string(&kernel_launch_request)?; + + let request = Request::builder() + .method("POST") + .uri(&remote_server.api_url("/kernels")) + .header("Authorization", format!("token {}", remote_server.token)) + .body(AsyncBody::from(kernel_launch_request))?; + + let response = http_client.send(request).await?; + + if !response.status().is_success() { + let mut body = String::new(); + response.into_body().read_to_string(&mut body).await?; + return Err(anyhow::anyhow!("Failed to launch kernel: {}", body)); + } + + let mut body = String::new(); + response.into_body().read_to_string(&mut body).await?; + + let response: jupyter_websocket_client::Kernel = serde_json::from_str(&body)?; + + Ok(response.id) +} + +pub async fn list_remote_kernelspecs( + remote_server: RemoteServer, + http_client: Arc, +) -> Result> { + let url = remote_server.api_url("/kernelspecs"); + + let request = Request::builder() + .method("GET") + .uri(&url) + .header("Authorization", format!("token {}", remote_server.token)) + .body(AsyncBody::default())?; + + let response = http_client.send(request).await?; + + if response.status().is_success() { + let mut body = response.into_body(); + + let mut body_bytes = Vec::new(); + body.read_to_end(&mut body_bytes).await?; + + let kernel_specs: KernelSpecsResponse = serde_json::from_slice(&body_bytes)?; + + let remote_kernelspecs = kernel_specs + .kernelspecs + .into_iter() + .map(|(name, spec)| RemoteKernelSpecification { + name: name.clone(), + url: remote_server.base_url.clone(), + token: remote_server.token.clone(), + // todo: line up the jupyter kernelspec from runtimelib with + // the kernelspec pulled from the API + // + // There are _small_ differences, so we may just want a impl `From` + kernelspec: JupyterKernelspec { + argv: spec.spec.argv, + display_name: spec.spec.display_name, + language: spec.spec.language, + // todo: fix up mismatch in types here + metadata: None, + interrupt_mode: None, + env: None, + }, + }) + .collect::>(); + + if remote_kernelspecs.is_empty() { + Err(anyhow::anyhow!("No kernel specs found")) + } else { + Ok(remote_kernelspecs.clone()) + } + } else { + Err(anyhow::anyhow!( + "Failed to fetch kernel specs: {}", + response.status() + )) + } +} + impl PartialEq for RemoteKernelSpecification { fn eq(&self, other: &Self) -> bool { self.name == other.name && self.url == other.url @@ -26,55 +130,91 @@ impl Eq for RemoteKernelSpecification {} pub struct RemoteRunningKernel { remote_server: RemoteServer, + _receiving_task: Task>, + _routing_task: Task>, + http_client: Arc, pub working_directory: std::path::PathBuf, pub request_tx: mpsc::Sender, pub execution_state: ExecutionState, pub kernel_info: Option, + pub kernel_id: String, } impl RemoteRunningKernel { - pub async fn new( + pub fn new( kernelspec: RemoteKernelSpecification, working_directory: std::path::PathBuf, - request_tx: mpsc::Sender, - _cx: &mut AppContext, - ) -> anyhow::Result<( - Self, - (), // Stream - )> { + session: View, + cx: &mut WindowContext, + ) -> Task>> { let remote_server = RemoteServer { base_url: kernelspec.url, token: kernelspec.token, }; - // todo: launch a kernel to get a kernel ID - let kernel_id = "not-implemented"; + let http_client = cx.http_client(); - let kernel_socket = remote_server.connect_to_kernel(kernel_id).await?; + cx.spawn(|cx| async move { + let kernel_id = launch_remote_kernel( + &remote_server, + http_client.clone(), + &kernelspec.name, + working_directory.to_str().unwrap_or_default(), + ) + .await?; - let (mut _w, mut _r) = kernel_socket.split(); + let kernel_socket = remote_server.connect_to_kernel(&kernel_id).await?; - let (_messages_tx, _messages_rx) = mpsc::channel::(100); + let (mut w, mut r): (JupyterWebSocketWriter, JupyterWebSocketReader) = + kernel_socket.split(); - // let routing_task = cx.background_executor().spawn({ - // async move { - // while let Some(message) = request_rx.next().await { - // w.send(message).await; - // } - // } - // }); - // let messages_rx = r.into(); + let (request_tx, mut request_rx) = + futures::channel::mpsc::channel::(100); - anyhow::Ok(( - Self { + let routing_task = cx.background_executor().spawn({ + async move { + while let Some(message) = request_rx.next().await { + w.send(message).await.ok(); + } + Ok(()) + } + }); + + let receiving_task = cx.spawn({ + let session = session.clone(); + + |mut cx| async move { + while let Some(message) = r.next().await { + match message { + Ok(message) => { + session + .update(&mut cx, |session, cx| { + session.route(&message, cx); + }) + .ok(); + } + Err(e) => { + log::error!("Error receiving message: {:?}", e); + } + } + } + Ok(()) + } + }); + + anyhow::Ok(Box::new(Self { + _routing_task: routing_task, + _receiving_task: receiving_task, remote_server, working_directory, request_tx, + // todo(kyle): pull this from the kernel API to start with execution_state: ExecutionState::Idle, kernel_info: None, - }, - (), - )) + kernel_id, + http_client: http_client.clone(), + }) as Box) + }) } } @@ -116,7 +256,30 @@ impl RunningKernel for RemoteRunningKernel { self.kernel_info = Some(info); } - fn force_shutdown(&mut self) -> anyhow::Result<()> { - unimplemented!("force_shutdown") + fn force_shutdown(&mut self, cx: &mut WindowContext) -> Task> { + let url = self + .remote_server + .api_url(&format!("/kernels/{}", self.kernel_id)); + let token = self.remote_server.token.clone(); + let http_client = self.http_client.clone(); + + cx.spawn(|_| async move { + let request = Request::builder() + .method("DELETE") + .uri(&url) + .header("Authorization", format!("token {}", token)) + .body(AsyncBody::default())?; + + let response = http_client.send(request).await?; + + if response.status().is_success() { + Ok(()) + } else { + Err(anyhow::anyhow!( + "Failed to shutdown kernel: {}", + response.status() + )) + } + }) } } diff --git a/crates/repl/src/repl_store.rs b/crates/repl/src/repl_store.rs index a4863b809b..27854c0eee 100644 --- a/crates/repl/src/repl_store.rs +++ b/crates/repl/src/repl_store.rs @@ -7,11 +7,14 @@ use command_palette_hooks::CommandPaletteFilter; use gpui::{ prelude::*, AppContext, EntityId, Global, Model, ModelContext, Subscription, Task, View, }; +use jupyter_websocket_client::RemoteServer; use language::Language; use project::{Fs, Project, WorktreeId}; use settings::{Settings, SettingsStore}; -use crate::kernels::{local_kernel_specifications, python_env_kernel_specifications}; +use crate::kernels::{ + list_remote_kernelspecs, local_kernel_specifications, python_env_kernel_specifications, +}; use crate::{JupyterSettings, KernelSpecification, Session}; struct GlobalReplStore(Model); @@ -141,19 +144,50 @@ impl ReplStore { }) } + fn get_remote_kernel_specifications( + &self, + cx: &mut ModelContext, + ) -> Option>>> { + match ( + std::env::var("JUPYTER_SERVER"), + std::env::var("JUPYTER_TOKEN"), + ) { + (Ok(server), Ok(token)) => { + let remote_server = RemoteServer { + base_url: server, + token, + }; + let http_client = cx.http_client(); + Some(cx.spawn(|_, _| async move { + list_remote_kernelspecs(remote_server, http_client) + .await + .map(|specs| specs.into_iter().map(KernelSpecification::Remote).collect()) + })) + } + _ => None, + } + } + pub fn refresh_kernelspecs(&mut self, cx: &mut ModelContext) -> Task> { let local_kernel_specifications = local_kernel_specifications(self.fs.clone()); - cx.spawn(|this, mut cx| async move { - let local_kernel_specifications = local_kernel_specifications.await?; + let remote_kernel_specifications = self.get_remote_kernel_specifications(cx); - let mut kernel_options = Vec::new(); - for kernel_specification in local_kernel_specifications { - kernel_options.push(KernelSpecification::Jupyter(kernel_specification)); + cx.spawn(|this, mut cx| async move { + let mut all_specs = local_kernel_specifications + .await? + .into_iter() + .map(KernelSpecification::Jupyter) + .collect::>(); + + if let Some(remote_task) = remote_kernel_specifications { + if let Ok(remote_specs) = remote_task.await { + all_specs.extend(remote_specs); + } } this.update(&mut cx, |this, cx| { - this.kernel_specifications = kernel_options; + this.kernel_specifications = all_specs; cx.notify(); }) }) diff --git a/crates/repl/src/session.rs b/crates/repl/src/session.rs index 513e85719d..0c1dc287ed 100644 --- a/crates/repl/src/session.rs +++ b/crates/repl/src/session.rs @@ -1,4 +1,5 @@ use crate::components::KernelListItem; +use crate::kernels::RemoteRunningKernel; use crate::setup_editor_session_actions; use crate::{ kernels::{Kernel, KernelSpecification, NativeRunningKernel}, @@ -15,8 +16,7 @@ use editor::{ scroll::Autoscroll, Anchor, AnchorRangeExt as _, Editor, MultiBuffer, ToPoint, }; -use futures::io::BufReader; -use futures::{AsyncBufReadExt as _, FutureExt as _, StreamExt as _}; +use futures::FutureExt as _; use gpui::{ div, prelude::*, EventEmitter, Model, Render, Subscription, Task, View, ViewContext, WeakView, }; @@ -29,14 +29,13 @@ use runtimelib::{ use std::{env::temp_dir, ops::Range, sync::Arc, time::Duration}; use theme::ActiveTheme; use ui::{prelude::*, IconButtonShape, Tooltip}; +use util::ResultExt as _; pub struct Session { fs: Arc, editor: WeakView, pub kernel: Kernel, blocks: HashMap, - messaging_task: Option>, - process_status_task: Option>, pub kernel_specification: KernelSpecification, telemetry: Arc, _buffer_subscription: Subscription, @@ -219,8 +218,6 @@ impl Session { fs, editor, kernel: Kernel::StartingKernel(Task::ready(()).shared()), - messaging_task: None, - process_status_task: None, blocks: HashMap::default(), kernel_specification, _buffer_subscription: subscription, @@ -246,6 +243,8 @@ impl Session { cx.entity_id().to_string(), ); + let session_view = cx.view().clone(); + let kernel = match self.kernel_specification.clone() { KernelSpecification::Jupyter(kernel_specification) | KernelSpecification::PythonEnv(kernel_specification) => NativeRunningKernel::new( @@ -253,11 +252,15 @@ impl Session { entity_id, working_directory, self.fs.clone(), + session_view, + cx, + ), + KernelSpecification::Remote(remote_kernel_specification) => RemoteRunningKernel::new( + remote_kernel_specification, + working_directory, + session_view, cx, ), - KernelSpecification::Remote(_remote_kernel_specification) => { - unimplemented!() - } }; let pending_kernel = cx @@ -265,119 +268,15 @@ impl Session { let kernel = kernel.await; match kernel { - Ok((mut kernel, mut messages_rx)) => { + Ok(kernel) => { this.update(&mut cx, |session, cx| { - let stderr = kernel.process.stderr.take(); - - cx.spawn(|_session, mut _cx| async move { - if stderr.is_none() { - return; - } - let reader = BufReader::new(stderr.unwrap()); - let mut lines = reader.lines(); - while let Some(Ok(line)) = lines.next().await { - // todo!(): Log stdout and stderr to something the session can show - log::error!("kernel: {}", line); - } - }) - .detach(); - - let stdout = kernel.process.stdout.take(); - - cx.spawn(|_session, mut _cx| async move { - if stdout.is_none() { - return; - } - let reader = BufReader::new(stdout.unwrap()); - let mut lines = reader.lines(); - while let Some(Ok(line)) = lines.next().await { - log::info!("kernel: {}", line); - } - }) - .detach(); - - let status = kernel.process.status(); - session.kernel(Kernel::RunningKernel(Box::new(kernel)), cx); - - let process_status_task = cx.spawn(|session, mut cx| async move { - let error_message = match status.await { - Ok(status) => { - if status.success() { - log::info!("kernel process exited successfully"); - return; - } - - format!("kernel process exited with status: {:?}", status) - } - Err(err) => { - format!("kernel process exited with error: {:?}", err) - } - }; - - log::error!("{}", error_message); - - session - .update(&mut cx, |session, cx| { - session.kernel( - Kernel::ErroredLaunch(error_message.clone()), - cx, - ); - - session.blocks.values().for_each(|block| { - block.execution_view.update( - cx, - |execution_view, cx| { - match execution_view.status { - ExecutionStatus::Finished => { - // Do nothing when the output was good - } - _ => { - // All other cases, set the status to errored - execution_view.status = - ExecutionStatus::KernelErrored( - error_message.clone(), - ) - } - } - cx.notify(); - }, - ); - }); - - cx.notify(); - }) - .ok(); - }); - - session.process_status_task = Some(process_status_task); - - session.messaging_task = Some(cx.spawn(|session, mut cx| async move { - while let Some(message) = messages_rx.next().await { - session - .update(&mut cx, |session, cx| { - session.route(&message, cx); - }) - .ok(); - } - })); - - // todo!(@rgbkrk): send KernelInfoRequest once our shell channel read/writes are split - // cx.spawn(|this, mut cx| async move { - // cx.background_executor() - // .timer(Duration::from_millis(120)) - // .await; - // this.update(&mut cx, |this, cx| { - // this.send(KernelInfoRequest {}.into(), cx).ok(); - // }) - // .ok(); - // }) - // .detach(); + session.kernel(Kernel::RunningKernel(kernel), cx); }) .ok(); } Err(err) => { this.update(&mut cx, |session, cx| { - session.kernel(Kernel::ErroredLaunch(err.to_string()), cx); + session.kernel_errored(err.to_string(), cx); }) .ok(); } @@ -389,6 +288,26 @@ impl Session { cx.notify(); } + pub fn kernel_errored(&mut self, error_message: String, cx: &mut ViewContext) { + self.kernel(Kernel::ErroredLaunch(error_message.clone()), cx); + + self.blocks.values().for_each(|block| { + block.execution_view.update(cx, |execution_view, cx| { + match execution_view.status { + ExecutionStatus::Finished => { + // Do nothing when the output was good + } + _ => { + // All other cases, set the status to errored + execution_view.status = + ExecutionStatus::KernelErrored(error_message.clone()) + } + } + cx.notify(); + }); + }); + } + fn on_buffer_event( &mut self, buffer: Model, @@ -559,7 +478,7 @@ impl Session { } } - fn route(&mut self, message: &JupyterMessage, cx: &mut ViewContext) { + pub fn route(&mut self, message: &JupyterMessage, cx: &mut ViewContext) { let parent_message_id = match message.parent_header.as_ref() { Some(header) => &header.msg_id, None => return, @@ -639,21 +558,17 @@ impl Session { Kernel::RunningKernel(mut kernel) => { let mut request_tx = kernel.request_tx().clone(); + let forced = kernel.force_shutdown(cx); + cx.spawn(|this, mut cx| async move { let message: JupyterMessage = ShutdownRequest { restart: false }.into(); request_tx.try_send(message).ok(); + forced.await.log_err(); + // Give the kernel a bit of time to clean up cx.background_executor().timer(Duration::from_secs(3)).await; - this.update(&mut cx, |session, _cx| { - session.messaging_task.take(); - session.process_status_task.take(); - }) - .ok(); - - kernel.force_shutdown().ok(); - this.update(&mut cx, |session, cx| { session.clear_outputs(cx); session.kernel(Kernel::Shutdown, cx); @@ -664,8 +579,6 @@ impl Session { .detach(); } _ => { - self.messaging_task.take(); - self.process_status_task.take(); self.kernel(Kernel::Shutdown, cx); } } @@ -682,23 +595,19 @@ impl Session { Kernel::RunningKernel(mut kernel) => { let mut request_tx = kernel.request_tx().clone(); + let forced = kernel.force_shutdown(cx); + cx.spawn(|this, mut cx| async move { // Send shutdown request with restart flag log::debug!("restarting kernel"); let message: JupyterMessage = ShutdownRequest { restart: true }.into(); request_tx.try_send(message).ok(); - this.update(&mut cx, |session, _cx| { - session.messaging_task.take(); - session.process_status_task.take(); - }) - .ok(); - // Wait for kernel to shutdown cx.background_executor().timer(Duration::from_secs(1)).await; // Force kill the kernel if it hasn't shut down - kernel.force_shutdown().ok(); + forced.await.log_err(); // Start a new kernel this.update(&mut cx, |session, cx| { @@ -711,9 +620,6 @@ impl Session { .detach(); } _ => { - // If it's not already running, we can just clean up and start a new kernel - self.messaging_task.take(); - self.process_status_task.take(); self.clear_outputs(cx); self.start_kernel(cx); } From 5ee5a1a51e37751e368737f98e7bc53acf67b92f Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 21 Nov 2024 23:16:49 +0100 Subject: [PATCH 55/68] chore: Do not produce universal binaries for our releases (#21014) Closes #ISSUE Release Notes: - We no longer provide universal binaries for our releases on macOS. --- .github/workflows/ci.yml | 9 +-------- script/bundle-mac | 18 ------------------ script/upload-nightly | 1 - 3 files changed, 1 insertion(+), 27 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8f2f08aa1a..49881e2e7c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -272,18 +272,12 @@ jobs: - name: Create macOS app bundle run: script/bundle-mac - - name: Rename single-architecture binaries + - name: Rename binaries if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }} run: | mv target/aarch64-apple-darwin/release/Zed.dmg target/aarch64-apple-darwin/release/Zed-aarch64.dmg mv target/x86_64-apple-darwin/release/Zed.dmg target/x86_64-apple-darwin/release/Zed-x86_64.dmg - - name: Upload app bundle (universal) to workflow run if main branch or specific label - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4 - if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }} - with: - name: Zed_${{ github.event.pull_request.head.sha || github.sha }}.dmg - path: target/release/Zed.dmg - name: Upload app bundle (aarch64) to workflow run if main branch or specific label uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4 if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }} @@ -309,7 +303,6 @@ jobs: target/zed-remote-server-macos-aarch64.gz target/aarch64-apple-darwin/release/Zed-aarch64.dmg target/x86_64-apple-darwin/release/Zed-x86_64.dmg - target/release/Zed.dmg env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/script/bundle-mac b/script/bundle-mac index 06231a22ab..54247645cc 100755 --- a/script/bundle-mac +++ b/script/bundle-mac @@ -172,11 +172,6 @@ function download_git() { x86_64-apple-darwin) download_and_unpack "https://github.com/desktop/dugite-native/releases/download/${GIT_VERSION}/dugite-native-${GIT_VERSION}-${GIT_VERSION_SHA}-macOS-x64.tar.gz" bin/git ./git ;; - universal) - download_and_unpack "https://github.com/desktop/dugite-native/releases/download/${GIT_VERSION}/dugite-native-${GIT_VERSION}-${GIT_VERSION_SHA}-macOS-arm64.tar.gz" bin/git ./git_arm64 - download_and_unpack "https://github.com/desktop/dugite-native/releases/download/${GIT_VERSION}/dugite-native-${GIT_VERSION}-${GIT_VERSION_SHA}-macOS-x64.tar.gz" bin/git ./git_x64 - lipo -create ./git_arm64 ./git_x64 -output ./git - ;; *) echo "Unsupported architecture: $architecture" exit 1 @@ -377,20 +372,7 @@ else prepare_binaries "aarch64-apple-darwin" "$app_path_aarch64" prepare_binaries "x86_64-apple-darwin" "$app_path_x64" - cp -R "$app_path_x64" target/release/ - app_path=target/release/$(basename "$app_path_x64") - lipo \ - -create \ - target/{x86_64-apple-darwin,aarch64-apple-darwin}/${target_dir}/zed \ - -output \ - "${app_path}/Contents/MacOS/zed" - lipo \ - -create \ - target/{x86_64-apple-darwin,aarch64-apple-darwin}/${target_dir}/cli \ - -output \ - "${app_path}/Contents/MacOS/cli" - sign_app_binaries "$app_path" "universal" "." sign_app_binaries "$app_path_x64" "x86_64-apple-darwin" "x86_64-apple-darwin" sign_app_binaries "$app_path_aarch64" "aarch64-apple-darwin" "aarch64-apple-darwin" diff --git a/script/upload-nightly b/script/upload-nightly index fd37941981..87ad712ae4 100755 --- a/script/upload-nightly +++ b/script/upload-nightly @@ -43,7 +43,6 @@ case "$target" in macos) upload_to_blob_store $bucket_name "target/aarch64-apple-darwin/release/Zed.dmg" "nightly/Zed-aarch64.dmg" upload_to_blob_store $bucket_name "target/x86_64-apple-darwin/release/Zed.dmg" "nightly/Zed-x86_64.dmg" - upload_to_blob_store $bucket_name "target/release/Zed.dmg" "nightly/Zed.dmg" upload_to_blob_store $bucket_name "target/latest-sha" "nightly/latest-sha" rm -f "target/aarch64-apple-darwin/release/Zed.dmg" "target/x86_64-apple-darwin/release/Zed.dmg" "target/release/Zed.dmg" rm -f "target/latest-sha" From 9d95da56c34606388e12e5e814a3f9f5ec392369 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 21 Nov 2024 17:50:22 -0500 Subject: [PATCH 56/68] welcome: Remove dependency on `theme_selector` (#21024) This PR removes the dependency on `theme_selector` from `welcome`, as we can just dispatch the action instead. Release Notes: - N/A --- Cargo.lock | 1 - crates/welcome/Cargo.toml | 1 - crates/welcome/src/welcome.rs | 10 +++------- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d950324d2f..89c5ae8180 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14422,7 +14422,6 @@ dependencies = [ "schemars", "serde", "settings", - "theme_selector", "ui", "util", "vim_mode_setting", diff --git a/crates/welcome/Cargo.toml b/crates/welcome/Cargo.toml index 26fe379ec6..473e5e853e 100644 --- a/crates/welcome/Cargo.toml +++ b/crates/welcome/Cargo.toml @@ -27,7 +27,6 @@ project.workspace = true schemars.workspace = true serde.workspace = true settings.workspace = true -theme_selector.workspace = true ui.workspace = true util.workspace = true vim_mode_setting.workspace = true diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index e66feec768..8dcb26bcc1 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -5,7 +5,7 @@ mod multibuffer_hint; use client::{telemetry::Telemetry, TelemetrySettings}; use db::kvp::KEY_VALUE_STORE; use gpui::{ - actions, svg, AppContext, EventEmitter, FocusHandle, FocusableView, InteractiveElement, + actions, svg, Action, AppContext, EventEmitter, FocusHandle, FocusableView, InteractiveElement, ParentElement, Render, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowContext, }; @@ -133,12 +133,8 @@ impl Render for WelcomePage { "welcome page: change theme".to_string(), ); this.workspace - .update(cx, |workspace, cx| { - theme_selector::toggle( - workspace, - &Default::default(), - cx, - ) + .update(cx, |_workspace, cx| { + cx.dispatch_action(zed_actions::theme_selector::Toggle::default().boxed_clone()); }) .ok(); })), From 0663bf2a5311a04e842ab95741e52fba8e417d17 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 22 Nov 2024 00:25:30 +0100 Subject: [PATCH 57/68] pylsp: Tweak default user settings (#21025) I've also looked into not creating temp dirs in project directories and succeeded at that for Mypy; no dice for rope though, I'll have to send a patch to pylsp to fix that. Closes #20646 Release Notes: - Python: tweaked default pylsp settings to be less noisy (mypy and pycodestyle are no longer enabled by default). --- crates/languages/src/python.rs | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index df158b9c7d..429da01c8f 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -917,13 +917,17 @@ impl LspAdapter for PyLspAdapter { .unwrap_or_else(|| { json!({ "plugins": { - "rope_autoimport": {"enabled": true}, - "mypy": {"enabled": true} - } + "pycodestyle": {"enabled": false}, + "rope_autoimport": {"enabled": true, "memory": true}, + "mypy": {"enabled": false} + }, + "rope": { + "ropeFolder": null + }, }) }); - // If python.pythonPath is not set in user config, do so using our toolchain picker. + // If user did not explicitly modify their python venv, use one from picker. if let Some(toolchain) = toolchain { if user_settings.is_null() { user_settings = Value::Object(serde_json::Map::default()); @@ -939,23 +943,22 @@ impl LspAdapter for PyLspAdapter { .or_insert(Value::Object(serde_json::Map::default())) .as_object_mut() { - jedi.insert( - "environment".to_string(), - Value::String(toolchain.path.clone().into()), - ); + jedi.entry("environment".to_string()) + .or_insert_with(|| Value::String(toolchain.path.clone().into())); } if let Some(pylint) = python .entry("mypy") .or_insert(Value::Object(serde_json::Map::default())) .as_object_mut() { - pylint.insert( - "overrides".to_string(), + pylint.entry("overrides".to_string()).or_insert_with(|| { Value::Array(vec![ Value::String("--python-executable".into()), Value::String(toolchain.path.into()), - ]), - ); + Value::String("--cache-dir=/dev/null".into()), + Value::Bool(true), + ]) + }); } } } From 9211e699eef96fc4d67346ec0efac840f6b3aefb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=B0=8F=E7=99=BD?= <364772080@qq.com> Date: Fri, 22 Nov 2024 07:32:49 +0800 Subject: [PATCH 58/68] Follow-up on #18447: Unintentional deletion during merge-conflicts resolution (#20991) After #18447 was merged, I reviewed the PR code as usual. During this review, I realized that some code was unintentionally removed when I was resolving merge conflicts in #18447. Sorry! Release Notes: - N/A --- crates/languages/src/go.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/languages/src/go.rs b/crates/languages/src/go.rs index 64583ad61f..b3073d7eaa 100644 --- a/crates/languages/src/go.rs +++ b/crates/languages/src/go.rs @@ -139,7 +139,8 @@ impl super::LspAdapter for GoLspAdapter { let gobin_dir = container_dir.join("gobin"); fs::create_dir_all(&gobin_dir).await?; - let install_output = util::command::new_smol_command("go") + let go = delegate.which("go".as_ref()).await.unwrap_or("go".into()); + let install_output = util::command::new_smol_command(go) .env("GO111MODULE", "on") .env("GOBIN", &gobin_dir) .args(["install", "golang.org/x/tools/gopls@latest"]) From e0245b3f3042610692e7223d09f6e6fa6d10098f Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 21 Nov 2024 18:33:11 -0500 Subject: [PATCH 59/68] Merge `quick_action_bar` into `zed` (#21026) This PR merges the `quick_action_bar` crate into the `zed` crate. We weren't really gaining anything by having it be a separate crate, and it was introducing an additional step in the dependency graph that was getting in the way. It's only ~850 LOC, so the impact on the compilation speed of the `zed` crate itself is negligible. Release Notes: - N/A --- Cargo.lock | 20 +----------- Cargo.toml | 2 -- crates/quick_action_bar/Cargo.toml | 32 ------------------- crates/quick_action_bar/LICENSE-GPL | 1 - crates/zed/Cargo.toml | 2 +- crates/zed/src/zed.rs | 1 + .../src => zed/src/zed}/quick_action_bar.rs | 7 ++-- .../zed/quick_action_bar/markdown_preview.rs} | 2 +- .../src/zed/quick_action_bar}/repl_menu.rs | 5 ++- 9 files changed, 9 insertions(+), 63 deletions(-) delete mode 100644 crates/quick_action_bar/Cargo.toml delete mode 120000 crates/quick_action_bar/LICENSE-GPL rename crates/{quick_action_bar/src => zed/src/zed}/quick_action_bar.rs (99%) rename crates/{quick_action_bar/src/toggle_markdown_preview.rs => zed/src/zed/quick_action_bar/markdown_preview.rs} (98%) rename crates/{quick_action_bar/src => zed/src/zed/quick_action_bar}/repl_menu.rs (99%) diff --git a/Cargo.lock b/Cargo.lock index 89c5ae8180..ddd2e400e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9426,24 +9426,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "quick_action_bar" -version = "0.1.0" -dependencies = [ - "assistant", - "editor", - "gpui", - "markdown_preview", - "picker", - "repl", - "search", - "settings", - "ui", - "util", - "workspace", - "zed_actions", -] - [[package]] name = "quinn" version = "0.11.6" @@ -15541,12 +15523,12 @@ dependencies = [ "outline_panel", "parking_lot", "paths", + "picker", "profiling", "project", "project_panel", "project_symbols", "proto", - "quick_action_bar", "recent_projects", "release_channel", "remote", diff --git a/Cargo.toml b/Cargo.toml index 58239800da..c12079a26a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -81,7 +81,6 @@ members = [ "crates/project_panel", "crates/project_symbols", "crates/proto", - "crates/quick_action_bar", "crates/recent_projects", "crates/refineable", "crates/refineable/derive_refineable", @@ -259,7 +258,6 @@ project = { path = "crates/project" } project_panel = { path = "crates/project_panel" } project_symbols = { path = "crates/project_symbols" } proto = { path = "crates/proto" } -quick_action_bar = { path = "crates/quick_action_bar" } recent_projects = { path = "crates/recent_projects" } refineable = { path = "crates/refineable" } release_channel = { path = "crates/release_channel" } diff --git a/crates/quick_action_bar/Cargo.toml b/crates/quick_action_bar/Cargo.toml deleted file mode 100644 index b3228820f6..0000000000 --- a/crates/quick_action_bar/Cargo.toml +++ /dev/null @@ -1,32 +0,0 @@ -[package] -name = "quick_action_bar" -version = "0.1.0" -edition = "2021" -publish = false -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/quick_action_bar.rs" -doctest = false - -[dependencies] -assistant.workspace = true -editor.workspace = true -gpui.workspace = true -markdown_preview.workspace = true -repl.workspace = true -search.workspace = true -settings.workspace = true -ui.workspace = true -util.workspace = true -workspace.workspace = true -zed_actions.workspace = true -picker.workspace = true - -[dev-dependencies] -editor = { workspace = true, features = ["test-support"] } -gpui = { workspace = true, features = ["test-support"] } -workspace = { workspace = true, features = ["test-support"] } diff --git a/crates/quick_action_bar/LICENSE-GPL b/crates/quick_action_bar/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/quick_action_bar/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 6c447bcabe..52ec265480 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -78,12 +78,12 @@ outline.workspace = true outline_panel.workspace = true parking_lot.workspace = true paths.workspace = true +picker.workspace = true profiling.workspace = true project.workspace = true project_panel.workspace = true project_symbols.workspace = true proto.workspace = true -quick_action_bar.workspace = true recent_projects.workspace = true release_channel.workspace = true remote.workspace = true diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index b2dbc087b0..322ea3610b 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -6,6 +6,7 @@ pub(crate) mod linux_prompts; #[cfg(target_os = "macos")] pub(crate) mod mac_only_instance; mod open_listener; +mod quick_action_bar; #[cfg(target_os = "windows")] pub(crate) mod windows_only_instance; diff --git a/crates/quick_action_bar/src/quick_action_bar.rs b/crates/zed/src/zed/quick_action_bar.rs similarity index 99% rename from crates/quick_action_bar/src/quick_action_bar.rs rename to crates/zed/src/zed/quick_action_bar.rs index 7849620093..85090a1b97 100644 --- a/crates/quick_action_bar/src/quick_action_bar.rs +++ b/crates/zed/src/zed/quick_action_bar.rs @@ -1,3 +1,6 @@ +mod markdown_preview; +mod repl_menu; + use assistant::assistant_settings::AssistantSettings; use assistant::AssistantPanel; use editor::actions::{ @@ -6,7 +9,6 @@ use editor::actions::{ SelectNext, SelectSmallerSyntaxNode, ToggleGoToLine, ToggleOutline, }; use editor::{Editor, EditorSettings}; - use gpui::{ Action, AnchorCorner, ClickEvent, ElementId, EventEmitter, FocusHandle, FocusableView, InteractiveElement, ParentElement, Render, Styled, Subscription, View, ViewContext, WeakView, @@ -22,9 +24,6 @@ use workspace::{ }; use zed_actions::InlineAssist; -mod repl_menu; -mod toggle_markdown_preview; - pub struct QuickActionBar { _inlay_hints_enabled_subscription: Option, active_item: Option>, diff --git a/crates/quick_action_bar/src/toggle_markdown_preview.rs b/crates/zed/src/zed/quick_action_bar/markdown_preview.rs similarity index 98% rename from crates/quick_action_bar/src/toggle_markdown_preview.rs rename to crates/zed/src/zed/quick_action_bar/markdown_preview.rs index 527da3a568..5162cb0644 100644 --- a/crates/quick_action_bar/src/toggle_markdown_preview.rs +++ b/crates/zed/src/zed/quick_action_bar/markdown_preview.rs @@ -5,7 +5,7 @@ use markdown_preview::{ use ui::{prelude::*, text_for_keystroke, IconButtonShape, Tooltip}; use workspace::Workspace; -use crate::QuickActionBar; +use super::QuickActionBar; impl QuickActionBar { pub fn render_toggle_markdown_preview( diff --git a/crates/quick_action_bar/src/repl_menu.rs b/crates/zed/src/zed/quick_action_bar/repl_menu.rs similarity index 99% rename from crates/quick_action_bar/src/repl_menu.rs rename to crates/zed/src/zed/quick_action_bar/repl_menu.rs index b9ae940579..5f616da9d3 100644 --- a/crates/quick_action_bar/src/repl_menu.rs +++ b/crates/zed/src/zed/quick_action_bar/repl_menu.rs @@ -1,5 +1,6 @@ use std::time::Duration; +use gpui::ElementId; use gpui::{percentage, Animation, AnimationExt, AnyElement, Transformation, View}; use picker::Picker; use repl::{ @@ -11,11 +12,9 @@ use ui::{ prelude::*, ButtonLike, ContextMenu, IconWithIndicator, Indicator, IntoElement, PopoverMenu, PopoverMenuHandle, Tooltip, }; - -use gpui::ElementId; use util::ResultExt; -use crate::QuickActionBar; +use super::QuickActionBar; const ZED_REPL_DOCUMENTATION: &str = "https://zed.dev/docs/repl"; From 6c470748ac481db9e31503d7349ead286f9886b3 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 21 Nov 2024 18:47:44 -0500 Subject: [PATCH 60/68] zed: Remove unnecessary `#[allow(non_snake_case)]` attribute (#21030) This PR removes the `#[allow(non_snake_case)]` attribute from the `zed` crate, as it wasn't actually doing anything. Release Notes: - N/A --- crates/zed/src/main.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index b1b721c7c6..e96a70f91d 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -1,5 +1,3 @@ -// Allow binary to be called Zed for a nice application menu when running executable directly -#![allow(non_snake_case)] // Disable command line from opening on release mode #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] From 477c6e6833b40a6789a819658ce55b4c02e96235 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 22 Nov 2024 01:13:48 +0100 Subject: [PATCH 61/68] pylsp: Update mypy plugin name (#21031) Follow-up to #21025 Release Notes: - N/A --- crates/languages/src/python.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 429da01c8f..2cedd704cf 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -919,7 +919,7 @@ impl LspAdapter for PyLspAdapter { "plugins": { "pycodestyle": {"enabled": false}, "rope_autoimport": {"enabled": true, "memory": true}, - "mypy": {"enabled": false} + "pylsp_mypy": {"enabled": false} }, "rope": { "ropeFolder": null @@ -947,7 +947,7 @@ impl LspAdapter for PyLspAdapter { .or_insert_with(|| Value::String(toolchain.path.clone().into())); } if let Some(pylint) = python - .entry("mypy") + .entry("pylsp_mypy") .or_insert(Value::Object(serde_json::Map::default())) .as_object_mut() { From 14ea4621ab98a315c4742c379723b8dbfd2087b9 Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Thu, 21 Nov 2024 19:21:18 -0700 Subject: [PATCH 62/68] Add `fs::MTime` newtype to encourage `!=` instead of `>` (#20830) See ["mtime comparison considered harmful"](https://apenwarr.ca/log/20181113) for details of why comparators other than equality/inequality should not be used with mtime. Release Notes: - N/A --- Cargo.lock | 3 + crates/assistant/src/context_store.rs | 2 +- crates/copilot/src/copilot.rs | 2 +- crates/editor/Cargo.toml | 1 + crates/editor/src/items.rs | 21 +-- crates/editor/src/persistence.rs | 24 ++-- crates/extension_host/src/extension_host.rs | 5 +- crates/fs/Cargo.toml | 1 + crates/fs/src/fs.rs | 136 ++++++++++++------- crates/language/Cargo.toml | 1 + crates/language/src/buffer.rs | 24 ++-- crates/semantic_index/src/embedding_index.rs | 14 +- crates/semantic_index/src/summary_backlog.rs | 9 +- crates/semantic_index/src/summary_index.rs | 18 +-- crates/worktree/src/worktree.rs | 6 +- 15 files changed, 155 insertions(+), 112 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ddd2e400e7..75d69fdcf9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3731,6 +3731,7 @@ dependencies = [ "emojis", "env_logger 0.11.5", "file_icons", + "fs", "futures 0.3.31", "fuzzy", "git", @@ -4621,6 +4622,7 @@ dependencies = [ "objc", "parking_lot", "paths", + "proto", "rope", "serde", "serde_json", @@ -6487,6 +6489,7 @@ dependencies = [ "ctor", "ec4rs", "env_logger 0.11.5", + "fs", "futures 0.3.31", "fuzzy", "git", diff --git a/crates/assistant/src/context_store.rs b/crates/assistant/src/context_store.rs index 568b04e492..217d59faa4 100644 --- a/crates/assistant/src/context_store.rs +++ b/crates/assistant/src/context_store.rs @@ -770,7 +770,7 @@ impl ContextStore { contexts.push(SavedContextMetadata { title: title.to_string(), path, - mtime: metadata.mtime.into(), + mtime: metadata.mtime.timestamp_for_user().into(), }); } } diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 7ea289706c..bc424d2d5a 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -1231,7 +1231,7 @@ mod tests { fn disk_state(&self) -> language::DiskState { language::DiskState::Present { - mtime: std::time::UNIX_EPOCH, + mtime: ::fs::MTime::from_seconds_and_nanos(100, 42), } } diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index 8d03fa79f0..f1f1b34981 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -42,6 +42,7 @@ emojis.workspace = true file_icons.workspace = true futures.workspace = true fuzzy.workspace = true +fs.workspace = true git.workspace = true gpui.workspace = true http_client.workspace = true diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index bd54d2c376..51ad9b9dec 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1618,15 +1618,14 @@ fn path_for_file<'a>( #[cfg(test)] mod tests { use crate::editor_tests::init_test; + use fs::Fs; use super::*; + use fs::MTime; use gpui::{AppContext, VisualTestContext}; use language::{LanguageMatcher, TestFile}; use project::FakeFs; - use std::{ - path::{Path, PathBuf}, - time::SystemTime, - }; + use std::path::{Path, PathBuf}; #[gpui::test] fn test_path_for_file(cx: &mut AppContext) { @@ -1679,9 +1678,7 @@ mod tests { async fn test_deserialize(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); - let now = SystemTime::now(); let fs = FakeFs::new(cx.executor()); - fs.set_next_mtime(now); fs.insert_file("/file.rs", Default::default()).await; // Test case 1: Deserialize with path and contents @@ -1690,12 +1687,18 @@ mod tests { let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx)); let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap(); let item_id = 1234 as ItemId; + let mtime = fs + .metadata(Path::new("/file.rs")) + .await + .unwrap() + .unwrap() + .mtime; let serialized_editor = SerializedEditor { abs_path: Some(PathBuf::from("/file.rs")), contents: Some("fn main() {}".to_string()), language: Some("Rust".to_string()), - mtime: Some(now), + mtime: Some(mtime), }; DB.save_serialized_editor(item_id, workspace_id, serialized_editor.clone()) @@ -1792,9 +1795,7 @@ mod tests { let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap(); let item_id = 9345 as ItemId; - let old_mtime = now - .checked_sub(std::time::Duration::from_secs(60 * 60 * 24)) - .unwrap(); + let old_mtime = MTime::from_seconds_and_nanos(0, 50); let serialized_editor = SerializedEditor { abs_path: Some(PathBuf::from("/file.rs")), contents: Some("fn main() {}".to_string()), diff --git a/crates/editor/src/persistence.rs b/crates/editor/src/persistence.rs index a52fb60543..06e2ea1f9b 100644 --- a/crates/editor/src/persistence.rs +++ b/crates/editor/src/persistence.rs @@ -1,8 +1,8 @@ use anyhow::Result; use db::sqlez::bindable::{Bind, Column, StaticColumnCount}; use db::sqlez::statement::Statement; +use fs::MTime; use std::path::PathBuf; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; use db::sqlez_macros::sql; use db::{define_connection, query}; @@ -14,7 +14,7 @@ pub(crate) struct SerializedEditor { pub(crate) abs_path: Option, pub(crate) contents: Option, pub(crate) language: Option, - pub(crate) mtime: Option, + pub(crate) mtime: Option, } impl StaticColumnCount for SerializedEditor { @@ -29,16 +29,13 @@ impl Bind for SerializedEditor { let start_index = statement.bind(&self.contents, start_index)?; let start_index = statement.bind(&self.language, start_index)?; - let mtime = self.mtime.and_then(|mtime| { - mtime - .duration_since(UNIX_EPOCH) - .ok() - .map(|duration| (duration.as_secs() as i64, duration.subsec_nanos() as i32)) - }); - let start_index = match mtime { + let start_index = match self + .mtime + .and_then(|mtime| mtime.to_seconds_and_nanos_for_persistence()) + { Some((seconds, nanos)) => { - let start_index = statement.bind(&seconds, start_index)?; - statement.bind(&nanos, start_index)? + let start_index = statement.bind(&(seconds as i64), start_index)?; + statement.bind(&(nanos as i32), start_index)? } None => { let start_index = statement.bind::>(&None, start_index)?; @@ -64,7 +61,7 @@ impl Column for SerializedEditor { let mtime = mtime_seconds .zip(mtime_nanos) - .map(|(seconds, nanos)| UNIX_EPOCH + Duration::new(seconds as u64, nanos as u32)); + .map(|(seconds, nanos)| MTime::from_seconds_and_nanos(seconds as u64, nanos as u32)); let editor = Self { abs_path, @@ -280,12 +277,11 @@ mod tests { assert_eq!(have, serialized_editor); // Storing and retrieving mtime - let now = SystemTime::now(); let serialized_editor = SerializedEditor { abs_path: None, contents: None, language: None, - mtime: Some(now), + mtime: Some(MTime::from_seconds_and_nanos(100, 42)), }; DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone()) diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index a858123fd9..4a832faeff 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -345,7 +345,10 @@ impl ExtensionStore { if let (Ok(Some(index_metadata)), Ok(Some(extensions_metadata))) = (index_metadata, extensions_metadata) { - if index_metadata.mtime > extensions_metadata.mtime { + if index_metadata + .mtime + .bad_is_greater_than(extensions_metadata.mtime) + { extension_index_needs_rebuild = false; } } diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml index a9dbb751b6..7a1cfaeaa5 100644 --- a/crates/fs/Cargo.toml +++ b/crates/fs/Cargo.toml @@ -24,6 +24,7 @@ libc.workspace = true parking_lot.workspace = true paths.workspace = true rope.workspace = true +proto.workspace = true serde.workspace = true serde_json.workspace = true smol.workspace = true diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 268a9d3f32..fc0fae3fe8 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -27,13 +27,14 @@ use futures::{future::BoxFuture, AsyncRead, Stream, StreamExt}; use git::repository::{GitRepository, RealGitRepository}; use gpui::{AppContext, Global, ReadGlobal}; use rope::Rope; +use serde::{Deserialize, Serialize}; use smol::io::AsyncWriteExt; use std::{ io::{self, Write}, path::{Component, Path, PathBuf}, pin::Pin, sync::Arc, - time::{Duration, SystemTime}, + time::{Duration, SystemTime, UNIX_EPOCH}, }; use tempfile::{NamedTempFile, TempDir}; use text::LineEnding; @@ -179,13 +180,62 @@ pub struct RemoveOptions { #[derive(Copy, Clone, Debug)] pub struct Metadata { pub inode: u64, - pub mtime: SystemTime, + pub mtime: MTime, pub is_symlink: bool, pub is_dir: bool, pub len: u64, pub is_fifo: bool, } +/// Filesystem modification time. The purpose of this newtype is to discourage use of operations +/// that do not make sense for mtimes. In particular, it is not always valid to compare mtimes using +/// `<` or `>`, as there are many things that can cause the mtime of a file to be earlier than it +/// was. See ["mtime comparison considered harmful" - apenwarr](https://apenwarr.ca/log/20181113). +/// +/// Do not derive Ord, PartialOrd, or arithmetic operation traits. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)] +#[serde(transparent)] +pub struct MTime(SystemTime); + +impl MTime { + /// Conversion intended for persistence and testing. + pub fn from_seconds_and_nanos(secs: u64, nanos: u32) -> Self { + MTime(UNIX_EPOCH + Duration::new(secs, nanos)) + } + + /// Conversion intended for persistence. + pub fn to_seconds_and_nanos_for_persistence(self) -> Option<(u64, u32)> { + self.0 + .duration_since(UNIX_EPOCH) + .ok() + .map(|duration| (duration.as_secs(), duration.subsec_nanos())) + } + + /// Returns the value wrapped by this `MTime`, for presentation to the user. The name including + /// "_for_user" is to discourage misuse - this method should not be used when making decisions + /// about file dirtiness. + pub fn timestamp_for_user(self) -> SystemTime { + self.0 + } + + /// Temporary method to split out the behavior changes from introduction of this newtype. + pub fn bad_is_greater_than(self, other: MTime) -> bool { + self.0 > other.0 + } +} + +impl From for MTime { + fn from(timestamp: proto::Timestamp) -> Self { + MTime(timestamp.into()) + } +} + +impl From for proto::Timestamp { + fn from(mtime: MTime) -> Self { + mtime.0.into() + } +} + #[derive(Default)] pub struct RealFs { git_hosting_provider_registry: Arc, @@ -558,7 +608,7 @@ impl Fs for RealFs { Ok(Some(Metadata { inode, - mtime: metadata.modified().unwrap(), + mtime: MTime(metadata.modified().unwrap()), len: metadata.len(), is_symlink, is_dir: metadata.file_type().is_dir(), @@ -818,13 +868,13 @@ struct FakeFsState { enum FakeFsEntry { File { inode: u64, - mtime: SystemTime, + mtime: MTime, len: u64, content: Vec, }, Dir { inode: u64, - mtime: SystemTime, + mtime: MTime, len: u64, entries: BTreeMap>>, git_repo_state: Option>>, @@ -836,6 +886,18 @@ enum FakeFsEntry { #[cfg(any(test, feature = "test-support"))] impl FakeFsState { + fn get_and_increment_mtime(&mut self) -> MTime { + let mtime = self.next_mtime; + self.next_mtime += FakeFs::SYSTEMTIME_INTERVAL; + MTime(mtime) + } + + fn get_and_increment_inode(&mut self) -> u64 { + let inode = self.next_inode; + self.next_inode += 1; + inode + } + fn read_path(&self, target: &Path) -> Result>> { Ok(self .try_read_path(target, true) @@ -959,7 +1021,7 @@ pub static FS_DOT_GIT: std::sync::LazyLock<&'static OsStr> = impl FakeFs { /// We need to use something large enough for Windows and Unix to consider this a new file. /// https://doc.rust-lang.org/nightly/std/time/struct.SystemTime.html#platform-specific-behavior - const SYSTEMTIME_INTERVAL: u64 = 100; + const SYSTEMTIME_INTERVAL: Duration = Duration::from_nanos(100); pub fn new(executor: gpui::BackgroundExecutor) -> Arc { let (tx, mut rx) = smol::channel::bounded::(10); @@ -969,13 +1031,13 @@ impl FakeFs { state: Mutex::new(FakeFsState { root: Arc::new(Mutex::new(FakeFsEntry::Dir { inode: 0, - mtime: SystemTime::UNIX_EPOCH, + mtime: MTime(UNIX_EPOCH), len: 0, entries: Default::default(), git_repo_state: None, })), git_event_tx: tx, - next_mtime: SystemTime::UNIX_EPOCH, + next_mtime: UNIX_EPOCH + Self::SYSTEMTIME_INTERVAL, next_inode: 1, event_txs: Default::default(), buffered_events: Vec::new(), @@ -1007,13 +1069,16 @@ impl FakeFs { state.next_mtime = next_mtime; } + pub fn get_and_increment_mtime(&self) -> MTime { + let mut state = self.state.lock(); + state.get_and_increment_mtime() + } + pub async fn touch_path(&self, path: impl AsRef) { let mut state = self.state.lock(); let path = path.as_ref(); - let new_mtime = state.next_mtime; - let new_inode = state.next_inode; - state.next_inode += 1; - state.next_mtime += Duration::from_nanos(Self::SYSTEMTIME_INTERVAL); + let new_mtime = state.get_and_increment_mtime(); + let new_inode = state.get_and_increment_inode(); state .write_path(path, move |entry| { match entry { @@ -1062,19 +1127,14 @@ impl FakeFs { fn write_file_internal(&self, path: impl AsRef, content: Vec) -> Result<()> { let mut state = self.state.lock(); - let path = path.as_ref(); - let inode = state.next_inode; - let mtime = state.next_mtime; - state.next_inode += 1; - state.next_mtime += Duration::from_nanos(Self::SYSTEMTIME_INTERVAL); let file = Arc::new(Mutex::new(FakeFsEntry::File { - inode, - mtime, + inode: state.get_and_increment_inode(), + mtime: state.get_and_increment_mtime(), len: content.len() as u64, content, })); let mut kind = None; - state.write_path(path, { + state.write_path(path.as_ref(), { let kind = &mut kind; move |entry| { match entry { @@ -1090,7 +1150,7 @@ impl FakeFs { Ok(()) } })?; - state.emit_event([(path, kind)]); + state.emit_event([(path.as_ref(), kind)]); Ok(()) } @@ -1383,16 +1443,6 @@ impl FakeFsEntry { } } - fn set_file_content(&mut self, path: &Path, new_content: Vec) -> Result<()> { - if let Self::File { content, mtime, .. } = self { - *mtime = SystemTime::now(); - *content = new_content; - Ok(()) - } else { - Err(anyhow!("not a file: {}", path.display())) - } - } - fn dir_entries( &mut self, path: &Path, @@ -1456,10 +1506,8 @@ impl Fs for FakeFs { } let mut state = self.state.lock(); - let inode = state.next_inode; - let mtime = state.next_mtime; - state.next_mtime += Duration::from_nanos(Self::SYSTEMTIME_INTERVAL); - state.next_inode += 1; + let inode = state.get_and_increment_inode(); + let mtime = state.get_and_increment_mtime(); state.write_path(&cur_path, |entry| { entry.or_insert_with(|| { created_dirs.push((cur_path.clone(), Some(PathEventKind::Created))); @@ -1482,10 +1530,8 @@ impl Fs for FakeFs { async fn create_file(&self, path: &Path, options: CreateOptions) -> Result<()> { self.simulate_random_delay().await; let mut state = self.state.lock(); - let inode = state.next_inode; - let mtime = state.next_mtime; - state.next_mtime += Duration::from_nanos(Self::SYSTEMTIME_INTERVAL); - state.next_inode += 1; + let inode = state.get_and_increment_inode(); + let mtime = state.get_and_increment_mtime(); let file = Arc::new(Mutex::new(FakeFsEntry::File { inode, mtime, @@ -1625,13 +1671,12 @@ impl Fs for FakeFs { let source = normalize_path(source); let target = normalize_path(target); let mut state = self.state.lock(); - let mtime = state.next_mtime; - let inode = util::post_inc(&mut state.next_inode); - state.next_mtime += Duration::from_nanos(Self::SYSTEMTIME_INTERVAL); + let mtime = state.get_and_increment_mtime(); + let inode = state.get_and_increment_inode(); let source_entry = state.read_path(&source)?; let content = source_entry.lock().file_content(&source)?.clone(); let mut kind = Some(PathEventKind::Created); - let entry = state.write_path(&target, |e| match e { + state.write_path(&target, |e| match e { btree_map::Entry::Occupied(e) => { if options.overwrite { kind = Some(PathEventKind::Changed); @@ -1647,14 +1692,11 @@ impl Fs for FakeFs { inode, mtime, len: content.len() as u64, - content: Vec::new(), + content, }))) .clone(), )), })?; - if let Some(entry) = entry { - entry.lock().set_file_content(&target, content)?; - } state.emit_event([(target, kind)]); Ok(()) } diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index 41285d8222..8b97d4a95f 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -31,6 +31,7 @@ async-watch.workspace = true clock.workspace = true collections.workspace = true ec4rs.workspace = true +fs.workspace = true futures.workspace = true fuzzy.workspace = true git.workspace = true diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index d1a01c26e6..2479eafd7a 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -21,6 +21,7 @@ use async_watch as watch; use clock::Lamport; pub use clock::ReplicaId; use collections::HashMap; +use fs::MTime; use futures::channel::oneshot; use gpui::{ AnyElement, AppContext, Context as _, EventEmitter, HighlightStyle, Model, ModelContext, @@ -51,7 +52,7 @@ use std::{ path::{Path, PathBuf}, str, sync::{Arc, LazyLock}, - time::{Duration, Instant, SystemTime}, + time::{Duration, Instant}, vec, }; use sum_tree::TreeMap; @@ -108,7 +109,7 @@ pub struct Buffer { file: Option>, /// The mtime of the file when this buffer was last loaded from /// or saved to disk. - saved_mtime: Option, + saved_mtime: Option, /// The version vector when this buffer was last loaded from /// or saved to disk. saved_version: clock::Global, @@ -406,22 +407,19 @@ pub trait File: Send + Sync { /// modified. In the case where the file is not stored, it can be either `New` or `Deleted`. In the /// UI these two states are distinguished. For example, the buffer tab does not display a deletion /// indicator for new files. -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Copy, Clone, Debug, PartialEq)] pub enum DiskState { /// File created in Zed that has not been saved. New, /// File present on the filesystem. - Present { - /// Last known mtime (modification time). - mtime: SystemTime, - }, + Present { mtime: MTime }, /// Deleted file that was previously present. Deleted, } impl DiskState { /// Returns the file's last known modification time on disk. - pub fn mtime(self) -> Option { + pub fn mtime(self) -> Option { match self { DiskState::New => None, DiskState::Present { mtime } => Some(mtime), @@ -976,7 +974,7 @@ impl Buffer { } /// The mtime of the buffer's file when the buffer was last saved or reloaded from disk. - pub fn saved_mtime(&self) -> Option { + pub fn saved_mtime(&self) -> Option { self.saved_mtime } @@ -1011,7 +1009,7 @@ impl Buffer { pub fn did_save( &mut self, version: clock::Global, - mtime: Option, + mtime: Option, cx: &mut ModelContext, ) { self.saved_version = version; @@ -1077,7 +1075,7 @@ impl Buffer { &mut self, version: clock::Global, line_ending: LineEnding, - mtime: Option, + mtime: Option, cx: &mut ModelContext, ) { self.saved_version = version; @@ -1777,7 +1775,9 @@ impl Buffer { match file.disk_state() { DiskState::New => false, DiskState::Present { mtime } => match self.saved_mtime { - Some(saved_mtime) => mtime > saved_mtime && self.has_unsaved_edits(), + Some(saved_mtime) => { + mtime.bad_is_greater_than(saved_mtime) && self.has_unsaved_edits() + } None => true, }, DiskState::Deleted => true, diff --git a/crates/semantic_index/src/embedding_index.rs b/crates/semantic_index/src/embedding_index.rs index 0913124341..4e3d74a2ea 100644 --- a/crates/semantic_index/src/embedding_index.rs +++ b/crates/semantic_index/src/embedding_index.rs @@ -7,6 +7,7 @@ use anyhow::{anyhow, Context as _, Result}; use collections::Bound; use feature_flags::FeatureFlagAppExt; use fs::Fs; +use fs::MTime; use futures::stream::StreamExt; use futures_batch::ChunksTimeoutStreamExt; use gpui::{AppContext, Model, Task}; @@ -17,14 +18,7 @@ use project::{Entry, UpdatedEntriesSet, Worktree}; use serde::{Deserialize, Serialize}; use smol::channel; use smol::future::FutureExt; -use std::{ - cmp::Ordering, - future::Future, - iter, - path::Path, - sync::Arc, - time::{Duration, SystemTime}, -}; +use std::{cmp::Ordering, future::Future, iter, path::Path, sync::Arc, time::Duration}; use util::ResultExt; use worktree::Snapshot; @@ -451,7 +445,7 @@ struct ChunkFiles { pub struct ChunkedFile { pub path: Arc, - pub mtime: Option, + pub mtime: Option, pub handle: IndexingEntryHandle, pub text: String, pub chunks: Vec, @@ -465,7 +459,7 @@ pub struct EmbedFiles { #[derive(Debug, Serialize, Deserialize)] pub struct EmbeddedFile { pub path: Arc, - pub mtime: Option, + pub mtime: Option, pub chunks: Vec, } diff --git a/crates/semantic_index/src/summary_backlog.rs b/crates/semantic_index/src/summary_backlog.rs index c6d8e33a45..e77fa4862f 100644 --- a/crates/semantic_index/src/summary_backlog.rs +++ b/crates/semantic_index/src/summary_backlog.rs @@ -1,5 +1,6 @@ use collections::HashMap; -use std::{path::Path, sync::Arc, time::SystemTime}; +use fs::MTime; +use std::{path::Path, sync::Arc}; const MAX_FILES_BEFORE_RESUMMARIZE: usize = 4; const MAX_BYTES_BEFORE_RESUMMARIZE: u64 = 1_000_000; // 1 MB @@ -7,14 +8,14 @@ const MAX_BYTES_BEFORE_RESUMMARIZE: u64 = 1_000_000; // 1 MB #[derive(Default, Debug)] pub struct SummaryBacklog { /// Key: path to a file that needs summarization, but that we haven't summarized yet. Value: that file's size on disk, in bytes, and its mtime. - files: HashMap, (u64, Option)>, + files: HashMap, (u64, Option)>, /// Cache of the sum of all values in `files`, so we don't have to traverse the whole map to check if we're over the byte limit. total_bytes: u64, } impl SummaryBacklog { /// Store the given path in the backlog, along with how many bytes are in it. - pub fn insert(&mut self, path: Arc, bytes_on_disk: u64, mtime: Option) { + pub fn insert(&mut self, path: Arc, bytes_on_disk: u64, mtime: Option) { let (prev_bytes, _) = self .files .insert(path, (bytes_on_disk, mtime)) @@ -34,7 +35,7 @@ impl SummaryBacklog { /// Remove all the entries in the backlog and return the file paths as an iterator. #[allow(clippy::needless_lifetimes)] // Clippy thinks this 'a can be elided, but eliding it gives a compile error - pub fn drain<'a>(&'a mut self) -> impl Iterator, Option)> + 'a { + pub fn drain<'a>(&'a mut self) -> impl Iterator, Option)> + 'a { self.total_bytes = 0; self.files diff --git a/crates/semantic_index/src/summary_index.rs b/crates/semantic_index/src/summary_index.rs index 1cbb670397..44cac88564 100644 --- a/crates/semantic_index/src/summary_index.rs +++ b/crates/semantic_index/src/summary_index.rs @@ -1,6 +1,6 @@ use anyhow::{anyhow, Context as _, Result}; use arrayvec::ArrayString; -use fs::Fs; +use fs::{Fs, MTime}; use futures::{stream::StreamExt, TryFutureExt}; use futures_batch::ChunksTimeoutStreamExt; use gpui::{AppContext, Model, Task}; @@ -21,7 +21,7 @@ use std::{ future::Future, path::Path, sync::Arc, - time::{Duration, Instant, SystemTime}, + time::{Duration, Instant}, }; use util::ResultExt; use worktree::Snapshot; @@ -39,7 +39,7 @@ struct UnsummarizedFile { // Path to the file on disk path: Arc, // The mtime of the file on disk - mtime: Option, + mtime: Option, // BLAKE3 hash of the source file's contents digest: Blake3Digest, // The source file's contents @@ -51,7 +51,7 @@ struct SummarizedFile { // Path to the file on disk path: String, // The mtime of the file on disk - mtime: Option, + mtime: Option, // BLAKE3 hash of the source file's contents digest: Blake3Digest, // The LLM's summary of the file's contents @@ -63,7 +63,7 @@ pub type Blake3Digest = ArrayString<{ blake3::OUT_LEN * 2 }>; #[derive(Debug, Serialize, Deserialize)] pub struct FileDigest { - pub mtime: Option, + pub mtime: Option, pub digest: Blake3Digest, } @@ -88,7 +88,7 @@ pub struct SummaryIndex { } struct Backlogged { - paths_to_digest: channel::Receiver, Option)>>, + paths_to_digest: channel::Receiver, Option)>>, task: Task>, } @@ -319,7 +319,7 @@ impl SummaryIndex { digest_db: heed::Database>, txn: &RoTxn<'_>, entry: &Entry, - ) -> Vec<(Arc, Option)> { + ) -> Vec<(Arc, Option)> { let entry_db_key = db_key_for_path(&entry.path); match digest_db.get(&txn, &entry_db_key) { @@ -414,7 +414,7 @@ impl SummaryIndex { fn digest_files( &self, - paths: channel::Receiver, Option)>>, + paths: channel::Receiver, Option)>>, worktree_abs_path: Arc, cx: &AppContext, ) -> MightNeedSummaryFiles { @@ -646,7 +646,7 @@ impl SummaryIndex { let start = Instant::now(); let backlogged = { let (tx, rx) = channel::bounded(512); - let needs_summary: Vec<(Arc, Option)> = { + let needs_summary: Vec<(Arc, Option)> = { let mut backlog = self.backlog.lock(); backlog.drain().collect() diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index bf072ca549..b7ee4466c7 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -7,7 +7,7 @@ use ::ignore::gitignore::{Gitignore, GitignoreBuilder}; use anyhow::{anyhow, Context as _, Result}; use clock::ReplicaId; use collections::{HashMap, HashSet, VecDeque}; -use fs::{copy_recursive, Fs, PathEvent, RemoveOptions, Watcher}; +use fs::{copy_recursive, Fs, MTime, PathEvent, RemoveOptions, Watcher}; use futures::{ channel::{ mpsc::{self, UnboundedSender}, @@ -61,7 +61,7 @@ use std::{ atomic::{AtomicUsize, Ordering::SeqCst}, Arc, }, - time::{Duration, Instant, SystemTime}, + time::{Duration, Instant}, }; use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet}; use text::{LineEnding, Rope}; @@ -3395,7 +3395,7 @@ pub struct Entry { pub kind: EntryKind, pub path: Arc, pub inode: u64, - pub mtime: Option, + pub mtime: Option, pub canonical_path: Option>, /// Whether this entry is ignored by Git. From 933c11a9b2c071b7ab8465542fcec3f824a6cee0 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Thu, 21 Nov 2024 23:06:03 -0500 Subject: [PATCH 63/68] Remove dead snowflake code (#21041) Co-authored-by: Nathan Sobo <1789+nathansobo@users.noreply.github.com> Release Notes: - N/A --- crates/collab/src/api/events.rs | 45 --------------------------------- 1 file changed, 45 deletions(-) diff --git a/crates/collab/src/api/events.rs b/crates/collab/src/api/events.rs index 2679193cad..3cda6a397a 100644 --- a/crates/collab/src/api/events.rs +++ b/crates/collab/src/api/events.rs @@ -1588,48 +1588,3 @@ struct SnowflakeRow { pub user_properties: Option, pub insert_id: Option, } - -#[derive(Serialize, Deserialize)] -struct SnowflakeData { - /// Identifier unique to each Zed installation (differs for stable, preview, dev) - pub installation_id: Option, - /// Identifier unique to each logged in Zed user (randomly generated on first sign in) - /// Identifier unique to each Zed session (differs for each time you open Zed) - pub session_id: Option, - pub metrics_id: Option, - /// True for Zed staff, otherwise false - pub is_staff: Option, - /// Zed version number - pub app_version: String, - pub os_name: String, - pub os_version: Option, - pub architecture: String, - /// Zed release channel (stable, preview, dev) - pub release_channel: Option, - pub signed_in: bool, - - #[serde(flatten)] - pub editor_event: Option, - #[serde(flatten)] - pub inline_completion_event: Option, - #[serde(flatten)] - pub call_event: Option, - #[serde(flatten)] - pub assistant_event: Option, - #[serde(flatten)] - pub cpu_event: Option, - #[serde(flatten)] - pub memory_event: Option, - #[serde(flatten)] - pub app_event: Option, - #[serde(flatten)] - pub setting_event: Option, - #[serde(flatten)] - pub extension_event: Option, - #[serde(flatten)] - pub edit_event: Option, - #[serde(flatten)] - pub repl_event: Option, - #[serde(flatten)] - pub action_event: Option, -} From 114c4621433eb948b87fa5f4f41df8af6601f66e Mon Sep 17 00:00:00 2001 From: tims <0xtimsb@gmail.com> Date: Fri, 22 Nov 2024 16:29:04 +0530 Subject: [PATCH 64/68] Maintain selection on file/dir deletion in project panel (#20577) Closes #20444 - Focus on next file/dir on deletion. - Focus on prev file/dir in case where it's last item in worktree. - Tested when multiple files/dirs are being deleted. Release Notes: - Maintain selection on file/dir deletion in project panel. --------- Co-authored-by: Kirill Bulatov --- crates/project_panel/src/project_panel.rs | 800 +++++++++++++++++++++- crates/util/src/paths.rs | 32 +- crates/workspace/src/pane.rs | 2 +- 3 files changed, 813 insertions(+), 21 deletions(-) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 5ad2c2d12e..c757924727 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -40,6 +40,7 @@ use serde::{Deserialize, Serialize}; use smallvec::SmallVec; use std::{ cell::OnceCell, + cmp, collections::HashSet, ffi::OsStr, ops::Range, @@ -53,7 +54,7 @@ use ui::{ IndentGuideColors, IndentGuideLayout, KeyBinding, Label, ListItem, Scrollbar, ScrollbarState, Tooltip, }; -use util::{maybe, ResultExt, TryFutureExt}; +use util::{maybe, paths::compare_paths, ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, notifications::{DetachAndPromptErr, NotifyTaskExt}, @@ -550,7 +551,7 @@ impl ProjectPanel { .entry((project_path.worktree_id, path_buffer.clone())) .and_modify(|strongest_diagnostic_severity| { *strongest_diagnostic_severity = - std::cmp::min(*strongest_diagnostic_severity, diagnostic_severity); + cmp::min(*strongest_diagnostic_severity, diagnostic_severity); }) .or_insert(diagnostic_severity); } @@ -1184,15 +1185,15 @@ impl ProjectPanel { fn remove(&mut self, trash: bool, skip_prompt: bool, cx: &mut ViewContext<'_, ProjectPanel>) { maybe!({ - if self.marked_entries.is_empty() && self.selection.is_none() { + let items_to_delete = self.disjoint_entries_for_removal(cx); + if items_to_delete.is_empty() { return None; } let project = self.project.read(cx); - let items_to_delete = self.marked_entries(); let mut dirty_buffers = 0; let file_paths = items_to_delete - .into_iter() + .iter() .filter_map(|selection| { let project_path = project.path_for_entry(selection.entry_id, cx)?; dirty_buffers += @@ -1261,28 +1262,120 @@ impl ProjectPanel { } else { None }; - - cx.spawn(|this, mut cx| async move { + let next_selection = self.find_next_selection_after_deletion(items_to_delete, cx); + cx.spawn(|panel, mut cx| async move { if let Some(answer) = answer { if answer.await != Ok(0) { - return Result::<(), anyhow::Error>::Ok(()); + return anyhow::Ok(()); } } for (entry_id, _) in file_paths { - this.update(&mut cx, |this, cx| { - this.project - .update(cx, |project, cx| project.delete_entry(entry_id, trash, cx)) - .ok_or_else(|| anyhow!("no such entry")) - })?? - .await?; + panel + .update(&mut cx, |panel, cx| { + panel + .project + .update(cx, |project, cx| project.delete_entry(entry_id, trash, cx)) + .context("no such entry") + })?? + .await?; } - Result::<(), anyhow::Error>::Ok(()) + panel.update(&mut cx, |panel, cx| { + if let Some(next_selection) = next_selection { + panel.selection = Some(next_selection); + panel.autoscroll(cx); + } else { + panel.select_last(&SelectLast {}, cx); + } + })?; + Ok(()) }) .detach_and_log_err(cx); Some(()) }); } + fn find_next_selection_after_deletion( + &self, + sanitized_entries: BTreeSet, + cx: &mut ViewContext, + ) -> Option { + if sanitized_entries.is_empty() { + return None; + } + + let project = self.project.read(cx); + let (worktree_id, worktree) = sanitized_entries + .iter() + .map(|entry| entry.worktree_id) + .filter_map(|id| project.worktree_for_id(id, cx).map(|w| (id, w.read(cx)))) + .max_by(|(_, a), (_, b)| a.root_name().cmp(b.root_name()))?; + + let marked_entries_in_worktree = sanitized_entries + .iter() + .filter(|e| e.worktree_id == worktree_id) + .collect::>(); + let latest_entry = marked_entries_in_worktree + .iter() + .max_by(|a, b| { + match ( + worktree.entry_for_id(a.entry_id), + worktree.entry_for_id(b.entry_id), + ) { + (Some(a), Some(b)) => { + compare_paths((&a.path, a.is_file()), (&b.path, b.is_file())) + } + _ => cmp::Ordering::Equal, + } + }) + .and_then(|e| worktree.entry_for_id(e.entry_id))?; + + let parent_path = latest_entry.path.parent()?; + let parent_entry = worktree.entry_for_path(parent_path)?; + + // Remove all siblings that are being deleted except the last marked entry + let mut siblings: Vec = worktree + .snapshot() + .child_entries(parent_path) + .filter(|sibling| { + sibling.id == latest_entry.id + || !marked_entries_in_worktree.contains(&&SelectedEntry { + worktree_id, + entry_id: sibling.id, + }) + }) + .cloned() + .collect(); + + project::sort_worktree_entries(&mut siblings); + let sibling_entry_index = siblings + .iter() + .position(|sibling| sibling.id == latest_entry.id)?; + + if let Some(next_sibling) = sibling_entry_index + .checked_add(1) + .and_then(|i| siblings.get(i)) + { + return Some(SelectedEntry { + worktree_id, + entry_id: next_sibling.id, + }); + } + if let Some(prev_sibling) = sibling_entry_index + .checked_sub(1) + .and_then(|i| siblings.get(i)) + { + return Some(SelectedEntry { + worktree_id, + entry_id: prev_sibling.id, + }); + } + // No neighbour sibling found, fall back to parent + Some(SelectedEntry { + worktree_id, + entry_id: parent_entry.id, + }) + } + fn unfold_directory(&mut self, _: &UnfoldDirectory, cx: &mut ViewContext) { if let Some((worktree, entry)) = self.selected_entry(cx) { self.unfolded_dir_ids.insert(entry.id); @@ -1835,6 +1928,54 @@ impl ProjectPanel { None } + fn disjoint_entries_for_removal(&self, cx: &AppContext) -> BTreeSet { + let marked_entries = self.marked_entries(); + let mut sanitized_entries = BTreeSet::new(); + if marked_entries.is_empty() { + return sanitized_entries; + } + + let project = self.project.read(cx); + let marked_entries_by_worktree: HashMap> = marked_entries + .into_iter() + .filter(|entry| !project.entry_is_worktree_root(entry.entry_id, cx)) + .fold(HashMap::default(), |mut map, entry| { + map.entry(entry.worktree_id).or_default().push(entry); + map + }); + + for (worktree_id, marked_entries) in marked_entries_by_worktree { + if let Some(worktree) = project.worktree_for_id(worktree_id, cx) { + let worktree = worktree.read(cx); + let marked_dir_paths = marked_entries + .iter() + .filter_map(|entry| { + worktree.entry_for_id(entry.entry_id).and_then(|entry| { + if entry.is_dir() { + Some(entry.path.as_ref()) + } else { + None + } + }) + }) + .collect::>(); + + sanitized_entries.extend(marked_entries.into_iter().filter(|entry| { + let Some(entry_info) = worktree.entry_for_id(entry.entry_id) else { + return false; + }; + let entry_path = entry_info.path.as_ref(); + let inside_marked_dir = marked_dir_paths.iter().any(|&marked_dir_path| { + entry_path != marked_dir_path && entry_path.starts_with(marked_dir_path) + }); + !inside_marked_dir + })); + } + } + + sanitized_entries + } + // Returns list of entries that should be affected by an operation. // When currently selected entry is not marked, it's treated as the only marked entry. fn marked_entries(&self) -> BTreeSet { @@ -5080,14 +5221,13 @@ mod tests { &[ "v src", " v test", - " second.rs", + " second.rs <== selected", " third.rs" ], "Project panel should have no deleted file, no other file is selected in it" ); ensure_no_open_items_and_panes(&workspace, cx); - select_path(&panel, "src/test/second.rs", cx); panel.update(cx, |panel, cx| panel.open(&Open, cx)); cx.executor().run_until_parked(); assert_eq!( @@ -5121,7 +5261,7 @@ mod tests { submit_deletion_skipping_prompt(&panel, cx); assert_eq!( visible_entries_as_strings(&panel, 0..10, cx), - &["v src", " v test", " third.rs"], + &["v src", " v test", " third.rs <== selected"], "Project panel should have no deleted file, with one last file remaining" ); ensure_no_open_items_and_panes(&workspace, cx); @@ -5630,7 +5770,11 @@ mod tests { submit_deletion(&panel, cx); assert_eq!( visible_entries_as_strings(&panel, 0..10, cx), - &["v project_root", " v dir_1", " v nested_dir",] + &[ + "v project_root", + " v dir_1", + " v nested_dir <== selected", + ] ); } #[gpui::test] @@ -6327,6 +6471,598 @@ mod tests { ); } + #[gpui::test] + async fn test_basic_file_deletion_scenarios(cx: &mut gpui::TestAppContext) { + init_test_with_editor(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/root", + json!({ + "dir1": { + "subdir1": {}, + "file1.txt": "", + "file2.txt": "", + }, + "dir2": { + "subdir2": {}, + "file3.txt": "", + "file4.txt": "", + }, + "file5.txt": "", + "file6.txt": "", + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + + toggle_expand_dir(&panel, "root/dir1", cx); + toggle_expand_dir(&panel, "root/dir2", cx); + + // Test Case 1: Delete middle file in directory + select_path(&panel, "root/dir1/file1.txt", cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..15, cx), + &[ + "v root", + " v dir1", + " > subdir1", + " file1.txt <== selected", + " file2.txt", + " v dir2", + " > subdir2", + " file3.txt", + " file4.txt", + " file5.txt", + " file6.txt", + ], + "Initial state before deleting middle file" + ); + + submit_deletion(&panel, cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..15, cx), + &[ + "v root", + " v dir1", + " > subdir1", + " file2.txt <== selected", + " v dir2", + " > subdir2", + " file3.txt", + " file4.txt", + " file5.txt", + " file6.txt", + ], + "Should select next file after deleting middle file" + ); + + // Test Case 2: Delete last file in directory + submit_deletion(&panel, cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..15, cx), + &[ + "v root", + " v dir1", + " > subdir1 <== selected", + " v dir2", + " > subdir2", + " file3.txt", + " file4.txt", + " file5.txt", + " file6.txt", + ], + "Should select next directory when last file is deleted" + ); + + // Test Case 3: Delete root level file + select_path(&panel, "root/file6.txt", cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..15, cx), + &[ + "v root", + " v dir1", + " > subdir1", + " v dir2", + " > subdir2", + " file3.txt", + " file4.txt", + " file5.txt", + " file6.txt <== selected", + ], + "Initial state before deleting root level file" + ); + + submit_deletion(&panel, cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..15, cx), + &[ + "v root", + " v dir1", + " > subdir1", + " v dir2", + " > subdir2", + " file3.txt", + " file4.txt", + " file5.txt <== selected", + ], + "Should select prev entry at root level" + ); + } + + #[gpui::test] + async fn test_complex_selection_scenarios(cx: &mut gpui::TestAppContext) { + init_test_with_editor(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/root", + json!({ + "dir1": { + "subdir1": { + "a.txt": "", + "b.txt": "" + }, + "file1.txt": "", + }, + "dir2": { + "subdir2": { + "c.txt": "", + "d.txt": "" + }, + "file2.txt": "", + }, + "file3.txt": "", + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + + toggle_expand_dir(&panel, "root/dir1", cx); + toggle_expand_dir(&panel, "root/dir1/subdir1", cx); + toggle_expand_dir(&panel, "root/dir2", cx); + toggle_expand_dir(&panel, "root/dir2/subdir2", cx); + + // Test Case 1: Select and delete nested directory with parent + cx.simulate_modifiers_change(gpui::Modifiers { + control: true, + ..Default::default() + }); + select_path_with_mark(&panel, "root/dir1/subdir1", cx); + select_path_with_mark(&panel, "root/dir1", cx); + + assert_eq!( + visible_entries_as_strings(&panel, 0..15, cx), + &[ + "v root", + " v dir1 <== selected <== marked", + " v subdir1 <== marked", + " a.txt", + " b.txt", + " file1.txt", + " v dir2", + " v subdir2", + " c.txt", + " d.txt", + " file2.txt", + " file3.txt", + ], + "Initial state before deleting nested directory with parent" + ); + + submit_deletion(&panel, cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..15, cx), + &[ + "v root", + " v dir2 <== selected", + " v subdir2", + " c.txt", + " d.txt", + " file2.txt", + " file3.txt", + ], + "Should select next directory after deleting directory with parent" + ); + + // Test Case 2: Select mixed files and directories across levels + select_path_with_mark(&panel, "root/dir2/subdir2/c.txt", cx); + select_path_with_mark(&panel, "root/dir2/file2.txt", cx); + select_path_with_mark(&panel, "root/file3.txt", cx); + + assert_eq!( + visible_entries_as_strings(&panel, 0..15, cx), + &[ + "v root", + " v dir2", + " v subdir2", + " c.txt <== marked", + " d.txt", + " file2.txt <== marked", + " file3.txt <== selected <== marked", + ], + "Initial state before deleting" + ); + + submit_deletion(&panel, cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..15, cx), + &[ + "v root", + " v dir2 <== selected", + " v subdir2", + " d.txt", + ], + "Should select sibling directory" + ); + } + + #[gpui::test] + async fn test_delete_all_files_and_directories(cx: &mut gpui::TestAppContext) { + init_test_with_editor(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/root", + json!({ + "dir1": { + "subdir1": { + "a.txt": "", + "b.txt": "" + }, + "file1.txt": "", + }, + "dir2": { + "subdir2": { + "c.txt": "", + "d.txt": "" + }, + "file2.txt": "", + }, + "file3.txt": "", + "file4.txt": "", + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + + toggle_expand_dir(&panel, "root/dir1", cx); + toggle_expand_dir(&panel, "root/dir1/subdir1", cx); + toggle_expand_dir(&panel, "root/dir2", cx); + toggle_expand_dir(&panel, "root/dir2/subdir2", cx); + + // Test Case 1: Select all root files and directories + cx.simulate_modifiers_change(gpui::Modifiers { + control: true, + ..Default::default() + }); + select_path_with_mark(&panel, "root/dir1", cx); + select_path_with_mark(&panel, "root/dir2", cx); + select_path_with_mark(&panel, "root/file3.txt", cx); + select_path_with_mark(&panel, "root/file4.txt", cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..20, cx), + &[ + "v root", + " v dir1 <== marked", + " v subdir1", + " a.txt", + " b.txt", + " file1.txt", + " v dir2 <== marked", + " v subdir2", + " c.txt", + " d.txt", + " file2.txt", + " file3.txt <== marked", + " file4.txt <== selected <== marked", + ], + "State before deleting all contents" + ); + + submit_deletion(&panel, cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..20, cx), + &["v root <== selected"], + "Only empty root directory should remain after deleting all contents" + ); + } + + #[gpui::test] + async fn test_nested_selection_deletion(cx: &mut gpui::TestAppContext) { + init_test_with_editor(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/root", + json!({ + "dir1": { + "subdir1": { + "file_a.txt": "content a", + "file_b.txt": "content b", + }, + "subdir2": { + "file_c.txt": "content c", + }, + "file1.txt": "content 1", + }, + "dir2": { + "file2.txt": "content 2", + }, + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + + toggle_expand_dir(&panel, "root/dir1", cx); + toggle_expand_dir(&panel, "root/dir1/subdir1", cx); + toggle_expand_dir(&panel, "root/dir2", cx); + cx.simulate_modifiers_change(gpui::Modifiers { + control: true, + ..Default::default() + }); + + // Test Case 1: Select parent directory, subdirectory, and a file inside the subdirectory + select_path_with_mark(&panel, "root/dir1", cx); + select_path_with_mark(&panel, "root/dir1/subdir1", cx); + select_path_with_mark(&panel, "root/dir1/subdir1/file_a.txt", cx); + + assert_eq!( + visible_entries_as_strings(&panel, 0..20, cx), + &[ + "v root", + " v dir1 <== marked", + " v subdir1 <== marked", + " file_a.txt <== selected <== marked", + " file_b.txt", + " > subdir2", + " file1.txt", + " v dir2", + " file2.txt", + ], + "State with parent dir, subdir, and file selected" + ); + submit_deletion(&panel, cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..20, cx), + &["v root", " v dir2 <== selected", " file2.txt",], + "Only dir2 should remain after deletion" + ); + } + + #[gpui::test] + async fn test_multiple_worktrees_deletion(cx: &mut gpui::TestAppContext) { + init_test_with_editor(cx); + + let fs = FakeFs::new(cx.executor().clone()); + // First worktree + fs.insert_tree( + "/root1", + json!({ + "dir1": { + "file1.txt": "content 1", + "file2.txt": "content 2", + }, + "dir2": { + "file3.txt": "content 3", + }, + }), + ) + .await; + + // Second worktree + fs.insert_tree( + "/root2", + json!({ + "dir3": { + "file4.txt": "content 4", + "file5.txt": "content 5", + }, + "file6.txt": "content 6", + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + + // Expand all directories for testing + toggle_expand_dir(&panel, "root1/dir1", cx); + toggle_expand_dir(&panel, "root1/dir2", cx); + toggle_expand_dir(&panel, "root2/dir3", cx); + + // Test Case 1: Delete files across different worktrees + cx.simulate_modifiers_change(gpui::Modifiers { + control: true, + ..Default::default() + }); + select_path_with_mark(&panel, "root1/dir1/file1.txt", cx); + select_path_with_mark(&panel, "root2/dir3/file4.txt", cx); + + assert_eq!( + visible_entries_as_strings(&panel, 0..20, cx), + &[ + "v root1", + " v dir1", + " file1.txt <== marked", + " file2.txt", + " v dir2", + " file3.txt", + "v root2", + " v dir3", + " file4.txt <== selected <== marked", + " file5.txt", + " file6.txt", + ], + "Initial state with files selected from different worktrees" + ); + + submit_deletion(&panel, cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..20, cx), + &[ + "v root1", + " v dir1", + " file2.txt", + " v dir2", + " file3.txt", + "v root2", + " v dir3", + " file5.txt <== selected", + " file6.txt", + ], + "Should select next file in the last worktree after deletion" + ); + + // Test Case 2: Delete directories from different worktrees + select_path_with_mark(&panel, "root1/dir1", cx); + select_path_with_mark(&panel, "root2/dir3", cx); + + assert_eq!( + visible_entries_as_strings(&panel, 0..20, cx), + &[ + "v root1", + " v dir1 <== marked", + " file2.txt", + " v dir2", + " file3.txt", + "v root2", + " v dir3 <== selected <== marked", + " file5.txt", + " file6.txt", + ], + "State with directories marked from different worktrees" + ); + + submit_deletion(&panel, cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..20, cx), + &[ + "v root1", + " v dir2", + " file3.txt", + "v root2", + " file6.txt <== selected", + ], + "Should select remaining file in last worktree after directory deletion" + ); + + // Test Case 4: Delete all remaining files except roots + select_path_with_mark(&panel, "root1/dir2/file3.txt", cx); + select_path_with_mark(&panel, "root2/file6.txt", cx); + + assert_eq!( + visible_entries_as_strings(&panel, 0..20, cx), + &[ + "v root1", + " v dir2", + " file3.txt <== marked", + "v root2", + " file6.txt <== selected <== marked", + ], + "State with all remaining files marked" + ); + + submit_deletion(&panel, cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..20, cx), + &["v root1", " v dir2", "v root2 <== selected"], + "Second parent root should be selected after deleting" + ); + } + + #[gpui::test] + async fn test_selection_fallback_to_next_highest_worktree(cx: &mut gpui::TestAppContext) { + init_test_with_editor(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/root_b", + json!({ + "dir1": { + "file1.txt": "content 1", + "file2.txt": "content 2", + }, + }), + ) + .await; + + fs.insert_tree( + "/root_c", + json!({ + "dir2": {}, + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/root_b".as_ref(), "/root_c".as_ref()], cx).await; + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let panel = workspace.update(cx, ProjectPanel::new).unwrap(); + + toggle_expand_dir(&panel, "root_b/dir1", cx); + toggle_expand_dir(&panel, "root_c/dir2", cx); + + cx.simulate_modifiers_change(gpui::Modifiers { + control: true, + ..Default::default() + }); + select_path_with_mark(&panel, "root_b/dir1/file1.txt", cx); + select_path_with_mark(&panel, "root_b/dir1/file2.txt", cx); + + assert_eq!( + visible_entries_as_strings(&panel, 0..20, cx), + &[ + "v root_b", + " v dir1", + " file1.txt <== marked", + " file2.txt <== selected <== marked", + "v root_c", + " v dir2", + ], + "Initial state with files marked in root_b" + ); + + submit_deletion(&panel, cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..20, cx), + &[ + "v root_b", + " v dir1 <== selected", + "v root_c", + " v dir2", + ], + "After deletion in root_b as it's last deletion, selection should be in root_b" + ); + + select_path_with_mark(&panel, "root_c/dir2", cx); + + submit_deletion(&panel, cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..20, cx), + &["v root_b", " v dir1", "v root_c <== selected",], + "After deleting from root_c, it should remain in root_c" + ); + } + fn toggle_expand_dir( panel: &View, path: impl AsRef, @@ -6364,6 +7100,32 @@ mod tests { }); } + fn select_path_with_mark( + panel: &View, + path: impl AsRef, + cx: &mut VisualTestContext, + ) { + let path = path.as_ref(); + panel.update(cx, |panel, cx| { + for worktree in panel.project.read(cx).worktrees(cx).collect::>() { + let worktree = worktree.read(cx); + if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) { + let entry_id = worktree.entry_for_path(relative_path).unwrap().id; + let entry = crate::SelectedEntry { + worktree_id: worktree.id(), + entry_id, + }; + if !panel.marked_entries.contains(&entry) { + panel.marked_entries.insert(entry); + } + panel.selection = Some(entry); + return; + } + } + panic!("no worktree for path {:?}", path); + }); + } + fn find_project_entry( panel: &View, path: impl AsRef, diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index d629c8facc..f4e494f66e 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -378,7 +378,15 @@ pub fn compare_paths( .as_deref() .map(NumericPrefixWithSuffix::from_numeric_prefixed_str); - num_and_remainder_a.cmp(&num_and_remainder_b) + num_and_remainder_a.cmp(&num_and_remainder_b).then_with(|| { + if a_is_file && b_is_file { + let ext_a = path_a.extension().unwrap_or_default(); + let ext_b = path_b.extension().unwrap_or_default(); + ext_a.cmp(ext_b) + } else { + cmp::Ordering::Equal + } + }) }); if !ordering.is_eq() { return ordering; @@ -433,6 +441,28 @@ mod tests { ); } + #[test] + fn compare_paths_with_same_name_different_extensions() { + let mut paths = vec![ + (Path::new("test_dirs/file.rs"), true), + (Path::new("test_dirs/file.txt"), true), + (Path::new("test_dirs/file.md"), true), + (Path::new("test_dirs/file"), true), + (Path::new("test_dirs/file.a"), true), + ]; + paths.sort_by(|&a, &b| compare_paths(a, b)); + assert_eq!( + paths, + vec![ + (Path::new("test_dirs/file"), true), + (Path::new("test_dirs/file.a"), true), + (Path::new("test_dirs/file.md"), true), + (Path::new("test_dirs/file.rs"), true), + (Path::new("test_dirs/file.txt"), true), + ] + ); + } + #[test] fn compare_paths_case_semi_sensitive() { let mut paths = vec![ diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index e9b81d4554..4eec2f18d1 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -48,7 +48,7 @@ use ui::{v_flex, ContextMenu}; use util::{debug_panic, maybe, truncate_and_remove_front, ResultExt}; /// A selected entry in e.g. project panel. -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct SelectedEntry { pub worktree_id: WorktreeId, pub entry_id: ProjectEntryId, From d5f2bca382e5b5d653f917ec9d52fdb028acee2b Mon Sep 17 00:00:00 2001 From: Techatrix Date: Fri, 22 Nov 2024 13:01:00 +0100 Subject: [PATCH 65/68] Filter LSP code actions based on the requested kinds (#20847) I've observed that Zed's implementation of [Code Actions On Format](https://zed.dev/docs/configuring-zed#code-actions-on-format) uses the [CodeActionContext.only](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#codeActionContext) parameter to request specific code action kinds from the server. The issue is that it does not filter out code actions from the response, believing that the server will do it. The [LSP specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#codeActionContext) says that the client is responsible for filtering out unwanted code actions: ```js /** * Requested kind of actions to return. * * Actions not of this kind are filtered out by the client before being * shown. So servers can omit computing them. */ only?: CodeActionKind[]; ``` This PR will filter out unwanted code action on the client side. I have initially encountered this issue because the [ZLS language server](https://github.com/zigtools/zls) (until https://github.com/zigtools/zls/pull/2087) does not filter code action based on `CodeActionContext.only` so Zed runs all received code actions even if they are explicitly disabled in the `code_actions_on_format` setting. Release Notes: - Fix the `code_actions_on_format` setting when used with a language server like ZLS --------- Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> --- .../random_project_collaboration_tests.rs | 2 +- crates/editor/src/editor.rs | 4 +- crates/project/src/lsp_command.rs | 30 +++++-- crates/project/src/lsp_store.rs | 7 +- crates/project/src/project.rs | 7 +- crates/project/src/project_tests.rs | 84 ++++++++++++++++++- 6 files changed, 116 insertions(+), 18 deletions(-) diff --git a/crates/collab/src/tests/random_project_collaboration_tests.rs b/crates/collab/src/tests/random_project_collaboration_tests.rs index 66a9d06804..1f39190d75 100644 --- a/crates/collab/src/tests/random_project_collaboration_tests.rs +++ b/crates/collab/src/tests/random_project_collaboration_tests.rs @@ -835,7 +835,7 @@ impl RandomizedTest for ProjectCollaborationTest { .map_ok(|_| ()) .boxed(), LspRequestKind::CodeAction => project - .code_actions(&buffer, offset..offset, cx) + .code_actions(&buffer, offset..offset, None, cx) .map(|_| Ok(())) .boxed(), LspRequestKind::Definition => project diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b31938bcfd..401462795e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -13811,7 +13811,9 @@ impl CodeActionProvider for Model { range: Range, cx: &mut WindowContext, ) -> Task>> { - self.update(cx, |project, cx| project.code_actions(buffer, range, cx)) + self.update(cx, |project, cx| { + project.code_actions(buffer, range, None, cx) + }) } fn apply_code_action( diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 57f8cea348..6de4902746 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -2090,19 +2090,33 @@ impl LspCommand for GetCodeActions { server_id: LanguageServerId, _: AsyncAppContext, ) -> Result> { + let requested_kinds_set = if let Some(kinds) = self.kinds { + Some(kinds.into_iter().collect::>()) + } else { + None + }; + Ok(actions .unwrap_or_default() .into_iter() .filter_map(|entry| { - if let lsp::CodeActionOrCommand::CodeAction(lsp_action) = entry { - Some(CodeAction { - server_id, - range: self.range.clone(), - lsp_action, - }) - } else { - None + let lsp::CodeActionOrCommand::CodeAction(lsp_action) = entry else { + return None; + }; + + if let Some((requested_kinds, kind)) = + requested_kinds_set.as_ref().zip(lsp_action.kind.as_ref()) + { + if !requested_kinds.contains(kind) { + return None; + } } + + Some(CodeAction { + server_id, + range: self.range.clone(), + lsp_action, + }) }) .collect()) } diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 3ed311a51d..29a4c8e71b 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -2015,6 +2015,7 @@ impl LspStore { &mut self, buffer_handle: &Model, range: Range, + kinds: Option>, cx: &mut ModelContext, ) -> Task>> { if let Some((upstream_client, project_id)) = self.upstream_client() { @@ -2028,7 +2029,7 @@ impl LspStore { request: Some(proto::multi_lsp_query::Request::GetCodeActions( GetCodeActions { range: range.clone(), - kinds: None, + kinds: kinds.clone(), } .to_proto(project_id, buffer_handle.read(cx)), )), @@ -2054,7 +2055,7 @@ impl LspStore { .map(|code_actions_response| { GetCodeActions { range: range.clone(), - kinds: None, + kinds: kinds.clone(), } .response_from_proto( code_actions_response, @@ -2079,7 +2080,7 @@ impl LspStore { Some(range.start), GetCodeActions { range: range.clone(), - kinds: None, + kinds: kinds.clone(), }, cx, ); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 61a700e5d6..40da76ff3a 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -52,8 +52,8 @@ use language::{ Transaction, Unclipped, }; use lsp::{ - CompletionContext, CompletionItemKind, DocumentHighlightKind, LanguageServer, LanguageServerId, - LanguageServerName, MessageActionItem, + CodeActionKind, CompletionContext, CompletionItemKind, DocumentHighlightKind, LanguageServer, + LanguageServerId, LanguageServerName, MessageActionItem, }; use lsp_command::*; use node_runtime::NodeRuntime; @@ -2843,12 +2843,13 @@ impl Project { &mut self, buffer_handle: &Model, range: Range, + kinds: Option>, cx: &mut ModelContext, ) -> Task>> { let buffer = buffer_handle.read(cx); let range = buffer.anchor_before(range.start)..buffer.anchor_before(range.end); self.lsp_store.update(cx, |lsp_store, cx| { - lsp_store.code_actions(buffer_handle, range, cx) + lsp_store.code_actions(buffer_handle, range, kinds, cx) }) } diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index ab00d62d6c..2704259306 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -2792,7 +2792,9 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) { let fake_server = fake_language_servers.next().await.unwrap(); // Language server returns code actions that contain commands, and not edits. - let actions = project.update(cx, |project, cx| project.code_actions(&buffer, 0..0, cx)); + let actions = project.update(cx, |project, cx| { + project.code_actions(&buffer, 0..0, None, cx) + }); fake_server .handle_request::(|_, _| async move { Ok(Some(vec![ @@ -4961,6 +4963,84 @@ async fn test_hovers_with_empty_parts(cx: &mut gpui::TestAppContext) { ); } +#[gpui::test] +async fn test_code_actions_only_kinds(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + "/dir", + json!({ + "a.ts": "a", + }), + ) + .await; + + let project = Project::test(fs, ["/dir".as_ref()], cx).await; + + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + language_registry.add(typescript_lang()); + let mut fake_language_servers = language_registry.register_fake_lsp( + "TypeScript", + FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)), + ..lsp::ServerCapabilities::default() + }, + ..FakeLspAdapter::default() + }, + ); + + let buffer = project + .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx)) + .await + .unwrap(); + cx.executor().run_until_parked(); + + let fake_server = fake_language_servers + .next() + .await + .expect("failed to get the language server"); + + let mut request_handled = fake_server.handle_request::( + move |_, _| async move { + Ok(Some(vec![ + lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction { + title: "organize imports".to_string(), + kind: Some(CodeActionKind::SOURCE_ORGANIZE_IMPORTS), + ..lsp::CodeAction::default() + }), + lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction { + title: "fix code".to_string(), + kind: Some(CodeActionKind::SOURCE_FIX_ALL), + ..lsp::CodeAction::default() + }), + ])) + }, + ); + + let code_actions_task = project.update(cx, |project, cx| { + project.code_actions( + &buffer, + 0..buffer.read(cx).len(), + Some(vec![CodeActionKind::SOURCE_ORGANIZE_IMPORTS]), + cx, + ) + }); + + let () = request_handled + .next() + .await + .expect("The code action request should have been triggered"); + + let code_actions = code_actions_task.await.unwrap(); + assert_eq!(code_actions.len(), 1); + assert_eq!( + code_actions[0].lsp_action.kind, + Some(CodeActionKind::SOURCE_ORGANIZE_IMPORTS) + ); +} + #[gpui::test] async fn test_multiple_language_server_actions(cx: &mut gpui::TestAppContext) { init_test(cx); @@ -5092,7 +5172,7 @@ async fn test_multiple_language_server_actions(cx: &mut gpui::TestAppContext) { } let code_actions_task = project.update(cx, |project, cx| { - project.code_actions(&buffer, 0..buffer.read(cx).len(), cx) + project.code_actions(&buffer, 0..buffer.read(cx).len(), None, cx) }); // cx.run_until_parked(); From b4659bb44ed148f11a0874159fbb58538052dc96 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Fri, 22 Nov 2024 15:10:01 +0000 Subject: [PATCH 66/68] Fix inaccurate Ollama context length for qwen2.5 models (#20933) Since Ollama/llama.cpp do not currently YARN for context length extension, the context length is limited to `32768`. This can be confirmed by the Ollama model card. See corresponding issue on Ollama repo : https://github.com/ollama/ollama/issues/6865 Co-authored-by: Patrick Samson <1416027+patricksamson@users.noreply.github.com> --- crates/ollama/src/ollama.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/ollama/src/ollama.rs b/crates/ollama/src/ollama.rs index a133085020..5168da38be 100644 --- a/crates/ollama/src/ollama.rs +++ b/crates/ollama/src/ollama.rs @@ -81,9 +81,10 @@ fn get_max_tokens(name: &str) -> usize { "llama2" | "yi" | "vicuna" | "stablelm2" => 4096, "llama3" | "gemma2" | "gemma" | "codegemma" | "starcoder" | "aya" => 8192, "codellama" | "starcoder2" => 16384, - "mistral" | "codestral" | "mixstral" | "llava" | "qwen2" | "dolphin-mixtral" => 32768, + "mistral" | "codestral" | "mixstral" | "llava" | "qwen2" | "qwen2.5-coder" + | "dolphin-mixtral" => 32768, "llama3.1" | "phi3" | "phi3.5" | "command-r" | "deepseek-coder-v2" | "yi-coder" - | "llama3.2" | "qwen2.5-coder" => 128000, + | "llama3.2" => 128000, _ => DEFAULT_TOKENS, } .clamp(1, MAXIMUM_TOKENS) From d489f96aefb6166f42fcdd9cd40f1c765244b66d Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 22 Nov 2024 10:58:11 -0500 Subject: [PATCH 67/68] Don't name `ExtensionLspAdapter` in `ExtensionRegistrationHooks` (#21064) This PR updates the `ExtensionRegistrationHooks` trait to not name the `ExtensionLspAdapter` type. This helps decouple the two. Release Notes: - N/A --- crates/extension_host/src/extension_host.rs | 16 +++++----- .../src/extension_lsp_adapter.rs | 20 +++++++++++-- .../src/extension_store_test.rs | 20 +++++++++---- crates/extension_host/src/headless_host.rs | 29 ++++++++++++------- .../src/extension_registration_hooks.rs | 18 ++++++++---- 5 files changed, 73 insertions(+), 30 deletions(-) diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index 4a832faeff..236c8091b4 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -6,7 +6,6 @@ pub mod wasm_host; #[cfg(test)] mod extension_store_test; -use crate::extension_lsp_adapter::ExtensionLspAdapter; use anyhow::{anyhow, bail, Context as _, Result}; use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; @@ -122,7 +121,13 @@ pub trait ExtensionRegistrationHooks: Send + Sync + 'static { ) { } - fn register_lsp_adapter(&self, _language: LanguageName, _adapter: ExtensionLspAdapter) {} + fn register_lsp_adapter( + &self, + _extension: Arc, + _language_server_id: LanguageServerName, + _language: LanguageName, + ) { + } fn remove_lsp_adapter(&self, _language: &LanguageName, _server_name: &LanguageServerName) {} @@ -1255,12 +1260,9 @@ impl ExtensionStore { for (language_server_id, language_server_config) in &manifest.language_servers { for language in language_server_config.languages() { this.registration_hooks.register_lsp_adapter( + extension.clone(), + language_server_id.clone(), language.clone(), - ExtensionLspAdapter { - extension: extension.clone(), - language_server_id: language_server_id.clone(), - language_name: language.clone(), - }, ); } } diff --git a/crates/extension_host/src/extension_lsp_adapter.rs b/crates/extension_host/src/extension_lsp_adapter.rs index 8f83c68e31..069eddba57 100644 --- a/crates/extension_host/src/extension_lsp_adapter.rs +++ b/crates/extension_host/src/extension_lsp_adapter.rs @@ -45,9 +45,23 @@ impl WorktreeDelegate for WorktreeDelegateAdapter { } pub struct ExtensionLspAdapter { - pub(crate) extension: Arc, - pub(crate) language_server_id: LanguageServerName, - pub(crate) language_name: LanguageName, + extension: Arc, + language_server_id: LanguageServerName, + language_name: LanguageName, +} + +impl ExtensionLspAdapter { + pub fn new( + extension: Arc, + language_server_id: LanguageServerName, + language_name: LanguageName, + ) -> Self { + Self { + extension, + language_server_id, + language_name, + } + } } #[async_trait(?Send)] diff --git a/crates/extension_host/src/extension_store_test.rs b/crates/extension_host/src/extension_store_test.rs index 23004e9d7f..5d78539617 100644 --- a/crates/extension_host/src/extension_store_test.rs +++ b/crates/extension_host/src/extension_store_test.rs @@ -7,11 +7,14 @@ use crate::{ use anyhow::Result; use async_compression::futures::bufread::GzipEncoder; use collections::BTreeMap; +use extension::Extension; use fs::{FakeFs, Fs, RealFs}; use futures::{io::BufReader, AsyncReadExt, StreamExt}; use gpui::{BackgroundExecutor, Context, SemanticVersion, SharedString, Task, TestAppContext}; use http_client::{FakeHttpClient, Response}; -use language::{LanguageMatcher, LanguageRegistry, LanguageServerBinaryStatus, LoadedLanguage}; +use language::{ + LanguageMatcher, LanguageName, LanguageRegistry, LanguageServerBinaryStatus, LoadedLanguage, +}; use lsp::LanguageServerName; use node_runtime::NodeRuntime; use parking_lot::Mutex; @@ -80,11 +83,18 @@ impl ExtensionRegistrationHooks for TestExtensionRegistrationHooks { fn register_lsp_adapter( &self, - language_name: language::LanguageName, - adapter: ExtensionLspAdapter, + extension: Arc, + language_server_id: LanguageServerName, + language: LanguageName, ) { - self.language_registry - .register_lsp_adapter(language_name, Arc::new(adapter)); + self.language_registry.register_lsp_adapter( + language.clone(), + Arc::new(ExtensionLspAdapter::new( + extension, + language_server_id, + language, + )), + ); } fn update_lsp_status( diff --git a/crates/extension_host/src/headless_host.rs b/crates/extension_host/src/headless_host.rs index e297794bf1..6ad8b71aa3 100644 --- a/crates/extension_host/src/headless_host.rs +++ b/crates/extension_host/src/headless_host.rs @@ -177,20 +177,17 @@ impl HeadlessExtensionStore { let wasm_extension: Arc = Arc::new(WasmExtension::load(extension_dir, &manifest, wasm_host.clone(), &cx).await?); - for (language_server_name, language_server_config) in &manifest.language_servers { + for (language_server_id, language_server_config) in &manifest.language_servers { for language in language_server_config.languages() { this.update(cx, |this, _cx| { this.loaded_language_servers .entry(manifest.id.clone()) .or_default() - .push((language_server_name.clone(), language.clone())); + .push((language_server_id.clone(), language.clone())); this.registration_hooks.register_lsp_adapter( + wasm_extension.clone(), + language_server_id.clone(), language.clone(), - ExtensionLspAdapter { - extension: wasm_extension.clone(), - language_server_id: language_server_name.clone(), - language_name: language, - }, ); })?; } @@ -344,10 +341,22 @@ impl ExtensionRegistrationHooks for HeadlessRegistrationHooks { self.language_registry .register_language(language, None, matcher, load) } - fn register_lsp_adapter(&self, language: LanguageName, adapter: ExtensionLspAdapter) { + + fn register_lsp_adapter( + &self, + extension: Arc, + language_server_id: LanguageServerName, + language: LanguageName, + ) { log::info!("registering lsp adapter {:?}", language); - self.language_registry - .register_lsp_adapter(language, Arc::new(adapter) as _); + self.language_registry.register_lsp_adapter( + language.clone(), + Arc::new(ExtensionLspAdapter::new( + extension, + language_server_id, + language, + )), + ); } fn register_wasm_grammars(&self, grammars: Vec<(Arc, PathBuf)>) { diff --git a/crates/extensions_ui/src/extension_registration_hooks.rs b/crates/extensions_ui/src/extension_registration_hooks.rs index f8cd9a3429..07a4c1455c 100644 --- a/crates/extensions_ui/src/extension_registration_hooks.rs +++ b/crates/extensions_ui/src/extension_registration_hooks.rs @@ -11,7 +11,8 @@ use extension_host::{extension_lsp_adapter::ExtensionLspAdapter, wasm_host}; use fs::Fs; use gpui::{AppContext, BackgroundExecutor, Model, Task}; use indexed_docs::{ExtensionIndexedDocsProvider, IndexedDocsRegistry, ProviderId}; -use language::{LanguageRegistry, LanguageServerBinaryStatus, LoadedLanguage}; +use language::{LanguageName, LanguageRegistry, LanguageServerBinaryStatus, LoadedLanguage}; +use lsp::LanguageServerName; use snippet_provider::SnippetRegistry; use theme::{ThemeRegistry, ThemeSettings}; use ui::SharedString; @@ -159,11 +160,18 @@ impl extension_host::ExtensionRegistrationHooks for ConcreteExtensionRegistratio fn register_lsp_adapter( &self, - language_name: language::LanguageName, - adapter: ExtensionLspAdapter, + extension: Arc, + language_server_id: LanguageServerName, + language: LanguageName, ) { - self.language_registry - .register_lsp_adapter(language_name, Arc::new(adapter)); + self.language_registry.register_lsp_adapter( + language.clone(), + Arc::new(ExtensionLspAdapter::new( + extension, + language_server_id, + language, + )), + ); } fn remove_lsp_adapter( From 852fb5152869f31a9bc96bdd7459135f2ea2d1cd Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 22 Nov 2024 09:20:49 -0700 Subject: [PATCH 68/68] Canonicalize paths when opening workspaces (#21039) Closes #17161 Release Notes: - Added symlink resolution when opening projects from within Zed. Previously this only happened within zed's cli, but that broke file watching on Linux when opening a symlinked directory. --- crates/workspace/src/workspace.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 32e441ee50..45de781577 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1096,10 +1096,17 @@ impl Workspace { ); cx.spawn(|mut cx| async move { - let serialized_workspace: Option = - persistence::DB.workspace_for_roots(abs_paths.as_slice()); + let mut paths_to_open = Vec::with_capacity(abs_paths.len()); + for path in abs_paths.into_iter() { + if let Some(canonical) = app_state.fs.canonicalize(&path).await.ok() { + paths_to_open.push(canonical) + } else { + paths_to_open.push(path) + } + } - let mut paths_to_open = abs_paths; + let serialized_workspace: Option = + persistence::DB.workspace_for_roots(paths_to_open.as_slice()); let workspace_location = serialized_workspace .as_ref()