From 9260abafbaa3eddb3922da3555d6dbe9903b05d0 Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Mon, 18 Nov 2024 05:38:31 -0700 Subject: [PATCH 001/886] Use `HashMap` instead of `HashSet` in outline_panel (#20780) Came across this because I noticed that `Entry` implements `Hash`, which was surprising to me. I believe that `ProjectEntryId` should be unique and so it seems better to dedupe based on this. Release Notes: - N/A --- crates/outline_panel/src/outline_panel.rs | 16 ++++++++++------ crates/worktree/src/worktree.rs | 4 ++-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index a6d1903282..f878b582d9 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -2178,8 +2178,10 @@ impl OutlinePanel { .background_executor() .spawn(async move { let mut processed_external_buffers = HashSet::default(); - let mut new_worktree_entries = - HashMap::)>::default(); + let mut new_worktree_entries = HashMap::< + WorktreeId, + (worktree::Snapshot, HashMap), + >::default(); let mut worktree_excerpts = HashMap::< WorktreeId, HashMap)>, @@ -2213,7 +2215,7 @@ impl OutlinePanel { entry.path.as_ref(), ); - let mut entries_to_add = HashSet::default(); + let mut entries_to_add = HashMap::default(); worktree_excerpts .entry(worktree_id) .or_default() @@ -2238,7 +2240,9 @@ impl OutlinePanel { } } - let new_entry_added = entries_to_add.insert(current_entry); + let new_entry_added = entries_to_add + .insert(current_entry.id, current_entry) + .is_none(); if new_entry_added && traversal.back_to_parent() { if let Some(parent_entry) = traversal.entry() { current_entry = parent_entry.clone(); @@ -2249,7 +2253,7 @@ impl OutlinePanel { } new_worktree_entries .entry(worktree_id) - .or_insert_with(|| (worktree.clone(), HashSet::default())) + .or_insert_with(|| (worktree.clone(), HashMap::default())) .1 .extend(entries_to_add); } @@ -2276,7 +2280,7 @@ impl OutlinePanel { let worktree_entries = new_worktree_entries .into_iter() .map(|(worktree_id, (worktree_snapshot, entries))| { - let mut entries = entries.into_iter().collect::>(); + let mut entries = entries.into_values().collect::>(); // For a proper git status propagation, we have to keep the entries sorted lexicographically. entries.sort_by(|a, b| a.path.as_ref().cmp(b.path.as_ref())); worktree_snapshot.propagate_git_statuses(&mut entries); diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 28b23d2fa7..a3e290e6d6 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -3344,7 +3344,7 @@ impl File { } } -#[derive(Clone, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Entry { pub id: ProjectEntryId, pub kind: EntryKind, @@ -3376,7 +3376,7 @@ pub struct Entry { pub is_fifo: bool, } -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum EntryKind { UnloadedDir, PendingDir, From 5fd7afb9da3ff54ea1841a99e7c2d32b4beae994 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Mon, 18 Nov 2024 14:23:29 +0000 Subject: [PATCH 002/886] docs: More language extension config.toml key documentation (#20818) Release Notes: - N/A --- docs/src/extensions/languages.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/src/extensions/languages.md b/docs/src/extensions/languages.md index b7e0cb4482..e4fc1aca2b 100644 --- a/docs/src/extensions/languages.md +++ b/docs/src/extensions/languages.md @@ -20,22 +20,22 @@ path_suffixes = ["myl"] line_comments = ["# "] ``` -- `name` is the human readable name that will show up in the Select Language dropdown. -- `grammar` is the name of a grammar. Grammars are registered separately, described below. -- `path_suffixes` (optional) is an array of file suffixes that should be associated with this language. This supports glob patterns like `config/**/*.toml` where `**` matches 0 or more directories and `*` matches 0 or more characters. -- `line_comments` (optional) is an array of strings that are used to identify line comments in the language. +- `name` (required) is the human readable name that will show up in the Select Language dropdown. +- `grammar` (required) is the name of a grammar. Grammars are registered separately, described below. +- `path_suffixes` is an array of file suffixes that should be associated with this language. Unlike `file_types` in settings, this does not support glob patterns. +- `line_comments` is an array of strings that are used to identify line comments in the language. This is used for the `editor::ToggleComments` keybind: `{#kb editor::ToggleComments}` for toggling lines of code. +- `tab_size` defines the indentation/tab size used for this language (default is `4`). +- `hard_tabs` whether to indent with tabs (`true`) or spaces (`false`, the default). +- `first_line_pattern` is a regular expression, that in addition to `path_suffixes` (above) or `file_types` in settings can be used to match files which should use this language. For example Zed uses this to identify Shell Scripts by matching the [shebangs lines](https://github.com/zed-industries/zed/blob/main/crates/languages/src/bash/config.toml) in the first line of a script. ## Grammar From d265e44209f120b0c4804aef6b3f3ce047a01ddf Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Mon, 18 Nov 2024 10:55:44 -0800 Subject: [PATCH 010/886] Don't treat absence of a file on fs as conflict for new files from CLI (#20828) Closes #20827 Release Notes: - Fixes bug where save for new files created via CLI would report a conflict and ask about overwriting. --- crates/language/src/buffer.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 571e444d7c..d1a01c26e6 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1775,11 +1775,12 @@ impl Buffer { return false; }; match file.disk_state() { - DiskState::New | DiskState::Deleted => true, + DiskState::New => false, DiskState::Present { mtime } => match self.saved_mtime { Some(saved_mtime) => mtime > saved_mtime && self.has_unsaved_edits(), None => true, }, + DiskState::Deleted => true, } } From 37899187c630a629d3e2c972c4e1316f7d3a0195 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 18 Nov 2024 16:32:16 -0300 Subject: [PATCH 011/886] Adjust file finder width configuration (#20819) Follow up to: https://github.com/zed-industries/zed/pull/18682 This PR tweaks the setting value, so it's clear we're referring to `max-width`, meaning the width will change up to a specific value depending on the available window size. Then, it also makes `Small` the default value, which, in practice, makes the modal size the same as it was before the original PR linked above. Release Notes: - N/A --------- Co-authored-by: Kirill Bulatov --- assets/settings/default.json | 28 +++++++------ crates/file_finder/src/file_finder.rs | 25 ++++++++--- .../file_finder/src/file_finder_settings.rs | 42 +++---------------- docs/src/configuring-zed.md | 8 ++-- 4 files changed, 44 insertions(+), 59 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index c881c61d35..2352e75ee9 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -581,20 +581,22 @@ "file_finder": { // Whether to show file icons in the file finder. "file_icons": true, - // Width of the file finder modal. This setting can - // take four values. + // Determines how much space the file finder can take up in relation to the available window width. + // There are 5 possible width values: // - // 1. Small width: - // "modal_width": "small", - // 2. Medium width (default): - // "modal_width": "medium", - // 3. Large width: - // "modal_width": "large", - // 4. Extra Large width: - // "modal_width": "xlarge" - // 5. Fullscreen width: - // "modal_width": "full" - "modal_width": "medium" + // 1. Small: This value is essentially a fixed width. + // "modal_width": "small" + // 2. Medium: + // "modal_width": "medium" + // 3. Large: + // "modal_width": "large" + // 4. Extra Large: + // "modal_width": "xlarge" + // 5. Fullscreen: This value removes any horizontal padding, as it consumes the whole viewport width. + // "modal_width": "full" + // + // Default: small + "modal_max_width": "small" }, // Whether or not to remove any trailing whitespace from lines of a buffer // before saving it. diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index f9c058de23..138a02d1f6 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -10,7 +10,7 @@ pub use open_path_prompt::OpenPathDelegate; use collections::HashMap; use editor::{scroll::Autoscroll, Bias, Editor}; -use file_finder_settings::FileFinderSettings; +use file_finder_settings::{FileFinderSettings, FileFinderWidth}; use file_icons::FileIcons; use fuzzy::{CharBag, PathMatch, PathMatchCandidate}; use gpui::{ @@ -244,6 +244,22 @@ impl FileFinder { } }) } + + pub fn modal_max_width( + width_setting: Option, + cx: &mut ViewContext, + ) -> Pixels { + let window_width = cx.viewport_size().width; + let small_width = Pixels(545.); + + match width_setting { + None | Some(FileFinderWidth::Small) => small_width, + Some(FileFinderWidth::Full) => window_width, + Some(FileFinderWidth::XLarge) => (window_width - Pixels(512.)).max(small_width), + Some(FileFinderWidth::Large) => (window_width - Pixels(768.)).max(small_width), + Some(FileFinderWidth::Medium) => (window_width - Pixels(1024.)).max(small_width), + } + } } impl EventEmitter for FileFinder {} @@ -258,13 +274,12 @@ impl Render for FileFinder { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let key_context = self.picker.read(cx).delegate.key_context(cx); - let window_max_width: Pixels = cx.viewport_size().width; - let modal_choice = FileFinderSettings::get_global(cx).modal_width; - let width = modal_choice.calc_width(window_max_width); + let file_finder_settings = FileFinderSettings::get_global(cx); + let modal_max_width = Self::modal_max_width(file_finder_settings.modal_max_width, cx); v_flex() .key_context(key_context) - .w(width) + .w(modal_max_width) .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed)) .on_action(cx.listener(Self::handle_select_prev)) .on_action(cx.listener(Self::handle_open_menu)) diff --git a/crates/file_finder/src/file_finder_settings.rs b/crates/file_finder/src/file_finder_settings.rs index 4379c8f543..0512021d87 100644 --- a/crates/file_finder/src/file_finder_settings.rs +++ b/crates/file_finder/src/file_finder_settings.rs @@ -2,13 +2,11 @@ use anyhow::Result; use schemars::JsonSchema; use serde_derive::{Deserialize, Serialize}; use settings::{Settings, SettingsSources}; -use std::cmp; -use ui::Pixels; #[derive(Deserialize, Debug, Clone, Copy, PartialEq)] pub struct FileFinderSettings { pub file_icons: bool, - pub modal_width: FileFinderWidth, + pub modal_max_width: Option, } #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] @@ -17,10 +15,10 @@ pub struct FileFinderSettingsContent { /// /// Default: true pub file_icons: Option, - /// The width of the file finder modal. + /// Determines how much space the file finder can take up in relation to the available window width. /// - /// Default: "medium" - pub modal_width: Option, + /// Default: small + pub modal_max_width: Option, } impl Settings for FileFinderSettings { @@ -36,40 +34,10 @@ impl Settings for FileFinderSettings { #[derive(Debug, PartialEq, Eq, Clone, Copy, Default, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "lowercase")] pub enum FileFinderWidth { - Small, #[default] + Small, Medium, Large, XLarge, Full, } - -impl FileFinderWidth { - const MIN_MODAL_WIDTH_PX: f32 = 384.; - - pub fn padding_px(&self) -> Pixels { - let padding_val = match self { - FileFinderWidth::Small => 1280., - FileFinderWidth::Medium => 1024., - FileFinderWidth::Large => 768., - FileFinderWidth::XLarge => 512., - FileFinderWidth::Full => 0., - }; - - Pixels(padding_val) - } - - pub fn calc_width(&self, window_width: Pixels) -> Pixels { - if self == &FileFinderWidth::Full { - return window_width; - } - - let min_modal_width_px = Pixels(FileFinderWidth::MIN_MODAL_WIDTH_PX); - - let padding_px = self.padding_px(); - let width_val = window_width - padding_px; - let finder_width = cmp::max(min_modal_width_px, width_val); - - finder_width - } -} diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index e6e6b662c0..ce8068fa3b 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -1418,11 +1418,11 @@ Or to set a `socks5` proxy: ## File Finder -### Modal Width +### Modal Max Width -- Description: Width of the file finder modal. Can take one of a few values: `small`, `medium`, `large`, `xlarge`, and `full`. -- Setting: `modal_width` -- Default: `medium` +- Description: Max-width of the file finder modal. It can take one of these values: `small`, `medium`, `large`, `xlarge`, and `full`. +- Setting: `max_modal_width` +- Default: `small` ## Preferred Line Length From e2552b9adda0e4db77bd6fdecc5e365b3c413d4e Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 18 Nov 2024 16:37:28 -0500 Subject: [PATCH 012/886] collab: Bypass account age check for users with active LLM subscriptions (#20837) This PR makes it so users with an active LLM subscription can bypass the account age check. Release Notes: - N/A --- crates/collab/src/rpc.rs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 0e977074f7..397fcefacf 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -4030,12 +4030,15 @@ async fn get_llm_api_token( Err(anyhow!("terms of service not accepted"))? } - let mut account_created_at = user.created_at; - if let Some(github_created_at) = user.github_user_created_at { - account_created_at = account_created_at.min(github_created_at); - } - if Utc::now().naive_utc() - account_created_at < MIN_ACCOUNT_AGE_FOR_LLM_USE { - Err(anyhow!("account too young"))? + let has_llm_subscription = session.has_llm_subscription(&db).await?; + if !has_llm_subscription { + let mut account_created_at = user.created_at; + if let Some(github_created_at) = user.github_user_created_at { + account_created_at = account_created_at.min(github_created_at); + } + if Utc::now().naive_utc() - account_created_at < MIN_ACCOUNT_AGE_FOR_LLM_USE { + Err(anyhow!("account too young"))? + } } let billing_preferences = db.get_billing_preferences(user.id).await?; @@ -4045,7 +4048,7 @@ async fn get_llm_api_token( session.is_staff(), billing_preferences, has_llm_closed_beta_feature_flag, - session.has_llm_subscription(&db).await?, + has_llm_subscription, session.current_plan(&db).await?, &session.app_state.config, )?; From 5b317f60dff6090ff407e617a85bbbeaadfaad21 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Mon, 18 Nov 2024 21:39:57 +0000 Subject: [PATCH 013/886] Improve install-cmake script (#20836) - Don't output junk to stderr when cmake unavailable - Kitware PPA does not include up to date bins for all distros (e.g. Ubuntu 24 only has 3.30.2 although 3.30.4 has been out for a while) so don't try to force install a specific version. Take the best we can get. --- script/install-cmake | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/script/install-cmake b/script/install-cmake index 71b5aaeeef..3a28aae1b8 100755 --- a/script/install-cmake +++ b/script/install-cmake @@ -35,7 +35,7 @@ CMAKE_VERSION="${CMAKE_VERSION:-${1:-3.30.4}}" if [ "$(whoami)" = root ]; then SUDO=; else SUDO="$(command -v sudo || command -v doas || true)"; fi -if cmake --version | grep -q "$CMAKE_VERSION"; then +if cmake --version 2>/dev/null | grep -q "$CMAKE_VERSION"; then echo "CMake $CMAKE_VERSION is already installed." exit 0 elif [ -e /usr/local/bin/cmake ]; then @@ -51,7 +51,7 @@ elif [ -e /etc/lsb-release ] && grep -qP 'DISTRIB_ID=Ubuntu' /etc/lsb-release; t echo "deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ $(lsb_release -cs) main" \ | $SUDO tee /etc/apt/sources.list.d/kitware.list >/dev/null $SUDO apt-get update - $SUDO apt-get install -y kitware-archive-keyring cmake==$CMAKE_VERSION + $SUDO apt-get install -y kitware-archive-keyring cmake else arch="$(uname -m)" if [ "$arch" != "x86_64" ] && [ "$arch" != "aarch64" ]; then From 5b9916e34bbe3824135bff3b6984e9329952de3a Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Mon, 18 Nov 2024 21:41:22 +0000 Subject: [PATCH 014/886] ci: Add shellcheck for scripts (#20631) Fixes shellcheck errors in script/* Adds a couple trailing newlines. Adds `script/shellcheck-scripts` and associated CI machinery. Current set ultra-conservative, does not output warnings, only errors. --- .github/workflows/script_checks.yml | 21 +++++++++++++++++++++ script/analyze_highlights.py | 1 + script/bundle-linux | 8 +++++--- script/clear-target-dir-if-larger-than | 2 +- script/deploy-postgrest | 2 +- script/get-crate-version | 4 ++-- script/kube-shell | 4 ++-- script/metal-debug | 2 +- script/shellcheck-scripts | 12 ++++++++++++ script/upload-nightly | 4 ++-- 10 files changed, 48 insertions(+), 12 deletions(-) create mode 100644 .github/workflows/script_checks.yml create mode 100755 script/shellcheck-scripts diff --git a/.github/workflows/script_checks.yml b/.github/workflows/script_checks.yml new file mode 100644 index 0000000000..c32a433e46 --- /dev/null +++ b/.github/workflows/script_checks.yml @@ -0,0 +1,21 @@ +name: Script + +on: + pull_request: + paths: + - "script/**" + push: + branches: + - main + +jobs: + shellcheck: + name: "ShellCheck Scripts" + if: github.repository_owner == 'zed-industries' + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - name: Shellcheck ./scripts + run: | + ./script/shellcheck-scripts error diff --git a/script/analyze_highlights.py b/script/analyze_highlights.py index 1fd16f2c0f..09a6419653 100644 --- a/script/analyze_highlights.py +++ b/script/analyze_highlights.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python3 """ This script analyzes all the highlight.scm files in our embedded languages and extensions. It counts the number of unique instances of @{name} and the languages in which they are used. diff --git a/script/bundle-linux b/script/bundle-linux index 2aa1dcab4a..98b49ae4da 100755 --- a/script/bundle-linux +++ b/script/bundle-linux @@ -69,7 +69,9 @@ strip --strip-debug "${target_dir}/${remote_server_triple}/release/remote_server # Ensure that remote_server does not depend on libssl nor libcrypto, as we got rid of these deps. -! ldd "${target_dir}/${remote_server_triple}/release/remote_server" | grep -q 'libcrypto\|libssl' +if ldd "${target_dir}/${remote_server_triple}/release/remote_server" | grep -q 'libcrypto\|libssl'; then + echo "Error: remote_server still depends on libssl or libcrypto" && exit 1 +fi suffix="" if [ "$channel" != "stable" ]; then @@ -89,8 +91,8 @@ cp "${target_dir}/${target_triple}/release/cli" "${zed_dir}/bin/zed" # Libs find_libs() { ldd ${target_dir}/${target_triple}/release/zed |\ - cut -d' ' -f3 |\ - grep -v '\<\(libstdc++.so\|libc.so\|libgcc_s.so\|libm.so\|libpthread.so\|libdl.so\)' + cut -d' ' -f3 |\ + grep -v '\<\(libstdc++.so\|libc.so\|libgcc_s.so\|libm.so\|libpthread.so\|libdl.so\)' } mkdir -p "${zed_dir}/lib" diff --git a/script/clear-target-dir-if-larger-than b/script/clear-target-dir-if-larger-than index d23c111ec1..691ff42ffd 100755 --- a/script/clear-target-dir-if-larger-than +++ b/script/clear-target-dir-if-larger-than @@ -2,7 +2,7 @@ set -eu -if [[ $# < 1 ]]; then +if [[ $# -ne 1 ]]; then echo "usage: $0 " exit 1 fi diff --git a/script/deploy-postgrest b/script/deploy-postgrest index 14fbd50e30..2a0b21a991 100755 --- a/script/deploy-postgrest +++ b/script/deploy-postgrest @@ -3,7 +3,7 @@ set -eu source script/lib/deploy-helpers.sh -if [[ $# < 1 ]]; then +if [[ $# != 1 ]]; then echo "Usage: $0 (postgrest not needed on preview or nightly)" exit 1 fi diff --git a/script/get-crate-version b/script/get-crate-version index b6346b32ec..0a35e4d49d 100755 --- a/script/get-crate-version +++ b/script/get-crate-version @@ -2,7 +2,7 @@ set -eu -if [[ $# < 1 ]]; then +if [[ $# -ne 1 ]]; then echo "Usage: $0 " >&2 exit 1 fi @@ -14,4 +14,4 @@ cargo metadata \ --format-version=1 \ | jq \ --raw-output \ - ".packages[] | select(.name == \"${CRATE_NAME}\") | .version" \ No newline at end of file + ".packages[] | select(.name == \"${CRATE_NAME}\") | .version" diff --git a/script/kube-shell b/script/kube-shell index 9181dc959c..0ca77acdd0 100755 --- a/script/kube-shell +++ b/script/kube-shell @@ -1,6 +1,6 @@ #!/bin/bash -if [[ $# < 1 ]]; then +if [[ $# -ne 1 ]]; then echo "Usage: $0 [production|staging|...]" exit 1 fi @@ -8,4 +8,4 @@ fi export ZED_KUBE_NAMESPACE=$1 pod=$(kubectl --namespace=${ZED_KUBE_NAMESPACE} get pods --selector=app=zed --output=jsonpath='{.items[*].metadata.name}') -exec kubectl --namespace $ZED_KUBE_NAMESPACE exec --tty --stdin $pod -- /bin/bash \ No newline at end of file +exec kubectl --namespace $ZED_KUBE_NAMESPACE exec --tty --stdin $pod -- /bin/bash diff --git a/script/metal-debug b/script/metal-debug index 6fc18e5ebd..de8476f3e3 100755 --- a/script/metal-debug +++ b/script/metal-debug @@ -10,4 +10,4 @@ export GPUProfilerEnabled="YES" export METAL_DEBUG_ERROR_MODE=0 export LD_LIBRARY_PATH="/Applications/Xcode.app/Contents/Developer/../SharedFrameworks/" -cargo run $@ +cargo run "$@" diff --git a/script/shellcheck-scripts b/script/shellcheck-scripts new file mode 100755 index 0000000000..d42b31d02f --- /dev/null +++ b/script/shellcheck-scripts @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +set -euo pipefail + +mode=${1:-error} +[[ "$mode" =~ ^(error|warning)$ ]] || { echo "Usage: $0 [error|warning]"; exit 1; } + +cd "$(dirname "$0")/.." || exit 1 + +find script -maxdepth 1 -type f -print0 | + xargs -0 grep -l -E '^#!(/bin/|/usr/bin/env )(sh|bash|dash)' | + xargs -r shellcheck -x -S "$mode" -C diff --git a/script/upload-nightly b/script/upload-nightly index 61b73d4e56..fd37941981 100755 --- a/script/upload-nightly +++ b/script/upload-nightly @@ -19,12 +19,12 @@ if [[ -n "${1:-}" ]]; then target="$1" else echo "Error: Target '$1' is not allowed" - echo "Usage: $0 [${allowed_targets[@]}]" + echo "Usage: $0 [${allowed_targets[*]}]" exit 1 fi else echo "Error: Target is not specified" -echo "Usage: $0 [${allowed_targets[@]}]" +echo "Usage: $0 [${allowed_targets[*]}]" exit 1 fi echo "Uploading nightly for target: $target" From 889aac9c037dd39be8e912f288439b22ebb53603 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Mon, 18 Nov 2024 16:56:34 -0500 Subject: [PATCH 015/886] Snippet choices (#13958) Closes: #12739 Release Notes: Solves #12739 by - Enable snippet parsing to successfully parse snippets with choices - Show completion menu when tabbing to a snippet variable with multiple choices Todo: - [x] Parse snippet choices - [x] Open completion menu when tabbing to a snippet variable with several choices (Thank you Piotr) - [x] Get snippet choices to reappear when tabbing back to a previous tabstop in a snippet - [x] add snippet unit tests - [x] Add fuzzy search to snippet choice completion menu & update completion menu based on choices - [x] add completion menu unit tests Current State: Using these custom snippets ```json "my snippet": { "prefix": "log", "body": ["type ${1|i32, u32|} = $2"], "description": "Expand `log` to `console.log()`" }, "my snippet2": { "prefix": "snip", "body": [ "type ${1|i,i8,i16,i64,i32|} ${2|test,test_again,test_final|} = $3" ], "description": "snippet choice tester" } ``` Using snippet choices: https://github.com/user-attachments/assets/d29fb1a2-7632-4071-944f-daeaa243e3ac --------- Co-authored-by: Piotr Osiewicz Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> --- crates/editor/src/debounced_delay.rs | 1 + crates/editor/src/editor.rs | 201 ++++++++++++++++++++++----- crates/editor/src/editor_tests.rs | 39 ++++++ crates/snippet/src/snippet.rs | 119 +++++++++++++++- 4 files changed, 321 insertions(+), 39 deletions(-) diff --git a/crates/editor/src/debounced_delay.rs b/crates/editor/src/debounced_delay.rs index 0dbf36d49e..ad4b55b209 100644 --- a/crates/editor/src/debounced_delay.rs +++ b/crates/editor/src/debounced_delay.rs @@ -5,6 +5,7 @@ use gpui::{Task, ViewContext}; use crate::Editor; +#[derive(Debug)] pub struct DebouncedDelay { task: Option>, cancel_channel: Option>, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 9d8044f075..11d47daa6b 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -883,6 +883,7 @@ struct AutocloseRegion { struct SnippetState { ranges: Vec>>, active_index: usize, + choices: Vec>>, } #[doc(hidden)] @@ -1000,7 +1001,7 @@ enum ContextMenuOrigin { GutterIndicator(DisplayRow), } -#[derive(Clone)] +#[derive(Clone, Debug)] struct CompletionsMenu { id: CompletionId, sort_completions: bool, @@ -1011,10 +1012,100 @@ struct CompletionsMenu { matches: Arc<[StringMatch]>, selected_item: usize, scroll_handle: UniformListScrollHandle, - selected_completion_documentation_resolve_debounce: Arc>, + selected_completion_documentation_resolve_debounce: Option>>, } impl CompletionsMenu { + fn new( + id: CompletionId, + sort_completions: bool, + initial_position: Anchor, + buffer: Model, + completions: Box<[Completion]>, + ) -> Self { + let match_candidates = completions + .iter() + .enumerate() + .map(|(id, completion)| StringMatchCandidate::new(id, completion.label.text.clone())) + .collect(); + + Self { + id, + sort_completions, + initial_position, + buffer, + completions: Arc::new(RwLock::new(completions)), + match_candidates, + matches: Vec::new().into(), + selected_item: 0, + scroll_handle: UniformListScrollHandle::new(), + selected_completion_documentation_resolve_debounce: Some(Arc::new(Mutex::new( + DebouncedDelay::new(), + ))), + } + } + + fn new_snippet_choices( + id: CompletionId, + sort_completions: bool, + choices: &Vec, + selection: Range, + buffer: Model, + ) -> Self { + let completions = choices + .iter() + .map(|choice| Completion { + old_range: selection.start.text_anchor..selection.end.text_anchor, + new_text: choice.to_string(), + label: CodeLabel { + text: choice.to_string(), + runs: Default::default(), + filter_range: Default::default(), + }, + server_id: LanguageServerId(usize::MAX), + documentation: None, + lsp_completion: Default::default(), + confirm: None, + }) + .collect(); + + let match_candidates = choices + .iter() + .enumerate() + .map(|(id, completion)| StringMatchCandidate::new(id, completion.to_string())) + .collect(); + let matches = choices + .iter() + .enumerate() + .map(|(id, completion)| StringMatch { + candidate_id: id, + score: 1., + positions: vec![], + string: completion.clone(), + }) + .collect(); + Self { + id, + sort_completions, + initial_position: selection.start, + buffer, + completions: Arc::new(RwLock::new(completions)), + match_candidates, + matches, + selected_item: 0, + scroll_handle: UniformListScrollHandle::new(), + selected_completion_documentation_resolve_debounce: Some(Arc::new(Mutex::new( + DebouncedDelay::new(), + ))), + } + } + + fn suppress_documentation_resolution(mut self) -> Self { + self.selected_completion_documentation_resolve_debounce + .take(); + self + } + fn select_first( &mut self, provider: Option<&dyn CompletionProvider>, @@ -1115,6 +1206,12 @@ impl CompletionsMenu { let Some(provider) = provider else { return; }; + let Some(documentation_resolve) = self + .selected_completion_documentation_resolve_debounce + .as_ref() + else { + return; + }; let resolve_task = provider.resolve_completions( self.buffer.clone(), @@ -1127,15 +1224,13 @@ impl CompletionsMenu { EditorSettings::get_global(cx).completion_documentation_secondary_query_debounce; let delay = Duration::from_millis(delay_ms); - self.selected_completion_documentation_resolve_debounce - .lock() - .fire_new(delay, cx, |_, cx| { - cx.spawn(move |this, mut cx| async move { - if let Some(true) = resolve_task.await.log_err() { - this.update(&mut cx, |_, cx| cx.notify()).ok(); - } - }) - }); + documentation_resolve.lock().fire_new(delay, cx, |_, cx| { + cx.spawn(move |this, mut cx| async move { + if let Some(true) = resolve_task.await.log_err() { + this.update(&mut cx, |_, cx| cx.notify()).ok(); + } + }) + }); } fn visible(&self) -> bool { @@ -1418,6 +1513,7 @@ impl CompletionsMenu { } } +#[derive(Clone)] struct AvailableCodeAction { excerpt_id: ExcerptId, action: CodeAction, @@ -4386,6 +4482,10 @@ impl Editor { return; }; + if !self.snippet_stack.is_empty() && self.context_menu.read().as_ref().is_some() { + return; + } + let position = self.selections.newest_anchor().head(); let (buffer, buffer_position) = if let Some(output) = self.buffer.read(cx).text_anchor_for_position(position, cx) { @@ -4431,30 +4531,13 @@ impl Editor { })?; let completions = completions.await.log_err(); let menu = if let Some(completions) = completions { - let mut menu = CompletionsMenu { + let mut menu = CompletionsMenu::new( id, sort_completions, - initial_position: position, - match_candidates: completions - .iter() - .enumerate() - .map(|(id, completion)| { - StringMatchCandidate::new( - id, - completion.label.text[completion.label.filter_range.clone()] - .into(), - ) - }) - .collect(), - buffer: buffer.clone(), - completions: Arc::new(RwLock::new(completions.into())), - matches: Vec::new().into(), - selected_item: 0, - scroll_handle: UniformListScrollHandle::new(), - selected_completion_documentation_resolve_debounce: Arc::new(Mutex::new( - DebouncedDelay::new(), - )), - }; + position, + buffer.clone(), + completions.into(), + ); menu.filter(query.as_deref(), cx.background_executor().clone()) .await; @@ -4657,7 +4740,11 @@ impl Editor { self.transact(cx, |this, cx| { if let Some(mut snippet) = snippet { snippet.text = text.to_string(); - for tabstop in snippet.tabstops.iter_mut().flatten() { + for tabstop in snippet + .tabstops + .iter_mut() + .flat_map(|tabstop| tabstop.ranges.iter_mut()) + { tabstop.start -= common_prefix_len as isize; tabstop.end -= common_prefix_len as isize; } @@ -5693,6 +5780,27 @@ impl Editor { context_menu } + fn show_snippet_choices( + &mut self, + choices: &Vec, + selection: Range, + cx: &mut ViewContext, + ) { + if selection.start.buffer_id.is_none() { + return; + } + let buffer_id = selection.start.buffer_id.unwrap(); + let buffer = self.buffer().read(cx).buffer(buffer_id); + let id = post_inc(&mut self.next_completion_id); + + if let Some(buffer) = buffer { + *self.context_menu.write() = Some(ContextMenu::Completions( + CompletionsMenu::new_snippet_choices(id, true, choices, selection, buffer) + .suppress_documentation_resolution(), + )); + } + } + pub fn insert_snippet( &mut self, insertion_ranges: &[Range], @@ -5702,6 +5810,7 @@ impl Editor { struct Tabstop { is_end_tabstop: bool, ranges: Vec>, + choices: Option>, } let tabstops = self.buffer.update(cx, |buffer, cx| { @@ -5721,10 +5830,11 @@ impl Editor { .tabstops .iter() .map(|tabstop| { - let is_end_tabstop = tabstop.first().map_or(false, |tabstop| { + let is_end_tabstop = tabstop.ranges.first().map_or(false, |tabstop| { tabstop.is_empty() && tabstop.start == snippet.text.len() as isize }); let mut tabstop_ranges = tabstop + .ranges .iter() .flat_map(|tabstop_range| { let mut delta = 0_isize; @@ -5746,6 +5856,7 @@ impl Editor { Tabstop { is_end_tabstop, ranges: tabstop_ranges, + choices: tabstop.choices.clone(), } }) .collect::>() @@ -5755,16 +5866,29 @@ impl Editor { s.select_ranges(tabstop.ranges.iter().cloned()); }); + if let Some(choices) = &tabstop.choices { + if let Some(selection) = tabstop.ranges.first() { + self.show_snippet_choices(choices, selection.clone(), cx) + } + } + // If we're already at the last tabstop and it's at the end of the snippet, // we're done, we don't need to keep the state around. if !tabstop.is_end_tabstop { + let choices = tabstops + .iter() + .map(|tabstop| tabstop.choices.clone()) + .collect(); + let ranges = tabstops .into_iter() .map(|tabstop| tabstop.ranges) .collect::>(); + self.snippet_stack.push(SnippetState { active_index: 0, ranges, + choices, }); } @@ -5839,6 +5963,13 @@ impl Editor { self.change_selections(Some(Autoscroll::fit()), cx, |s| { s.select_anchor_ranges(current_ranges.iter().cloned()) }); + + if let Some(choices) = &snippet.choices[snippet.active_index] { + if let Some(selection) = current_ranges.first() { + self.show_snippet_choices(&choices, selection.clone(), cx); + } + } + // If snippet state is not at the last tabstop, push it back on the stack if snippet.active_index + 1 < snippet.ranges.len() { self.snippet_stack.push(snippet); diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 4469b2e614..2e4edf98bc 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -6551,6 +6551,45 @@ async fn test_auto_replace_emoji_shortcode(cx: &mut gpui::TestAppContext) { }); } +#[gpui::test] +async fn test_snippet_placeholder_choices(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let (text, insertion_ranges) = marked_text_ranges( + indoc! {" + ˇ + "}, + false, + ); + + let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx)); + let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); + + _ = editor.update(cx, |editor, cx| { + let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap(); + + editor + .insert_snippet(&insertion_ranges, snippet, cx) + .unwrap(); + + fn assert(editor: &mut Editor, cx: &mut ViewContext, marked_text: &str) { + let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false); + assert_eq!(editor.text(cx), expected_text); + assert_eq!(editor.selections.ranges::(cx), selection_ranges); + } + + assert( + editor, + cx, + indoc! {" + type «» =• + "}, + ); + + assert!(editor.context_menu_visible(), "There should be a matches"); + }); +} + #[gpui::test] async fn test_snippets(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); diff --git a/crates/snippet/src/snippet.rs b/crates/snippet/src/snippet.rs index 41529939a1..3eeaff285e 100644 --- a/crates/snippet/src/snippet.rs +++ b/crates/snippet/src/snippet.rs @@ -8,7 +8,11 @@ pub struct Snippet { pub tabstops: Vec, } -type TabStop = SmallVec<[Range; 2]>; +#[derive(Clone, Debug, Default, PartialEq)] +pub struct TabStop { + pub ranges: SmallVec<[Range; 2]>, + pub choices: Option>, +} impl Snippet { pub fn parse(source: &str) -> Result { @@ -24,7 +28,11 @@ impl Snippet { if let Some(final_tabstop) = final_tabstop { tabstops.push(final_tabstop); } else { - let end_tabstop = [len..len].into_iter().collect(); + let end_tabstop = TabStop { + ranges: [len..len].into_iter().collect(), + choices: None, + }; + if !tabstops.last().map_or(false, |t| *t == end_tabstop) { tabstops.push(end_tabstop); } @@ -88,11 +96,17 @@ fn parse_tabstop<'a>( ) -> Result<&'a str> { let tabstop_start = text.len(); let tabstop_index; + let mut choices = None; + if source.starts_with('{') { let (index, rest) = parse_int(&source[1..])?; tabstop_index = index; source = rest; + if source.starts_with("|") { + (source, choices) = parse_choices(&source[1..], text)?; + } + if source.starts_with(':') { source = parse_snippet(&source[1..], true, text, tabstops)?; } @@ -110,7 +124,11 @@ fn parse_tabstop<'a>( tabstops .entry(tabstop_index) - .or_default() + .or_insert_with(|| TabStop { + ranges: Default::default(), + choices, + }) + .ranges .push(tabstop_start as isize..text.len() as isize); Ok(source) } @@ -126,6 +144,61 @@ fn parse_int(source: &str) -> Result<(usize, &str)> { Ok((prefix.parse()?, suffix)) } +fn parse_choices<'a>( + mut source: &'a str, + text: &mut String, +) -> Result<(&'a str, Option>)> { + let mut found_default_choice = false; + let mut current_choice = String::new(); + let mut choices = Vec::new(); + + loop { + match source.chars().next() { + None => return Ok(("", Some(choices))), + Some('\\') => { + source = &source[1..]; + + if let Some(c) = source.chars().next() { + if !found_default_choice { + current_choice.push(c); + text.push(c); + } + source = &source[c.len_utf8()..]; + } + } + Some(',') => { + found_default_choice = true; + source = &source[1..]; + choices.push(current_choice); + current_choice = String::new(); + } + Some('|') => { + source = &source[1..]; + choices.push(current_choice); + return Ok((source, Some(choices))); + } + Some(_) => { + let chunk_end = source.find([',', '|', '\\']); + + if chunk_end.is_none() { + return Err(anyhow!( + "Placeholder choice doesn't contain closing pipe-character '|'" + )); + } + + let (chunk, rest) = source.split_at(chunk_end.unwrap()); + + if !found_default_choice { + text.push_str(chunk); + } + + current_choice.push_str(chunk); + source = rest; + } + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -142,11 +215,13 @@ mod tests { let snippet = Snippet::parse("one$1two").unwrap(); assert_eq!(snippet.text, "onetwo"); assert_eq!(tabstops(&snippet), &[vec![3..3], vec![6..6]]); + assert_eq!(tabstop_choices(&snippet), &[&None, &None]); // Multi-digit numbers let snippet = Snippet::parse("one$123-$99-two").unwrap(); assert_eq!(snippet.text, "one--two"); assert_eq!(tabstops(&snippet), &[vec![4..4], vec![3..3], vec![8..8]]); + assert_eq!(tabstop_choices(&snippet), &[&None, &None, &None]); } #[test] @@ -157,6 +232,7 @@ mod tests { // an additional tabstop at the end. assert_eq!(snippet.text, r#"foo."#); assert_eq!(tabstops(&snippet), &[vec![4..4]]); + assert_eq!(tabstop_choices(&snippet), &[&None]); } #[test] @@ -167,6 +243,7 @@ mod tests { // don't insert an additional tabstop at the end. assert_eq!(snippet.text, r#"
"#); assert_eq!(tabstops(&snippet), &[vec![12..12], vec![14..14]]); + assert_eq!(tabstop_choices(&snippet), &[&None, &None]); } #[test] @@ -177,6 +254,30 @@ mod tests { tabstops(&snippet), &[vec![3..6], vec![11..15], vec![15..15]] ); + assert_eq!(tabstop_choices(&snippet), &[&None, &None, &None]); + } + + #[test] + fn test_snippet_with_choice_placeholders() { + let snippet = Snippet::parse("type ${1|i32, u32|} = $2") + .expect("Should be able to unpack choice placeholders"); + + assert_eq!(snippet.text, "type i32 = "); + assert_eq!(tabstops(&snippet), &[vec![5..8], vec![11..11],]); + assert_eq!( + tabstop_choices(&snippet), + &[&Some(vec!["i32".to_string(), " u32".to_string()]), &None] + ); + + let snippet = Snippet::parse(r"${1|\$\{1\|one\,two\,tree\|\}|}") + .expect("Should be able to parse choice with escape characters"); + + assert_eq!(snippet.text, "${1|one,two,tree|}"); + assert_eq!(tabstops(&snippet), &[vec![0..18], vec![18..18]]); + assert_eq!( + tabstop_choices(&snippet), + &[&Some(vec!["${1|one,two,tree|}".to_string(),]), &None] + ); } #[test] @@ -196,6 +297,10 @@ mod tests { vec![40..40], ] ); + assert_eq!( + tabstop_choices(&snippet), + &[&None, &None, &None, &None, &None] + ); } #[test] @@ -203,10 +308,12 @@ mod tests { let snippet = Snippet::parse("\"\\$schema\": $1").unwrap(); assert_eq!(snippet.text, "\"$schema\": "); assert_eq!(tabstops(&snippet), &[vec![11..11]]); + assert_eq!(tabstop_choices(&snippet), &[&None]); let snippet = Snippet::parse("{a\\}").unwrap(); assert_eq!(snippet.text, "{a}"); assert_eq!(tabstops(&snippet), &[vec![3..3]]); + assert_eq!(tabstop_choices(&snippet), &[&None]); // backslash not functioning as an escape let snippet = Snippet::parse("a\\b").unwrap(); @@ -221,6 +328,10 @@ mod tests { } fn tabstops(snippet: &Snippet) -> Vec>> { - snippet.tabstops.iter().map(|t| t.to_vec()).collect() + snippet.tabstops.iter().map(|t| t.ranges.to_vec()).collect() + } + + fn tabstop_choices(snippet: &Snippet) -> Vec<&Option>> { + snippet.tabstops.iter().map(|t| &t.choices).collect() } } From 8666ec95bae9b4978906c18d8f07febab8ffcbe0 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Mon, 18 Nov 2024 22:17:24 +0000 Subject: [PATCH 016/886] ssh: Fix SSH to mac remotes (#20838) Restore ability to SSH to macOS arm remotes (`uname -m` on mac == `arm64`). Fix regression introduced in https://github.com/zed-industries/zed/pull/20618 --- crates/remote/src/ssh_session.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index 20795be201..1ea76a24c8 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -1568,6 +1568,7 @@ impl SshRemoteConnection { // exclude armv5,6,7 as they are 32-bit. let arch = if arch.starts_with("armv8") || arch.starts_with("armv9") + || arch.starts_with("arm64") || arch.starts_with("aarch64") { "aarch64" From b4c2f29c8bf6fa399a7788b01199ee518ebc8caf Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Mon, 18 Nov 2024 14:30:38 -0800 Subject: [PATCH 017/886] Remove use of current `File` for new buffers that never have `File` (#20832) `create_buffer` calls `Buffer::local` which sets `file` to `None` [here](https://github.com/zed-industries/zed/blob/f12981db32f9b936cd29e39ccc7f8a0b4e54cee1/crates/language/src/buffer.rs#L629). So there's no point in then immediately attempting to update maps that rely on `file` being present. Release Notes: - N/A --- crates/project/src/buffer_store.rs | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index 634d87dca2..eb56680fb3 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -902,30 +902,12 @@ impl BufferStoreImpl for Model { } fn create_buffer(&self, cx: &mut ModelContext) -> Task>> { - let handle = self.clone(); cx.spawn(|buffer_store, mut cx| async move { let buffer = cx.new_model(|cx| { Buffer::local("", cx).with_language(language::PLAIN_TEXT.clone(), cx) })?; buffer_store.update(&mut cx, |buffer_store, cx| { buffer_store.add_buffer(buffer.clone(), cx).log_err(); - let buffer_id = buffer.read(cx).remote_id(); - handle.update(cx, |this, cx| { - if let Some(file) = File::from_dyn(buffer.read(cx).file()) { - this.local_buffer_ids_by_path.insert( - ProjectPath { - worktree_id: file.worktree_id(cx), - path: file.path.clone(), - }, - buffer_id, - ); - - if let Some(entry_id) = file.entry_id { - this.local_buffer_ids_by_entry_id - .insert(entry_id, buffer_id); - } - } - }); })?; Ok(buffer) }) From fb6c987e3e28ed006c32e66b59e791127308d7a4 Mon Sep 17 00:00:00 2001 From: Carroll Wainwright Date: Mon, 18 Nov 2024 15:05:39 -0800 Subject: [PATCH 018/886] python: Improve function syntax highlighting (#20487) Release Notes: - Differentiate between function and method calls and definitions. `function.definition` matches the highlight for e.g. rust, `function.call` is new. - Likewise differentiate between class calls and class definitions. - Better highlighting of function decorators (the `@` symbol is punctuation, and now the decorator itself has a `function.decorator` tag) - Make `cls` a special variable (like `self`) - Add `ellipsis` as a built-in constant Note that most themes do not currently make use of the `function.definition` tags, and none make use of the `type.class.definition` tag. Hopefully more themes will pick this up. *Before:* image *After:* image --- crates/languages/src/python/highlights.scm | 34 +++++++++++++++------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/crates/languages/src/python/highlights.scm b/crates/languages/src/python/highlights.scm index e5f1b4d423..78e5126d40 100644 --- a/crates/languages/src/python/highlights.scm +++ b/crates/languages/src/python/highlights.scm @@ -5,6 +5,14 @@ ; Type alias (type_alias_statement "type" @keyword) +; Identifier naming conventions + +((identifier) @type.class + (#match? @type.class "^[A-Z]")) + +((identifier) @constant + (#match? @constant "^_*[A-Z][A-Z\\d_]*$")) + ; TypeVar with constraints in type parameters (type (tuple (identifier) @type) @@ -12,25 +20,28 @@ ; Function calls -(decorator) @function +(decorator + "@" @punctuation.special + (identifier) @function.decorator) (call - function: (attribute attribute: (identifier) @function.method)) + function: (attribute attribute: (identifier) @function.method.call)) (call - function: (identifier) @function) + function: (identifier) @function.call) -; Function definitions +; Function and class definitions (function_definition - name: (identifier) @function) + name: (identifier) @function.definition) -; Identifier naming conventions +; Class definitions and calling: needs to come after the regex matching above -((identifier) @type - (#match? @type "^[A-Z]")) +(class_definition + name: (identifier) @type.class.definition) -((identifier) @constant - (#match? @constant "^_*[A-Z][A-Z\\d_]*$")) +(call + function: (identifier) @type.class.call + (#match? @type.class.call "^[A-Z][A-Z0-9_]*[a-z]")) ; Builtin functions @@ -46,6 +57,7 @@ (none) (true) (false) + (ellipsis) ] @constant.builtin [ @@ -58,7 +70,7 @@ [ (parameters (identifier) @variable.special) (attribute (identifier) @variable.special) - (#match? @variable.special "^self$") + (#match? @variable.special "^self|cls$") ] (comment) @comment From 80d50f56f3456ab184f680ed7afb9e134493eda2 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 18 Nov 2024 18:20:32 -0500 Subject: [PATCH 019/886] collab: Add feature flag to bypass account age check (#20843) This PR adds a `bypass-account-age-check` feature flag that can be used to bypass the minimum account age check. Release Notes: - N/A --- crates/collab/src/rpc.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 397fcefacf..1184c48618 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -4031,7 +4031,10 @@ async fn get_llm_api_token( } let has_llm_subscription = session.has_llm_subscription(&db).await?; - if !has_llm_subscription { + + let bypass_account_age_check = + has_llm_subscription || flags.iter().any(|flag| flag == "bypass-account-age-check"); + if !bypass_account_age_check { let mut account_created_at = user.created_at; if let Some(github_created_at) = user.github_user_created_at { account_created_at = account_created_at.min(github_created_at); From f0c7e62adc5f69866eb6434b78d552780adf47d2 Mon Sep 17 00:00:00 2001 From: lord Date: Mon, 18 Nov 2024 18:32:43 -0500 Subject: [PATCH 020/886] Leave goal_x unchanged when moving by rows past the start or end of the document (#20705) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Perhaps this was intentional behavior, but if not, I've attempted to write this hacky fix — I noticed using the vertical arrow keys to move past the document start/end would reset the goal_x to either zero (for moving upwards) or the line width (for moving downwards). This change makes Zed match most native text fields (at least on macOS) which leave goal_x unchanged, even when hitting the end of the document. I tested this change manually. Would be happy to add automatic tests for it too, but couldn't find any existing cursor movement tests. Release Notes: - Behavior when moving vertically past the start or end of a document now matches native text fields; it no longer resets the selection goal --- crates/editor/src/editor_tests.rs | 28 ++++++++++++++++++++++++++++ crates/editor/src/movement.rs | 18 ++++++++---------- 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 2e4edf98bc..01507c4e31 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -1398,6 +1398,15 @@ fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) { view.change_selections(None, cx, |s| { s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]); }); + + // moving above start of document should move selection to start of document, + // but the next move down should still be at the original goal_x + view.move_up(&MoveUp, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(0, "".len())] + ); + view.move_down(&MoveDown, cx); assert_eq!( view.selections.display_ranges(cx), @@ -1422,6 +1431,25 @@ fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) { &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())] ); + // moving past end of document should not change goal_x + view.move_down(&MoveDown, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(5, "".len())] + ); + + view.move_down(&MoveDown, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(5, "".len())] + ); + + view.move_up(&MoveUp, cx); + assert_eq!( + view.selections.display_ranges(cx), + &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())] + ); + view.move_up(&MoveUp, cx); assert_eq!( view.selections.display_ranges(cx), diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 19ba147e16..52bedde2e3 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -3,7 +3,7 @@ use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint}; use crate::{scroll::ScrollAnchor, CharKind, DisplayRow, EditorStyle, RowExt, ToOffset, ToPoint}; -use gpui::{px, Pixels, WindowTextSystem}; +use gpui::{Pixels, WindowTextSystem}; use language::Point; use multi_buffer::{MultiBufferRow, MultiBufferSnapshot}; use serde::Deserialize; @@ -120,7 +120,7 @@ pub(crate) fn up_by_rows( preserve_column_at_start: bool, text_layout_details: &TextLayoutDetails, ) -> (DisplayPoint, SelectionGoal) { - let mut goal_x = match goal { + let goal_x = match goal { SelectionGoal::HorizontalPosition(x) => x.into(), SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(), SelectionGoal::HorizontalRange { end, .. } => end.into(), @@ -138,7 +138,6 @@ pub(crate) fn up_by_rows( return (start, goal); } else { point = DisplayPoint::new(DisplayRow(0), 0); - goal_x = px(0.); } let mut clipped_point = map.clip_point(point, Bias::Left); @@ -159,7 +158,7 @@ pub(crate) fn down_by_rows( preserve_column_at_end: bool, text_layout_details: &TextLayoutDetails, ) -> (DisplayPoint, SelectionGoal) { - let mut goal_x = match goal { + let goal_x = match goal { SelectionGoal::HorizontalPosition(x) => x.into(), SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(), SelectionGoal::HorizontalRange { end, .. } => end.into(), @@ -174,7 +173,6 @@ pub(crate) fn down_by_rows( return (start, goal); } else { point = map.max_point(); - goal_x = map.x_for_display_point(point, text_layout_details) } let mut clipped_point = map.clip_point(point, Bias::Right); @@ -610,7 +608,7 @@ mod tests { test::{editor_test_context::EditorTestContext, marked_display_snapshot}, Buffer, DisplayMap, DisplayRow, ExcerptRange, FoldPlaceholder, InlayId, MultiBuffer, }; - use gpui::{font, Context as _}; + use gpui::{font, px, Context as _}; use language::Capability; use project::Project; use settings::SettingsStore; @@ -977,7 +975,7 @@ mod tests { ), ( DisplayPoint::new(DisplayRow(2), 0), - SelectionGoal::HorizontalPosition(0.0) + SelectionGoal::HorizontalPosition(col_2_x.0), ), ); assert_eq!( @@ -990,7 +988,7 @@ mod tests { ), ( DisplayPoint::new(DisplayRow(2), 0), - SelectionGoal::HorizontalPosition(0.0) + SelectionGoal::HorizontalPosition(0.0), ), ); @@ -1059,7 +1057,7 @@ mod tests { let max_point_x = snapshot .x_for_display_point(DisplayPoint::new(DisplayRow(7), 2), &text_layout_details); - // Can't move down off the end + // Can't move down off the end, and attempting to do so leaves the selection goal unchanged assert_eq!( down( &snapshot, @@ -1070,7 +1068,7 @@ mod tests { ), ( DisplayPoint::new(DisplayRow(7), 2), - SelectionGoal::HorizontalPosition(max_point_x.0) + SelectionGoal::HorizontalPosition(0.0) ), ); assert_eq!( From d4c5c0f05e69eab17e412bb3480546a145a8f7d7 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 18 Nov 2024 16:47:25 -0700 Subject: [PATCH 021/886] Don't render invisibles with elements (#20841) Turns out that in the case you have a somehow valid utf-8 file that contains almost all ascii control characters, we run out of element arena space. Fixes: #20652 Release Notes: - Fixed a crash when opening a file containing a very large number of ascii control characters on one line. --- crates/editor/src/display_map.rs | 51 +++++---- crates/editor/src/element.rs | 116 +++++++++++++-------- crates/gpui/src/text_system/line.rs | 15 +++ crates/gpui/src/text_system/line_layout.rs | 2 +- 4 files changed, 119 insertions(+), 65 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index f2e986b91b..b95c9312c5 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -66,7 +66,7 @@ use std::{ use sum_tree::{Bias, TreeMap}; use tab_map::{TabMap, TabSnapshot}; use text::LineIndent; -use ui::{div, px, IntoElement, ParentElement, SharedString, Styled, WindowContext}; +use ui::{px, SharedString, WindowContext}; use unicode_segmentation::UnicodeSegmentation; use wrap_map::{WrapMap, WrapSnapshot}; @@ -541,11 +541,17 @@ pub struct HighlightStyles { pub suggestion: Option, } +#[derive(Clone)] +pub enum ChunkReplacement { + Renderer(ChunkRenderer), + Str(SharedString), +} + pub struct HighlightedChunk<'a> { pub text: &'a str, pub style: Option, pub is_tab: bool, - pub renderer: Option, + pub replacement: Option, } impl<'a> HighlightedChunk<'a> { @@ -557,7 +563,7 @@ impl<'a> HighlightedChunk<'a> { let mut text = self.text; let style = self.style; let is_tab = self.is_tab; - let renderer = self.renderer; + let renderer = self.replacement; iter::from_fn(move || { let mut prefix_len = 0; while let Some(&ch) = chars.peek() { @@ -573,30 +579,33 @@ impl<'a> HighlightedChunk<'a> { text: prefix, style, is_tab, - renderer: renderer.clone(), + replacement: renderer.clone(), }); } chars.next(); let (prefix, suffix) = text.split_at(ch.len_utf8()); text = suffix; if let Some(replacement) = replacement(ch) { - let background = editor_style.status.hint_background; - let underline = editor_style.status.hint; + let invisible_highlight = HighlightStyle { + background_color: Some(editor_style.status.hint_background), + underline: Some(UnderlineStyle { + color: Some(editor_style.status.hint), + thickness: px(1.), + wavy: false, + }), + ..Default::default() + }; + let invisible_style = if let Some(mut style) = style { + style.highlight(invisible_highlight); + style + } else { + invisible_highlight + }; return Some(HighlightedChunk { text: prefix, - style: None, + style: Some(invisible_style), is_tab: false, - renderer: Some(ChunkRenderer { - render: Arc::new(move |_| { - div() - .child(replacement) - .bg(background) - .text_decoration_1() - .text_decoration_color(underline) - .into_any_element() - }), - constrain_width: false, - }), + replacement: Some(ChunkReplacement::Str(replacement.into())), }); } else { let invisible_highlight = HighlightStyle { @@ -619,7 +628,7 @@ impl<'a> HighlightedChunk<'a> { text: prefix, style: Some(invisible_style), is_tab: false, - renderer: renderer.clone(), + replacement: renderer.clone(), }); } } @@ -631,7 +640,7 @@ impl<'a> HighlightedChunk<'a> { text: remainder, style, is_tab, - renderer: renderer.clone(), + replacement: renderer.clone(), }) } else { None @@ -895,7 +904,7 @@ impl DisplaySnapshot { text: chunk.text, style: highlight_style, is_tab: chunk.is_tab, - renderer: chunk.renderer, + replacement: chunk.renderer.map(ChunkReplacement::Renderer), } .highlight_invisibles(editor_style) }) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 56322ff9f5..7702134409 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -16,8 +16,8 @@ use crate::{ items::BufferSearchHighlights, mouse_context_menu::{self, MenuPosition, MouseContextMenu}, scroll::scroll_amount::ScrollAmount, - BlockId, CodeActionsMenu, CursorShape, CustomBlockId, DisplayPoint, DisplayRow, - DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode, EditorSettings, + BlockId, ChunkReplacement, CodeActionsMenu, CursorShape, CustomBlockId, DisplayPoint, + DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor, HoveredHunk, JumpData, LineDown, LineUp, OpenExcerpts, PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase, Selection, SoftWrap, ToPoint, @@ -34,8 +34,8 @@ use gpui::{ FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, Length, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size, - StatefulInteractiveElement, Style, Styled, TextRun, TextStyle, TextStyleRefinement, View, - ViewContext, WeakView, WindowContext, + StatefulInteractiveElement, Style, Styled, TextRun, TextStyleRefinement, View, ViewContext, + WeakView, WindowContext, }; use gpui::{ClickEvent, Subscription}; use itertools::Itertools; @@ -2019,7 +2019,7 @@ impl EditorElement { let chunks = snapshot.highlighted_chunks(rows.clone(), true, style); LineWithInvisibles::from_chunks( chunks, - &style.text, + &style, MAX_LINE_LEN, rows.len(), snapshot.mode, @@ -4372,7 +4372,7 @@ impl LineWithInvisibles { #[allow(clippy::too_many_arguments)] fn from_chunks<'a>( chunks: impl Iterator>, - text_style: &TextStyle, + editor_style: &EditorStyle, max_line_len: usize, max_line_count: usize, editor_mode: EditorMode, @@ -4380,6 +4380,7 @@ impl LineWithInvisibles { is_row_soft_wrapped: impl Copy + Fn(usize) -> bool, cx: &mut WindowContext, ) -> Vec { + let text_style = &editor_style.text; let mut layouts = Vec::with_capacity(max_line_count); let mut fragments: SmallVec<[LineFragment; 1]> = SmallVec::new(); let mut line = String::new(); @@ -4398,9 +4399,9 @@ impl LineWithInvisibles { text: "\n", style: None, is_tab: false, - renderer: None, + replacement: None, }]) { - if let Some(renderer) = highlighted_chunk.renderer { + if let Some(replacement) = highlighted_chunk.replacement { if !line.is_empty() { let shaped_line = cx .text_system() @@ -4413,42 +4414,71 @@ impl LineWithInvisibles { styles.clear(); } - let available_width = if renderer.constrain_width { - let chunk = if highlighted_chunk.text == ellipsis.as_ref() { - ellipsis.clone() - } else { - SharedString::from(Arc::from(highlighted_chunk.text)) - }; - let shaped_line = cx - .text_system() - .shape_line( - chunk, - font_size, - &[text_style.to_run(highlighted_chunk.text.len())], - ) - .unwrap(); - AvailableSpace::Definite(shaped_line.width) - } else { - AvailableSpace::MinContent - }; + match replacement { + ChunkReplacement::Renderer(renderer) => { + let available_width = if renderer.constrain_width { + let chunk = if highlighted_chunk.text == ellipsis.as_ref() { + ellipsis.clone() + } else { + SharedString::from(Arc::from(highlighted_chunk.text)) + }; + let shaped_line = cx + .text_system() + .shape_line( + chunk, + font_size, + &[text_style.to_run(highlighted_chunk.text.len())], + ) + .unwrap(); + AvailableSpace::Definite(shaped_line.width) + } else { + AvailableSpace::MinContent + }; - let mut element = (renderer.render)(&mut ChunkRendererContext { - context: cx, - max_width: text_width, - }); - let line_height = text_style.line_height_in_pixels(cx.rem_size()); - let size = element.layout_as_root( - size(available_width, AvailableSpace::Definite(line_height)), - cx, - ); + let mut element = (renderer.render)(&mut ChunkRendererContext { + context: cx, + max_width: text_width, + }); + let line_height = text_style.line_height_in_pixels(cx.rem_size()); + let size = element.layout_as_root( + size(available_width, AvailableSpace::Definite(line_height)), + cx, + ); - width += size.width; - len += highlighted_chunk.text.len(); - fragments.push(LineFragment::Element { - element: Some(element), - size, - len: highlighted_chunk.text.len(), - }); + width += size.width; + len += highlighted_chunk.text.len(); + fragments.push(LineFragment::Element { + element: Some(element), + size, + len: highlighted_chunk.text.len(), + }); + } + ChunkReplacement::Str(x) => { + let text_style = if let Some(style) = highlighted_chunk.style { + Cow::Owned(text_style.clone().highlight(style)) + } else { + Cow::Borrowed(text_style) + }; + + let run = TextRun { + len: x.len(), + font: text_style.font(), + color: text_style.color, + background_color: text_style.background_color, + underline: text_style.underline, + strikethrough: text_style.strikethrough, + }; + let line_layout = cx + .text_system() + .shape_line(x, font_size, &[run]) + .unwrap() + .with_len(highlighted_chunk.text.len()); + + width += line_layout.width; + len += highlighted_chunk.text.len(); + fragments.push(LineFragment::Text(line_layout)) + } + } } else { for (ix, mut line_chunk) in highlighted_chunk.text.split('\n').enumerate() { if ix > 0 { @@ -5992,7 +6022,7 @@ fn layout_line( let chunks = snapshot.highlighted_chunks(row..row + DisplayRow(1), true, style); LineWithInvisibles::from_chunks( chunks, - &style.text, + &style, MAX_LINE_LEN, 1, snapshot.mode, diff --git a/crates/gpui/src/text_system/line.rs b/crates/gpui/src/text_system/line.rs index b8b698a042..7c18684cbc 100644 --- a/crates/gpui/src/text_system/line.rs +++ b/crates/gpui/src/text_system/line.rs @@ -44,6 +44,21 @@ impl ShapedLine { self.layout.len } + /// Override the len, useful if you're rendering text a + /// as text b (e.g. rendering invisibles). + pub fn with_len(mut self, len: usize) -> Self { + let layout = self.layout.as_ref(); + self.layout = Arc::new(LineLayout { + font_size: layout.font_size, + width: layout.width, + ascent: layout.ascent, + descent: layout.descent, + runs: layout.runs.clone(), + len, + }); + self + } + /// Paint the line of text to the window. pub fn paint( &self, diff --git a/crates/gpui/src/text_system/line_layout.rs b/crates/gpui/src/text_system/line_layout.rs index 7e5a43dee8..66eb914a30 100644 --- a/crates/gpui/src/text_system/line_layout.rs +++ b/crates/gpui/src/text_system/line_layout.rs @@ -29,7 +29,7 @@ pub struct LineLayout { } /// A run of text that has been shaped . -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct ShapedRun { /// The font id for this run pub font_id: FontId, From e7a0890086fbb99a2543bdfcc1d815d8d9ce6e7d Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 18 Nov 2024 16:47:36 -0700 Subject: [PATCH 022/886] Don't call setAllowsAutomaticKeyEquivalentLocalization on Big Sur (#20844) Closes #20821 Release Notes: - Fixed a crash on Big Sur (preview only) --- crates/gpui/src/platform/mac/platform.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index b744c658ce..faf9329734 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -343,8 +343,10 @@ impl MacPlatform { ns_string(key_to_native(&keystroke.key).as_ref()), ) .autorelease(); - let _: () = - msg_send![item, setAllowsAutomaticKeyEquivalentLocalization: NO]; + if MacPlatform::os_version().unwrap() >= SemanticVersion::new(12, 0, 0) { + let _: () = + msg_send![item, setAllowsAutomaticKeyEquivalentLocalization: NO]; + } item.setKeyEquivalentModifierMask_(mask); } // For multi-keystroke bindings, render the keystroke as part of the title. From 343c88574a6b9b701fc7dcf0edba185cfc1eb28f Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Tue, 19 Nov 2024 00:56:45 +0000 Subject: [PATCH 023/886] Improve file_types in default.json (#20429) Detect .env.* as Shell Script Move non glob json/jsonc/toml file_types into langauges/*/config.toml --- assets/settings/default.json | 11 ++--------- crates/languages/src/json/config.toml | 2 +- crates/languages/src/jsonc/config.toml | 2 +- extensions/toml/languages/toml/config.toml | 2 +- 4 files changed, 5 insertions(+), 12 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 2352e75ee9..7c4a9a8111 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -873,15 +873,8 @@ // "file_types": { "Plain Text": ["txt"], - "JSON": ["flake.lock"], - "JSONC": [ - "**/.zed/**/*.json", - "**/zed/**/*.json", - "**/Zed/**/*.json", - "tsconfig.json", - "pyrightconfig.json" - ], - "TOML": ["uv.lock"] + "JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json", "**/Zed/**/*.json"], + "Shell Script": [".env.*"] }, /// By default use a recent system version of node, or install our own. /// You can override this to use a version of node that is not in $PATH with: diff --git a/crates/languages/src/json/config.toml b/crates/languages/src/json/config.toml index c4a91c20b0..dc49f4f36e 100644 --- a/crates/languages/src/json/config.toml +++ b/crates/languages/src/json/config.toml @@ -1,6 +1,6 @@ name = "JSON" grammar = "json" -path_suffixes = ["json"] +path_suffixes = ["json", "flake.lock"] line_comments = ["// "] autoclose_before = ",]}" brackets = [ diff --git a/crates/languages/src/jsonc/config.toml b/crates/languages/src/jsonc/config.toml index fe62764b27..226ae92912 100644 --- a/crates/languages/src/jsonc/config.toml +++ b/crates/languages/src/jsonc/config.toml @@ -1,6 +1,6 @@ name = "JSONC" grammar = "jsonc" -path_suffixes = ["jsonc"] +path_suffixes = ["jsonc", "tsconfig.json", "pyrightconfig.json"] line_comments = ["// "] autoclose_before = ",]}" brackets = [ diff --git a/extensions/toml/languages/toml/config.toml b/extensions/toml/languages/toml/config.toml index d5c1172d84..f62290d9e9 100644 --- a/extensions/toml/languages/toml/config.toml +++ b/extensions/toml/languages/toml/config.toml @@ -1,6 +1,6 @@ name = "TOML" grammar = "toml" -path_suffixes = ["Cargo.lock", "toml", "Pipfile"] +path_suffixes = ["Cargo.lock", "toml", "Pipfile", "uv.lock"] line_comments = ["# "] autoclose_before = ",]}" brackets = [ From bd0f1974157bcc39c488e3ce86bc4f9b6d1a710d Mon Sep 17 00:00:00 2001 From: Kyle Kelley Date: Mon, 18 Nov 2024 18:12:23 -0800 Subject: [PATCH 024/886] Create `RunningKernel` trait to allow for native and remote jupyter kernels (#20842) Starts setting up a `RunningKernel` trait to make the remote kernel implementation easy to get started with. No release notes until this is all hooked up. Release Notes: - N/A --- Cargo.lock | 1985 ++++++++++------- Cargo.toml | 6 +- crates/quick_action_bar/src/repl_menu.rs | 2 +- crates/repl/Cargo.toml | 2 + crates/repl/src/kernels/mod.rs | 227 ++ .../{kernels.rs => kernels/native_kernel.rs} | 304 +-- crates/repl/src/kernels/remote_kernels.rs | 122 + crates/repl/src/repl.rs | 2 +- crates/repl/src/session.rs | 38 +- 9 files changed, 1600 insertions(+), 1088 deletions(-) create mode 100644 crates/repl/src/kernels/mod.rs rename crates/repl/src/{kernels.rs => kernels/native_kernel.rs} (62%) create mode 100644 crates/repl/src/kernels/remote_kernels.rs diff --git a/Cargo.lock b/Cargo.lock index aeded0f367..7f5934fca8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,7 +10,7 @@ dependencies = [ "auto_update", "editor", "extension_host", - "futures 0.3.30", + "futures 0.3.31", "gpui", "language", "lsp", @@ -23,19 +23,13 @@ dependencies = [ [[package]] name = "addr2line" -version = "0.24.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5fb1d8e4442bd405fdfd1dacb42792696b0cf9cb15882e5d097b742a676d375" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ - "gimli 0.31.0", + "gimli 0.31.1", ] -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - [[package]] name = "adler2" version = "2.0.0" @@ -100,8 +94,8 @@ dependencies = [ "miow", "parking_lot", "piper", - "polling 3.7.3", - "regex-automata 0.4.7", + "polling 3.7.4", + "regex-automata 0.4.9", "rustix-openpty", "serde", "signal-hook", @@ -124,9 +118,9 @@ checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1" [[package]] name = "allocator-api2" -version = "0.2.18" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +checksum = "45862d1c77f2228b9e10bc609d5bc203d86ebc9b87ad8d5d5167a6c9abf739d9" [[package]] name = "alsa" @@ -192,9 +186,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.6.15" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", @@ -207,36 +201,36 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.8" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.4" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" dependencies = [ "anstyle", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -245,13 +239,13 @@ version = "0.1.0" dependencies = [ "anyhow", "chrono", - "futures 0.3.30", + "futures 0.3.31", "http_client", "schemars", "serde", "serde_json", "strum 0.25.0", - "thiserror", + "thiserror 1.0.69", "util", ] @@ -278,9 +272,9 @@ dependencies = [ [[package]] name = "arbitrary" -version = "1.3.2" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" [[package]] name = "arg_enum_proc_macro" @@ -301,9 +295,9 @@ checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" [[package]] name = "arrayref" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d151e35f61089500b617991b791fc8bfd237ae50cd5950803758a179b41e67a" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" [[package]] name = "arrayvec" @@ -396,7 +390,7 @@ dependencies = [ "env_logger 0.11.5", "feature_flags", "fs", - "futures 0.3.30", + "futures 0.3.31", "fuzzy", "globset", "gpui", @@ -463,7 +457,7 @@ dependencies = [ "collections", "derive_more", "extension", - "futures 0.3.30", + "futures 0.3.31", "gpui", "language", "language_model", @@ -573,14 +567,14 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7ebdfa2ebdab6b1760375fa7d6f382b9f486eac35fc994625a00e89280bdbb7" +checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec" dependencies = [ "async-task", "concurrent-queue", - "fastrand 2.1.1", - "futures-lite 2.3.0", + "fastrand 2.2.0", + "futures-lite 2.5.0", "slab", ] @@ -604,7 +598,7 @@ checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" dependencies = [ "async-lock 3.4.0", "blocking", - "futures-lite 2.3.0", + "futures-lite 2.5.0", ] [[package]] @@ -615,10 +609,10 @@ checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" dependencies = [ "async-channel 2.3.1", "async-executor", - "async-io 2.3.4", + "async-io 2.4.0", "async-lock 3.4.0", "blocking", - "futures-lite 2.3.0", + "futures-lite 2.5.0", "once_cell", ] @@ -644,18 +638,18 @@ dependencies = [ [[package]] name = "async-io" -version = "2.3.4" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "444b0228950ee6501b3568d3c93bf1176a1fdbc3b758dcd9475046d30f4dc7e8" +checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" dependencies = [ "async-lock 3.4.0", "cfg-if", "concurrent-queue", "futures-io", - "futures-lite 2.3.0", + "futures-lite 2.5.0", "parking", - "polling 3.7.3", - "rustix 0.38.35", + "polling 3.7.4", + "rustix 0.38.40", "slab", "tracing", "windows-sys 0.59.0", @@ -689,7 +683,7 @@ checksum = "9343dc5acf07e79ff82d0c37899f079db3534d99f189a1837c8e549c99405bec" dependencies = [ "futures-util", "native-tls", - "thiserror", + "thiserror 1.0.69", "url", ] @@ -710,9 +704,9 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" dependencies = [ - "async-io 2.3.4", + "async-io 2.4.0", "blocking", - "futures-lite 2.3.0", + "futures-lite 2.5.0", ] [[package]] @@ -720,7 +714,7 @@ name = "async-pipe" version = "0.1.3" source = "git+https://github.com/zed-industries/async-pipe-rs?rev=82d00a04211cf4e1236029aa03e6b6ce2a74c553#82d00a04211cf4e1236029aa03e6b6ce2a74c553" dependencies = [ - "futures 0.3.30", + "futures 0.3.31", "log", ] @@ -737,28 +731,27 @@ dependencies = [ "cfg-if", "event-listener 3.1.0", "futures-lite 1.13.0", - "rustix 0.38.35", + "rustix 0.38.40", "windows-sys 0.48.0", ] [[package]] name = "async-process" -version = "2.2.4" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8a07789659a4d385b79b18b9127fc27e1a59e1e89117c78c5ea3b806f016374" +checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb" dependencies = [ "async-channel 2.3.1", - "async-io 2.3.4", + "async-io 2.4.0", "async-lock 3.4.0", "async-signal", "async-task", "blocking", "cfg-if", "event-listener 5.3.1", - "futures-lite 2.3.0", - "rustix 0.38.35", + "futures-lite 2.5.0", + "rustix 0.38.40", "tracing", - "windows-sys 0.59.0", ] [[package]] @@ -789,13 +782,13 @@ version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3" dependencies = [ - "async-io 2.3.4", + "async-io 2.4.0", "async-lock 3.4.0", "atomic-waker", "cfg-if", "futures-core", "futures-io", - "rustix 0.38.35", + "rustix 0.38.40", "signal-hook-registry", "slab", "windows-sys 0.59.0", @@ -803,21 +796,21 @@ dependencies = [ [[package]] name = "async-std" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" +checksum = "c634475f29802fde2b8f0b505b1bd00dfe4df7d4a000f0b36f7671197d5c3615" dependencies = [ "async-attributes", "async-channel 1.9.0", "async-global-executor", - "async-io 1.13.0", - "async-lock 2.8.0", - "async-process 1.8.1", + "async-io 2.4.0", + "async-lock 3.4.0", + "async-process 2.3.0", "crossbeam-utils", "futures-channel", "futures-core", "futures-io", - "futures-lite 1.13.0", + "futures-lite 2.5.0", "gloo-timers", "kv-log-macro", "log", @@ -831,9 +824,9 @@ dependencies = [ [[package]] name = "async-stream" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" dependencies = [ "async-stream-impl", "futures-core", @@ -842,9 +835,9 @@ dependencies = [ [[package]] name = "async-stream-impl" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", @@ -867,7 +860,7 @@ dependencies = [ "serde_qs 0.10.1", "smart-default", "smol_str", - "thiserror", + "thiserror 1.0.69", "tokio", ] @@ -915,6 +908,20 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "async-tungstenite" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce01ac37fdc85f10a43c43bc582cbd566720357011578a935761075f898baf58" +dependencies = [ + "async-std", + "futures-io", + "futures-util", + "log", + "pin-project-lite", + "tungstenite 0.19.0", +] + [[package]] name = "async-tungstenite" version = "0.28.0" @@ -947,9 +954,9 @@ checksum = "00b9f7252833d5ed4b00aa9604b563529dd5e11de9c23615de2dcdf91eb87b52" dependencies = [ "async-compression", "crc32fast", - "futures-lite 2.3.0", + "futures-lite 2.5.0", "pin-project", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -958,7 +965,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a860072022177f903e59730004fb5dc13db9275b79bb2aef7ba8ce831956c233" dependencies = [ - "bytes 1.7.2", + "bytes 1.8.0", "futures-sink", "futures-util", "memchr", @@ -1028,9 +1035,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "av1-grain" @@ -1048,18 +1055,18 @@ dependencies = [ [[package]] name = "avif-serialize" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "876c75a42f6364451a033496a14c44bffe41f5f4a8236f697391f11024e596d2" +checksum = "e335041290c43101ca215eed6f43ec437eb5a42125573f600fc3fa42b9bddd62" dependencies = [ "arrayvec", ] [[package]] name = "aws-config" -version = "1.5.5" +version = "1.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e95816a168520d72c0e7680c405a5a8c1fb6a035b4bc4b9d7b0de8e1a941697" +checksum = "9b49afaa341e8dd8577e1a2200468f98956d6eda50bcf4a53246cc00174ba924" dependencies = [ "aws-credential-types", "aws-runtime", @@ -1073,8 +1080,8 @@ dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", "aws-types", - "bytes 1.7.2", - "fastrand 2.1.1", + "bytes 1.8.0", + "fastrand 2.2.0", "hex", "http 0.2.12", "ring", @@ -1112,8 +1119,8 @@ dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", "aws-types", - "bytes 1.7.2", - "fastrand 2.1.1", + "bytes 1.8.0", + "fastrand 2.2.0", "http 0.2.12", "http-body 0.4.6", "once_cell", @@ -1138,7 +1145,7 @@ dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", "aws-types", - "bytes 1.7.2", + "bytes 1.8.0", "http 0.2.12", "once_cell", "regex-lite", @@ -1147,11 +1154,10 @@ dependencies = [ [[package]] name = "aws-sdk-s3" -version = "1.47.0" +version = "1.61.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cca49303c05d2a740b8a4552fac63a4db6ead84f7e7eeed04761fd3014c26f25" +checksum = "0e531658a0397d22365dfe26c3e1c0c8448bf6a3a2d8a098ded802f2b1261615" dependencies = [ - "ahash 0.8.11", "aws-credential-types", "aws-runtime", "aws-sigv4", @@ -1165,8 +1171,8 @@ dependencies = [ "aws-smithy-types", "aws-smithy-xml", "aws-types", - "bytes 1.7.2", - "fastrand 2.1.1", + "bytes 1.8.0", + "fastrand 2.2.0", "hex", "hmac", "http 0.2.12", @@ -1182,9 +1188,9 @@ dependencies = [ [[package]] name = "aws-sdk-sso" -version = "1.40.0" +version = "1.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5879bec6e74b648ce12f6085e7245417bc5f6d672781028384d2e494be3eb6d" +checksum = "09677244a9da92172c8dc60109b4a9658597d4d298b188dd0018b6a66b410ca4" dependencies = [ "aws-credential-types", "aws-runtime", @@ -1195,7 +1201,7 @@ dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", "aws-types", - "bytes 1.7.2", + "bytes 1.8.0", "http 0.2.12", "once_cell", "regex-lite", @@ -1204,9 +1210,9 @@ dependencies = [ [[package]] name = "aws-sdk-ssooidc" -version = "1.41.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ef4cd9362f638c22a3b959fd8df292e7e47fdf170270f86246b97109b5f2f7d" +checksum = "81fea2f3a8bb3bd10932ae7ad59cc59f65f270fc9183a7e91f501dc5efbef7ee" dependencies = [ "aws-credential-types", "aws-runtime", @@ -1217,7 +1223,7 @@ dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", "aws-types", - "bytes 1.7.2", + "bytes 1.8.0", "http 0.2.12", "once_cell", "regex-lite", @@ -1226,9 +1232,9 @@ dependencies = [ [[package]] name = "aws-sdk-sts" -version = "1.40.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b1e2735d2ab28b35ecbb5496c9d41857f52a0d6a0075bbf6a8af306045ea6f6" +checksum = "6ada54e5f26ac246dc79727def52f7f8ed38915cb47781e2a72213957dc3a7d5" dependencies = [ "aws-credential-types", "aws-runtime", @@ -1258,7 +1264,7 @@ dependencies = [ "aws-smithy-http", "aws-smithy-runtime-api", "aws-smithy-types", - "bytes 1.7.2", + "bytes 1.8.0", "crypto-bigint 0.5.5", "form_urlencoded", "hex", @@ -1289,13 +1295,13 @@ dependencies = [ [[package]] name = "aws-smithy-checksums" -version = "0.60.12" +version = "0.60.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "598b1689d001c4d4dc3cb386adb07d37786783aee3ac4b324bcadac116bf3d23" +checksum = "ba1a71073fca26775c8b5189175ea8863afb1c9ea2cceb02a5de5ad9dfbaa795" dependencies = [ "aws-smithy-http", "aws-smithy-types", - "bytes 1.7.2", + "bytes 1.8.0", "crc32c", "crc32fast", "hex", @@ -1315,7 +1321,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cef7d0a272725f87e51ba2bf89f8c21e4df61b9e49ae1ac367a6d69916ef7c90" dependencies = [ "aws-smithy-types", - "bytes 1.7.2", + "bytes 1.8.0", "crc32fast", ] @@ -1328,7 +1334,7 @@ dependencies = [ "aws-smithy-eventstream", "aws-smithy-runtime-api", "aws-smithy-types", - "bytes 1.7.2", + "bytes 1.8.0", "bytes-utils", "futures-core", "http 0.2.12", @@ -1369,8 +1375,8 @@ dependencies = [ "aws-smithy-http", "aws-smithy-runtime-api", "aws-smithy-types", - "bytes 1.7.2", - "fastrand 2.1.1", + "bytes 1.8.0", + "fastrand 2.2.0", "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", @@ -1394,7 +1400,7 @@ checksum = "92165296a47a812b267b4f41032ff8069ab7ff783696d217f0994a0d7ab585cd" dependencies = [ "aws-smithy-async", "aws-smithy-types", - "bytes 1.7.2", + "bytes 1.8.0", "http 0.2.12", "http 1.1.0", "pin-project-lite", @@ -1410,7 +1416,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fbd94a32b3a7d55d3806fe27d98d3ad393050439dd05eb53ece36ec5e3d3510" dependencies = [ "base64-simd", - "bytes 1.7.2", + "bytes 1.8.0", "bytes-utils", "futures-core", "http 0.2.12", @@ -1431,9 +1437,9 @@ dependencies = [ [[package]] name = "aws-smithy-xml" -version = "0.60.8" +version = "0.60.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d123fbc2a4adc3c301652ba8e149bf4bc1d1725affb9784eb20c953ace06bf55" +checksum = "ab0b0166827aa700d3dc519f72f8b3a91c35d0b8d042dc5d643a91e6f80648fc" dependencies = [ "xmlparser", ] @@ -1462,7 +1468,7 @@ dependencies = [ "axum-core", "base64 0.21.7", "bitflags 1.3.2", - "bytes 1.7.2", + "bytes 1.8.0", "futures-util", "headers", "http 0.2.12", @@ -1495,7 +1501,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" dependencies = [ "async-trait", - "bytes 1.7.2", + "bytes 1.8.0", "futures-util", "http 0.2.12", "http-body 0.4.6", @@ -1512,7 +1518,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9a320103719de37b7b4da4c8eb629d4573f6bcfd3dfe80d3208806895ccf81d" dependencies = [ "axum", - "bytes 1.7.2", + "bytes 1.8.0", "futures-util", "http 0.2.12", "mime", @@ -1535,7 +1541,7 @@ dependencies = [ "addr2line", "cfg-if", "libc", - "miniz_oxide 0.8.0", + "miniz_oxide", "object", "rustc-demangle", "windows-targets 0.52.6", @@ -1583,9 +1589,9 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "bigdecimal" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d712318a27c7150326677b321a5fa91b55f6d9034ffd67f20319e147d40cee" +checksum = "8f850665a0385e070b64c38d2354e6c104c8479c59868d1e48a0c13ee2c7a1c1" dependencies = [ "autocfg", "libm", @@ -1604,26 +1610,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bindgen" -version = "0.69.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" -dependencies = [ - "bitflags 2.6.0", - "cexpr", - "clang-sys", - "itertools 0.12.1", - "lazy_static", - "lazycell", - "proc-macro2", - "quote", - "regex", - "rustc-hash 1.1.0", - "shlex", - "syn 2.0.87", -] - [[package]] name = "bindgen" version = "0.70.1" @@ -1712,9 +1698,9 @@ dependencies = [ [[package]] name = "bitstream-io" -version = "2.5.3" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b81e1519b0d82120d2fd469d5bfb2919a9361c48b02d82d04befc1cdd2002452" +checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" [[package]] name = "bitvec" @@ -1825,15 +1811,15 @@ dependencies = [ "async-channel 2.3.1", "async-task", "futures-io", - "futures-lite 2.3.0", + "futures-lite 2.5.0", "piper", ] [[package]] name = "borsh" -version = "1.5.1" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6362ed55def622cddc70a4746a68554d7b687713770de539e59a739b249f8ed" +checksum = "2506947f73ad44e344215ccd6403ac2ae18cd8e046e581a441bf8d199f257f03" dependencies = [ "borsh-derive", "cfg_aliases 0.2.1", @@ -1841,16 +1827,15 @@ dependencies = [ [[package]] name = "borsh-derive" -version = "1.5.1" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3ef8005764f53cd4dca619f5bf64cafd4664dada50ece25e4d81de54c80cc0b" +checksum = "c2593a3b8b938bd68373196c9832f516be11fa487ef4ae745eb282e6a56a7244" dependencies = [ "once_cell", "proc-macro-crate", "proc-macro2", "quote", "syn 2.0.87", - "syn_derive", ] [[package]] @@ -1868,20 +1853,20 @@ dependencies = [ [[package]] name = "bstr" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c" +checksum = "1a68f1f47cdf0ec8ee4b941b2eee2a80cb796db73118c0dd09ac63fbe405be22" dependencies = [ "memchr", - "regex-automata 0.4.7", + "regex-automata 0.4.9", "serde", ] [[package]] name = "built" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "236e6289eda5a812bc6b53c3b024039382a2895fbbeef2d748b2931546d392c4" +checksum = "c360505aed52b7ec96a3636c3f039d99103c37d1d9b4f7a8c743d3ea9ffcd03b" [[package]] name = "bumpalo" @@ -1919,18 +1904,18 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.17.1" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773d90827bc3feecfb67fab12e24de0749aad83c74b9504ecde46237b5cd24e2" +checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.7.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc8b54b395f2fcfbb3d90c47b01c7f444d94d05bdeb775811dec868ac3bbc26" +checksum = "bcfcc3cd946cb52f0bbfdbbcfa2f4e24f75ebb6c0e1002f7c25904fada18b9ec" dependencies = [ "proc-macro2", "quote", @@ -1961,9 +1946,9 @@ dependencies = [ [[package]] name = "bytes" -version = "1.7.2" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" +checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" [[package]] name = "bytes-utils" @@ -1971,7 +1956,7 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" dependencies = [ - "bytes 1.7.2", + "bytes 1.8.0", "either", ] @@ -1984,7 +1969,7 @@ dependencies = [ "client", "collections", "fs", - "futures 0.3.30", + "futures 0.3.31", "gpui", "http_client", "language", @@ -2007,10 +1992,10 @@ checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" dependencies = [ "bitflags 2.6.0", "log", - "polling 3.7.3", - "rustix 0.38.35", + "polling 3.7.4", + "rustix 0.38.40", "slab", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -2020,7 +2005,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" dependencies = [ "calloop", - "rustix 0.38.35", + "rustix 0.38.40", "wayland-backend", "wayland-client", ] @@ -2036,9 +2021,9 @@ dependencies = [ [[package]] name = "cap-fs-ext" -version = "3.2.0" +version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb23061fc1c4ead4e45ca713080fe768e6234e959f5a5c399c39eb41aa34e56e" +checksum = "e16619ada836f12897a72011fe99b03f0025b87a8dbbea4f3c9f89b458a23bf3" dependencies = [ "cap-primitives", "cap-std", @@ -2048,21 +2033,21 @@ dependencies = [ [[package]] name = "cap-net-ext" -version = "3.2.0" +version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83ae11f116bcbafc5327c6af250341db96b5930046732e1905f7dc65887e0e1" +checksum = "710b0eb776410a22c89a98f2f80b2187c2ac3a8206b99f3412332e63c9b09de0" dependencies = [ "cap-primitives", "cap-std", - "rustix 0.38.35", + "rustix 0.38.40", "smallvec", ] [[package]] name = "cap-primitives" -version = "3.2.0" +version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d00bd8d26c4270d950eaaa837387964a2089a1c3c349a690a1fa03221d29531" +checksum = "82fa6c3f9773feab88d844aa50035a33fb6e7e7426105d2f4bb7aadc42a5f89a" dependencies = [ "ambient-authority", "fs-set-times", @@ -2070,16 +2055,16 @@ dependencies = [ "io-lifetimes 2.0.3", "ipnet", "maybe-owned", - "rustix 0.38.35", + "rustix 0.38.40", "windows-sys 0.52.0", "winx", ] [[package]] name = "cap-rand" -version = "3.2.0" +version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbcb16a619d8b8211ed61f42bd290d2a1ac71277a69cf8417ec0996fa92f5211" +checksum = "53774d49369892b70184f8312e50c1b87edccb376691de4485b0ff554b27c36c" dependencies = [ "ambient-authority", "rand 0.8.5", @@ -2087,27 +2072,27 @@ dependencies = [ [[package]] name = "cap-std" -version = "3.2.0" +version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19eb8e3d71996828751c1ed3908a439639752ac6bdc874e41469ef7fc15fbd7f" +checksum = "7f71b70818556b4fe2a10c7c30baac3f5f45e973f49fc2673d7c75c39d0baf5b" dependencies = [ "cap-primitives", "io-extras", "io-lifetimes 2.0.3", - "rustix 0.38.35", + "rustix 0.38.40", ] [[package]] name = "cap-time-ext" -version = "3.2.0" +version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61142dc51e25b7acc970ca578ce2c3695eac22bbba46c1073f5f583e78957725" +checksum = "69dd48afa2363f746c93f961c211f6f099fb594a3446b8097bc5f79db51b6816" dependencies = [ "ambient-authority", "cap-primitives", "iana-time-zone", "once_cell", - "rustix 0.38.35", + "rustix 0.38.40", "winx", ] @@ -2131,7 +2116,7 @@ dependencies = [ "semver", "serde", "serde_json", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -2166,7 +2151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fce8dd7fcfcbf3a0a87d8f515194b49d6135acab73e18bd380d1d93bb1a15eb" dependencies = [ "heck 0.4.1", - "indexmap 2.4.0", + "indexmap 2.6.0", "log", "proc-macro2", "quote", @@ -2179,9 +2164,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.15" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6" +checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" dependencies = [ "jobserver", "libc", @@ -2239,7 +2224,7 @@ dependencies = [ "client", "clock", "collections", - "futures 0.3.30", + "futures 0.3.31", "gpui", "http_client", "language", @@ -2349,9 +2334,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.24" +version = "4.5.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d7db6eca8c205649e8d3ccd05aa5042b1800a784e56bc7c43524fde8abbfa9b" +checksum = "d9647a559c112175f17cf724dc72d3645680a883c58481332779192b0d8e7a01" dependencies = [ "clap", ] @@ -2370,9 +2355,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" [[package]] name = "cli" @@ -2403,17 +2388,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0875e527e299fc5f4faba42870bf199a39ab0bb2dbba1b8aef0a2151451130f" dependencies = [ "bstr", - "bytes 1.7.2", + "bytes 1.8.0", "clickhouse-derive", "clickhouse-rs-cityhash-sys", - "futures 0.3.30", + "futures 0.3.31", "hyper 0.14.31", "hyper-tls", "lz4", "sealed", "serde", "static_assertions", - "thiserror", + "thiserror 1.0.69", "tokio", "url", ] @@ -2446,13 +2431,13 @@ dependencies = [ "anyhow", "async-native-tls", "async-recursion 0.3.2", - "async-tungstenite", + "async-tungstenite 0.28.0", "chrono", "clock", "cocoa 0.26.0", "collections", "feature_flags", - "futures 0.3.30", + "futures 0.3.31", "gpui", "http_client", "log", @@ -2474,7 +2459,7 @@ dependencies = [ "sysinfo", "telemetry_events", "text", - "thiserror", + "thiserror 1.0.69", "time", "tiny_http", "tokio-socks", @@ -2579,7 +2564,7 @@ dependencies = [ "assistant", "async-stripe", "async-trait", - "async-tungstenite", + "async-tungstenite 0.28.0", "audio", "aws-config", "aws-sdk-kinesis", @@ -2597,14 +2582,14 @@ dependencies = [ "collections", "context_servers", "ctor", - "dashmap 6.0.1", + "dashmap 6.1.0", "derive_more", "editor", "env_logger 0.11.5", "envy", "file_finder", "fs", - "futures 0.3.30", + "futures 0.3.31", "git", "git_hosting_providers", "google_ai", @@ -2657,7 +2642,7 @@ dependencies = [ "telemetry_events", "text", "theme", - "thiserror", + "thiserror 1.0.69", "time", "tokio", "toml 0.8.19", @@ -2685,7 +2670,7 @@ dependencies = [ "db", "editor", "emojis", - "futures 0.3.30", + "futures 0.3.31", "fuzzy", "gpui", "http_client", @@ -2732,9 +2717,9 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "colorchoice" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "combine" @@ -2742,7 +2727,7 @@ version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" dependencies = [ - "bytes 1.7.2", + "bytes 1.8.0", "memchr", ] @@ -2844,7 +2829,7 @@ dependencies = [ "anyhow", "collections", "command_palette_hooks", - "futures 0.3.30", + "futures 0.3.31", "gpui", "log", "parking_lot", @@ -2889,7 +2874,7 @@ dependencies = [ "command_palette_hooks", "editor", "fs", - "futures 0.3.30", + "futures 0.3.31", "gpui", "http_client", "indoc", @@ -3022,11 +3007,11 @@ dependencies = [ [[package]] name = "coreaudio-sys" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f01585027057ff5f0a5bf276174ae4c1594a2c5bde93d5f46a016d76270f5a9" +checksum = "2ce857aa0b77d77287acc1ac3e37a05a8c95a2af3647d23b15f263bdaeb7562b" dependencies = [ - "bindgen 0.69.4", + "bindgen", ] [[package]] @@ -3085,9 +3070,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.13" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad" +checksum = "0ca741a962e1b0bff6d724a1a0958b686406e853bb14061f218562e1896f95e6" dependencies = [ "libc", ] @@ -3406,9 +3391,9 @@ dependencies = [ [[package]] name = "dashmap" -version = "6.0.1" +version = "6.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804c8821570c3f8b70230c2ba75ffa5c0f9a4189b9a432b6656c536712acae28" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" dependencies = [ "cfg-if", "crossbeam-utils", @@ -3561,7 +3546,7 @@ dependencies = [ "fuzzy-matcher", "shell-words", "tempfile", - "thiserror", + "thiserror 1.0.69", "zeroize", ] @@ -3624,6 +3609,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "dlib" version = "0.5.2" @@ -3670,9 +3666,9 @@ dependencies = [ [[package]] name = "dwrote" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2da3498378ed373237bdef1eddcc64e7be2d3ba4841f4c22a998e81cadeea83c" +checksum = "70182709525a3632b2ba96b6569225467b18ecb4a77f46d255f713a6bebf05fd" dependencies = [ "lazy_static", "libc", @@ -3721,7 +3717,7 @@ dependencies = [ "emojis", "env_logger 0.11.5", "file_icons", - "futures 0.3.30", + "futures 0.3.31", "fuzzy", "git", "gpui", @@ -3767,18 +3763,6 @@ dependencies = [ "workspace", ] -[[package]] -name = "educe" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4bd92664bf78c4d3dba9b7cdafce6fa15b13ed3ed16175218196942e99168a8" -dependencies = [ - "enum-ordinalize", - "proc-macro2", - "quote", - "syn 2.0.87", -] - [[package]] name = "either" version = "1.13.0" @@ -3822,9 +3806,9 @@ dependencies = [ [[package]] name = "embed-resource" -version = "2.4.3" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4edcacde9351c33139a41e3c97eb2334351a81a2791bebb0b243df837128f602" +checksum = "b68b6f9f63a0b6a38bc447d4ce84e2b388f3ec95c99c641c8ff0dd3ef89a6379" dependencies = [ "cc", "memchr", @@ -3863,9 +3847,9 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "encoding_rs" -version = "0.8.34" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ "cfg-if", ] @@ -3876,26 +3860,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" -[[package]] -name = "enum-ordinalize" -version = "4.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea0dcfa4e54eeb516fe454635a95753ddd39acda650ce703031c6973e315dd5" -dependencies = [ - "enum-ordinalize-derive", -] - -[[package]] -name = "enum-ordinalize-derive" -version = "4.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.87", -] - [[package]] name = "enumflags2" version = "0.7.10" @@ -4117,15 +4081,14 @@ dependencies = [ [[package]] name = "exr" -version = "1.72.0" +version = "1.73.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "887d93f60543e9a9362ef8a21beedd0a833c5d9610e18c67abe15a5963dcb1a4" +checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0" dependencies = [ "bit_field", - "flume", "half", "lebe", - "miniz_oxide 0.7.4", + "miniz_oxide", "rayon-core", "smallvec", "zune-inflate", @@ -4141,7 +4104,7 @@ dependencies = [ "async-trait", "collections", "fs", - "futures 0.3.30", + "futures 0.3.31", "gpui", "http_client", "language", @@ -4193,7 +4156,7 @@ dependencies = [ "env_logger 0.11.5", "extension", "fs", - "futures 0.3.30", + "futures 0.3.31", "gpui", "http_client", "language", @@ -4282,8 +4245,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298" dependencies = [ "bit-set 0.8.0", - "regex-automata 0.4.7", - "regex-syntax 0.8.4", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", ] [[package]] @@ -4303,9 +4266,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" [[package]] name = "fd-lock" @@ -4314,15 +4277,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e5768da2206272c81ef0b5e951a41862938a6070da63bcea197899942d3b947" dependencies = [ "cfg-if", - "rustix 0.38.35", + "rustix 0.38.40", "windows-sys 0.52.0", ] [[package]] name = "fdeflate" -version = "0.3.4" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" +checksum = "07c6f4c64c1d33a3111c4466f7365ebdcc37c5bd1ea0d62aae2e3d722aacbedb" dependencies = [ "simd-adler32", ] @@ -4331,7 +4294,7 @@ dependencies = [ name = "feature_flags" version = "0.1.0" dependencies = [ - "futures 0.3.30", + "futures 0.3.31", "gpui", ] @@ -4344,7 +4307,7 @@ dependencies = [ "client", "db", "editor", - "futures 0.3.30", + "futures 0.3.31", "gpui", "http_client", "human_bytes", @@ -4385,7 +4348,7 @@ dependencies = [ "editor", "env_logger 0.11.5", "file_icons", - "futures 0.3.30", + "futures 0.3.31", "fuzzy", "gpui", "language", @@ -4423,7 +4386,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7199d965852c3bac31f779ef99cbb4537f80e952e2d6aa0ffeb30cce00f4f46e" dependencies = [ "libc", - "thiserror", + "thiserror 1.0.69", "winapi", ] @@ -4447,12 +4410,12 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" dependencies = [ "crc32fast", - "miniz_oxide 0.8.0", + "miniz_oxide", ] [[package]] @@ -4485,6 +4448,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" + [[package]] name = "font-kit" version = "0.14.1" @@ -4511,9 +4480,9 @@ dependencies = [ [[package]] name = "font-types" -version = "0.6.0" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f0189ccb084f77c5523e08288d418cbaa09c451a08515678a0aa265df9a8b60" +checksum = "b3971f9a5ca983419cdc386941ba3b9e1feba01a0ab888adf78739feb2798492" dependencies = [ "bytemuck", ] @@ -4623,7 +4592,7 @@ dependencies = [ "cocoa 0.26.0", "collections", "fsevent", - "futures 0.3.30", + "futures 0.3.31", "git", "git2", "gpui", @@ -4650,7 +4619,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "033b337d725b97690d86893f9de22b67b80dcc4e9ad815f348254c38119db8fb" dependencies = [ "io-lifetimes 2.0.3", - "rustix 0.38.35", + "rustix 0.38.40", "windows-sys 0.52.0", ] @@ -4707,9 +4676,9 @@ checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678" [[package]] name = "futures" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -4726,16 +4695,16 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f444c45a1cb86f2a7e301469fd50a82084a60dadc25d94529a8312276ecb71a" dependencies = [ - "futures 0.3.30", + "futures 0.3.31", "futures-timer", "pin-utils", ] [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -4743,15 +4712,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -4771,9 +4740,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" @@ -4792,11 +4761,11 @@ dependencies = [ [[package]] name = "futures-lite" -version = "2.3.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" +checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1" dependencies = [ - "fastrand 2.1.1", + "fastrand 2.2.0", "futures-core", "futures-io", "parking", @@ -4805,9 +4774,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", @@ -4816,15 +4785,15 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-timer" @@ -4834,9 +4803,9 @@ checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures 0.1.31", "futures-channel", @@ -4930,15 +4899,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" dependencies = [ "fallible-iterator", - "indexmap 2.4.0", + "indexmap 2.6.0", "stable_deref_trait", ] [[package]] name = "gimli" -version = "0.31.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "git" @@ -4987,7 +4956,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", - "futures 0.3.30", + "futures 0.3.31", "git", "gpui", "http_client", @@ -5015,15 +4984,15 @@ dependencies = [ "aho-corasick", "bstr", "log", - "regex-automata 0.4.7", - "regex-syntax 0.8.4", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", ] [[package]] name = "gloo-timers" -version = "0.2.6" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" dependencies = [ "futures-channel", "futures-core", @@ -5033,9 +5002,9 @@ dependencies = [ [[package]] name = "glow" -version = "0.14.0" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f865cbd94bd355b89611211e49508da98a1fce0ad755c1e8448fb96711b24528" +checksum = "d51fa363f025f5c111e03f13eda21162faeacb6911fe8caa0c0349f9cf0c4483" dependencies = [ "js-sys", "slotmap", @@ -5073,7 +5042,7 @@ name = "google_ai" version = "0.1.0" dependencies = [ "anyhow", - "futures 0.3.30", + "futures 0.3.31", "http_client", "schemars", "serde", @@ -5120,7 +5089,7 @@ dependencies = [ "ashpd", "async-task", "backtrace", - "bindgen 0.70.1", + "bindgen", "blade-graphics", "blade-macros", "blade-util", @@ -5145,7 +5114,7 @@ dependencies = [ "flume", "font-kit", "foreign-types 0.5.0", - "futures 0.3.30", + "futures 0.3.31", "gpui_macros", "http_client", "image", @@ -5179,7 +5148,7 @@ dependencies = [ "strum 0.25.0", "sum_tree", "taffy", - "thiserror", + "thiserror 1.0.69", "unicode-segmentation", "usvg", "util", @@ -5230,13 +5199,13 @@ version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" dependencies = [ - "bytes 1.7.2", + "bytes 1.8.0", "fnv", "futures-core", "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.4.0", + "indexmap 2.6.0", "slab", "tokio", "tokio-util", @@ -5250,12 +5219,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" dependencies = [ "atomic-waker", - "bytes 1.7.2", + "bytes 1.8.0", "fnv", "futures-core", "futures-sink", "http 1.1.0", - "indexmap 2.4.0", + "indexmap 2.6.0", "slab", "tokio", "tokio-util", @@ -5283,7 +5252,7 @@ dependencies = [ "pest_derive", "serde", "serde_json", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -5298,7 +5267,7 @@ dependencies = [ "pest_derive", "serde", "serde_json", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -5330,6 +5299,17 @@ dependencies = [ "serde", ] +[[package]] +name = "hashbrown" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + [[package]] name = "hashlink" version = "0.8.4" @@ -5355,7 +5335,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" dependencies = [ "base64 0.21.7", - "bytes 1.7.2", + "bytes 1.8.0", "headers-core", "http 0.2.12", "httpdate", @@ -5534,7 +5514,7 @@ version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ - "bytes 1.7.2", + "bytes 1.8.0", "fnv", "itoa", ] @@ -5545,7 +5525,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" dependencies = [ - "bytes 1.7.2", + "bytes 1.8.0", "fnv", "itoa", ] @@ -5556,7 +5536,7 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ - "bytes 1.7.2", + "bytes 1.8.0", "http 0.2.12", "pin-project-lite", ] @@ -5567,7 +5547,7 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ - "bytes 1.7.2", + "bytes 1.8.0", "http 1.1.0", ] @@ -5577,7 +5557,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ - "bytes 1.7.2", + "bytes 1.8.0", "futures-util", "http 1.1.0", "http-body 1.0.1", @@ -5616,9 +5596,9 @@ name = "http_client" version = "0.1.0" dependencies = [ "anyhow", - "bytes 1.7.2", + "bytes 1.8.0", "derive_more", - "futures 0.3.30", + "futures 0.3.31", "http 1.1.0", "log", "serde", @@ -5628,9 +5608,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.9.4" +version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" [[package]] name = "httpdate" @@ -5656,7 +5636,7 @@ version = "0.14.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c08302e8fa335b151b788c775ff56e7a03ae64ff85c548ee820fecb70356e85" dependencies = [ - "bytes 1.7.2", + "bytes 1.8.0", "futures-channel", "futures-core", "futures-util", @@ -5676,11 +5656,11 @@ dependencies = [ [[package]] name = "hyper" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" +checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" dependencies = [ - "bytes 1.7.2", + "bytes 1.8.0", "futures-channel", "futures-util", "h2 0.4.6", @@ -5718,9 +5698,9 @@ checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" dependencies = [ "futures-util", "http 1.1.0", - "hyper 1.4.1", + "hyper 1.5.0", "hyper-util", - "rustls 0.23.13", + "rustls 0.23.16", "rustls-native-certs 0.8.0", "rustls-pki-types", "tokio", @@ -5734,7 +5714,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ - "bytes 1.7.2", + "bytes 1.8.0", "hyper 0.14.31", "native-tls", "tokio", @@ -5743,16 +5723,16 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" dependencies = [ - "bytes 1.7.2", + "bytes 1.8.0", "futures-channel", "futures-util", "http 1.1.0", "http-body 1.0.1", - "hyper 1.4.1", + "hyper 1.5.0", "pin-project-lite", "socket2 0.5.7", "tokio", @@ -5762,9 +5742,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.60" +version = "0.1.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -5783,6 +5763,124 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "id-arena" version = "2.2.1" @@ -5791,12 +5889,23 @@ checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" [[package]] name = "idna" -version = "0.5.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", ] [[package]] @@ -5809,7 +5918,7 @@ dependencies = [ "globset", "log", "memchr", - "regex-automata 0.4.7", + "regex-automata 0.4.9", "same-file", "walkdir", "winapi-util", @@ -5817,9 +5926,9 @@ dependencies = [ [[package]] name = "image" -version = "0.25.2" +version = "0.25.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99314c8a2152b8ddb211f924cdae532d8c5e4c8bb54728e12fff1b0cd5963a10" +checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b" dependencies = [ "bytemuck", "byteorder-lite", @@ -5840,9 +5949,9 @@ dependencies = [ [[package]] name = "image-webp" -version = "0.1.3" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f79afb8cbee2ef20f59ccd477a218c12a93943d075b492015ecb1bb81f8ee904" +checksum = "e031e8e3d94711a9ccb5d6ea357439ef3dcbed361798bd4071dc4d9793fbe22f" dependencies = [ "byteorder-lite", "quick-error", @@ -5872,9 +5981,9 @@ checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" [[package]] name = "imgref" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44feda355f4159a7c757171a77de25daf6411e217b4cabd03bd6650690468126" +checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" [[package]] name = "indexed_docs" @@ -5887,7 +5996,7 @@ dependencies = [ "derive_more", "extension", "fs", - "futures 0.3.30", + "futures 0.3.31", "fuzzy", "gpui", "heed", @@ -5916,12 +6025,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.4.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", - "hashbrown 0.14.5", + "hashbrown 0.15.1", "serde", ] @@ -5956,7 +6065,7 @@ dependencies = [ "copilot", "editor", "fs", - "futures 0.3.30", + "futures 0.3.31", "gpui", "indoc", "language", @@ -6035,9 +6144,9 @@ dependencies = [ [[package]] name = "io-extras" -version = "0.18.2" +version = "0.18.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9f046b9af244f13b3bd939f55d16830ac3a201e8a9ba9661bfcb03e2be72b9b" +checksum = "7d45fd7584f9b67ac37bc041212d06bfac0700b36456b05890d36a3b626260eb" dependencies = [ "io-lifetimes 2.0.3", "windows-sys 0.52.0", @@ -6090,9 +6199,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.9.0" +version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" [[package]] name = "is-docker" @@ -6174,7 +6283,7 @@ dependencies = [ "combine", "jni-sys", "log", - "thiserror", + "thiserror 1.0.69", "walkdir", "windows-sys 0.45.0", ] @@ -6218,9 +6327,9 @@ checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" [[package]] name = "js-sys" -version = "0.3.70" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" dependencies = [ "wasm-bindgen", ] @@ -6241,15 +6350,51 @@ dependencies = [ ] [[package]] -name = "jupyter-serde" -version = "0.4.0" +name = "jupyter-protocol" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd71aa17c4fa65e6d7536ab2728881a41f8feb2ee5841c2240516c3c3d65d8b3" +checksum = "5f3e9d36f282f7e0400de20921d283121a97c5a5a6db2c1bb0c0853defff9934" +dependencies = [ + "anyhow", + "async-trait", + "bytes 1.8.0", + "chrono", + "futures 0.3.31", + "jupyter-serde", + "rand 0.8.5", + "serde", + "serde_json", + "uuid", +] + +[[package]] +name = "jupyter-serde" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11adb69edaf2eb03d5e84249f68f870dd03d4c8f955314b5a32b2db5798e9b9a" dependencies = [ "anyhow", "serde", "serde_json", - "thiserror", + "thiserror 1.0.69", + "uuid", +] + +[[package]] +name = "jupyter-websocket-client" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d315d037789a652084877b0919615e937d2f2e877b01aa4ba8fcc1ab07cb58b" +dependencies = [ + "anyhow", + "async-trait", + "async-tungstenite 0.22.2", + "futures 0.3.31", + "jupyter-protocol", + "jupyter-serde", + "serde", + "serde_json", + "url", "uuid", ] @@ -6285,9 +6430,9 @@ dependencies = [ [[package]] name = "kurbo" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e5aa9f0f96a938266bdb12928a67169e8d22c6a786fda8ed984b85e6ba93c3c" +checksum = "89234b2cc610a7dd927ebde6b41dd1a5d4214cffaef4cf1fb2195d592f92518f" dependencies = [ "arrayvec", "smallvec", @@ -6314,7 +6459,7 @@ dependencies = [ "ctor", "ec4rs", "env_logger 0.11.5", - "futures 0.3.30", + "futures 0.3.31", "fuzzy", "git", "globset", @@ -6372,7 +6517,7 @@ dependencies = [ "editor", "env_logger 0.11.5", "feature_flags", - "futures 0.3.30", + "futures 0.3.31", "google_ai", "gpui", "http_client", @@ -6396,7 +6541,7 @@ dependencies = [ "telemetry_events", "text", "theme", - "thiserror", + "thiserror 1.0.69", "tiktoken-rs", "ui", "unindent", @@ -6429,7 +6574,7 @@ dependencies = [ "copilot", "editor", "env_logger 0.11.5", - "futures 0.3.30", + "futures 0.3.31", "gpui", "itertools 0.13.0", "language", @@ -6455,7 +6600,7 @@ dependencies = [ "async-tar", "async-trait", "collections", - "futures 0.3.30", + "futures 0.3.31", "gpui", "http_client", "language", @@ -6512,12 +6657,6 @@ dependencies = [ "spin", ] -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - [[package]] name = "leb128" version = "0.2.5" @@ -6548,13 +6687,12 @@ dependencies = [ [[package]] name = "libfuzzer-sys" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7" +checksum = "9b9569d2f74e257076d8c6bfa73fb505b46b851e51ddaecc825944aa3bed17fa" dependencies = [ "arbitrary", "cc", - "once_cell", ] [[package]] @@ -6581,9 +6719,9 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.8" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" [[package]] name = "libmimalloc-sys" @@ -6603,7 +6741,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags 2.6.0", "libc", - "redox_syscall 0.5.3", + "redox_syscall 0.5.7", ] [[package]] @@ -6670,6 +6808,12 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "litemap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" + [[package]] name = "live_kit_client" version = "0.1.0" @@ -6679,7 +6823,7 @@ dependencies = [ "async-trait", "collections", "core-foundation 0.9.4", - "futures 0.3.30", + "futures 0.3.31", "gpui", "live_kit_server", "log", @@ -6750,11 +6894,11 @@ dependencies = [ [[package]] name = "lru" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37ee39891760e7d94734f6f63fedc29a2e4a152f836120753a72503f09fcf904" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown 0.14.5", + "hashbrown 0.15.1", ] [[package]] @@ -6766,7 +6910,7 @@ dependencies = [ "collections", "ctor", "env_logger 0.11.5", - "futures 0.3.30", + "futures 0.3.31", "gpui", "log", "lsp-types", @@ -6795,19 +6939,18 @@ dependencies = [ [[package]] name = "lz4" -version = "1.26.0" +version = "1.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "958b4caa893816eea05507c20cfe47574a43d9a697138a7872990bba8a0ece68" +checksum = "4d1febb2b4a79ddd1980eede06a8f7902197960aa0383ffcfdd62fe723036725" dependencies = [ - "libc", "lz4-sys", ] [[package]] name = "lz4-sys" -version = "1.10.0" +version = "1.11.1+lz4-1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "109de74d5d2353660401699a4174a4ff23fcc649caf553df71933c7fb45ad868" +checksum = "6bd8c0d6c6ed0cd30b3652886bb8711dc4bb01d637a68105a3d5158039b418e6" dependencies = [ "cc", "libc", @@ -6850,7 +6993,7 @@ dependencies = [ "anyhow", "assets", "env_logger 0.11.5", - "futures 0.3.30", + "futures 0.3.31", "gpui", "language", "languages", @@ -6938,6 +7081,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" dependencies = [ "cfg-if", + "rayon", ] [[package]] @@ -6991,7 +7135,7 @@ name = "media" version = "0.1.0" dependencies = [ "anyhow", - "bindgen 0.70.1", + "bindgen", "core-foundation 0.9.4", "foreign-types 0.5.0", "metal", @@ -7010,14 +7154,14 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2cffa4ad52c6f791f4f8b15f0c05f9824b2ced1160e88cc393d64fff9a8ac64" dependencies = [ - "rustix 0.38.35", + "rustix 0.38.40", ] [[package]] name = "memmap2" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" +checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" dependencies = [ "libc", ] @@ -7085,16 +7229,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" -[[package]] -name = "miniz_oxide" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" -dependencies = [ - "adler", - "simd-adler32", -] - [[package]] name = "miniz_oxide" version = "0.8.0" @@ -7102,6 +7236,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" dependencies = [ "adler2", + "simd-adler32", ] [[package]] @@ -7162,7 +7297,7 @@ dependencies = [ "collections", "ctor", "env_logger 0.11.5", - "futures 0.3.30", + "futures 0.3.31", "gpui", "itertools 0.13.0", "language", @@ -7196,12 +7331,12 @@ dependencies = [ "cfg_aliases 0.1.1", "codespan-reporting", "hexf-parse", - "indexmap 2.4.0", + "indexmap 2.6.0", "log", "rustc-hash 1.1.0", "spirv", "termcolor", - "thiserror", + "thiserror 1.0.69", "unicode-xid", ] @@ -7242,16 +7377,16 @@ dependencies = [ [[package]] name = "nbformat" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9ffb2ca556072f114bcaf2ca01dde7f1bc8a4946097dd804cb5a22d8af7d6df" +checksum = "187de1b1f1430353ef9b5208096d84f7bf089ee1593f14213d122b7fbb1f3dee" dependencies = [ "anyhow", "chrono", "jupyter-serde", "serde", "serde_json", - "thiserror", + "thiserror 1.0.69", "uuid", ] @@ -7266,7 +7401,7 @@ dependencies = [ "log", "ndk-sys", "num_enum", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -7314,7 +7449,7 @@ dependencies = [ "async-trait", "async-watch", "async_zip", - "futures 0.3.30", + "futures 0.3.31", "http_client", "log", "paths", @@ -7600,7 +7735,7 @@ version = "0.8.0-pre" source = "git+https://github.com/KillTheMule/nvim-rs?branch=master#69500bae73b8b3f02a05b7bee621a0d0e633da6c" dependencies = [ "async-trait", - "futures 0.3.30", + "futures 0.3.31", "log", "parity-tokio-ipc", "rmp", @@ -7621,13 +7756,13 @@ dependencies = [ [[package]] name = "object" -version = "0.36.4" +version = "0.36.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" dependencies = [ "crc32fast", - "hashbrown 0.14.5", - "indexmap 2.4.0", + "hashbrown 0.15.1", + "indexmap 2.6.0", "memchr", ] @@ -7659,7 +7794,7 @@ name = "ollama" version = "0.1.0" dependencies = [ "anyhow", - "futures 0.3.30", + "futures 0.3.31", "http_client", "schemars", "serde", @@ -7668,9 +7803,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.19.0" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "oo7" @@ -7680,7 +7815,7 @@ checksum = "8fc6ce4692fbfd044ce22ca07dcab1a30fa12432ca2aa5b1294eca50d3332a24" dependencies = [ "aes", "async-fs 2.1.2", - "async-io 2.3.4", + "async-io 2.4.0", "async-lock 3.4.0", "async-net 2.0.0", "blocking", @@ -7688,7 +7823,7 @@ dependencies = [ "cipher", "digest", "endi", - "futures-lite 2.3.0", + "futures-lite 2.5.0", "futures-util", "hkdf", "hmac", @@ -7713,9 +7848,9 @@ checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" [[package]] name = "open" -version = "5.3.0" +version = "5.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61a877bf6abd716642a53ef1b89fb498923a4afca5c754f9050b4d081c05c4b3" +checksum = "3ecd52f0b8d15c40ce4820aa251ed5de032e5d91fab27f7db2f40d42a8bdf69c" dependencies = [ "is-wsl", "libc", @@ -7727,7 +7862,7 @@ name = "open_ai" version = "0.1.0" dependencies = [ "anyhow", - "futures 0.3.30", + "futures 0.3.31", "http_client", "schemars", "serde", @@ -7749,9 +7884,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.66" +version = "0.10.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" dependencies = [ "bitflags 2.6.0", "cfg-if", @@ -7781,18 +7916,18 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "300.3.1+3.3.1" +version = "300.4.0+3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7259953d42a81bf137fbbd73bd30a8e1914d6dce43c2b90ed575783a22608b91" +checksum = "a709e02f2b4aca747929cca5ed248880847c650233cf8b8cdc48f40aaf4898a6" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.103" +version = "0.9.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" dependencies = [ "cc", "libc", @@ -7978,7 +8113,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9981e32fb75e004cc148f5fb70342f393830e0a4aa62e3cc93b50976218d42b6" dependencies = [ - "futures 0.3.30", + "futures 0.3.31", "libc", "log", "rand 0.7.3", @@ -8010,7 +8145,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.3", + "redox_syscall 0.5.7", "smallvec", "windows-targets 0.52.6", ] @@ -8110,20 +8245,20 @@ dependencies = [ [[package]] name = "pest" -version = "2.7.11" +version = "2.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd53dff83f26735fdc1ca837098ccf133605d794cdae66acfc2bfac3ec809d95" +checksum = "879952a81a83930934cbf1786752d6dedc3b1f29e8f8fb2ad1d0a36f377cf442" dependencies = [ "memchr", - "thiserror", + "thiserror 1.0.69", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.7.11" +version = "2.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a548d2beca6773b1c244554d36fcf8548a8a58e74156968211567250e48e49a" +checksum = "d214365f632b123a47fd913301e14c946c61d1c183ee245fa76eb752e59a02dd" dependencies = [ "pest", "pest_generator", @@ -8131,9 +8266,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.11" +version = "2.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c93a82e8d145725dcbaf44e5ea887c8a869efdcc28706df2d08c69e17077183" +checksum = "eb55586734301717aea2ac313f50b2eb8f60d2fc3dc01d190eefa2e625f60c4e" dependencies = [ "pest", "pest_meta", @@ -8144,9 +8279,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.7.11" +version = "2.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a941429fea7e08bedec25e4f6785b6ffaacc6b755da98df5ef3e7dcf4a124c4f" +checksum = "b75da2a70cf4d9cb76833c990ac9cd3923c9a8905a8929789ce347c84564d03d" dependencies = [ "once_cell", "pest", @@ -8520,7 +8655,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap 2.4.0", + "indexmap 2.6.0", ] [[package]] @@ -8618,18 +8753,18 @@ checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" [[package]] name = "pin-project" -version = "1.1.5" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.5" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" dependencies = [ "proc-macro2", "quote", @@ -8638,9 +8773,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" [[package]] name = "pin-utils" @@ -8655,7 +8790,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" dependencies = [ "atomic-waker", - "fastrand 2.1.1", + "fastrand 2.2.0", "futures-io", ] @@ -8692,9 +8827,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "plist" @@ -8703,7 +8838,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" dependencies = [ "base64 0.22.1", - "indexmap 2.4.0", + "indexmap 2.6.0", "quick-xml 0.32.0", "serde", "time", @@ -8711,9 +8846,9 @@ dependencies = [ [[package]] name = "plotters" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a15b6eccb8484002195a3e44fe65a4ce8e93a625797a063735536fd59cb01cf3" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" dependencies = [ "num-traits", "plotters-backend", @@ -8724,30 +8859,30 @@ dependencies = [ [[package]] name = "plotters-backend" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "414cec62c6634ae900ea1c56128dfe87cf63e7caece0852ec76aba307cebadb7" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" [[package]] name = "plotters-svg" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81b30686a7d9c3e010b84284bdd26a29f2138574f52f5eb6f794fc0ad924e705" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" dependencies = [ "plotters-backend", ] [[package]] name = "png" -version = "0.17.13" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" +checksum = "52f9d46a34a05a6a57566bc2bfae066ef07585a6e3fa30fbbdff5936380623f0" dependencies = [ "bitflags 1.3.2", "crc32fast", "fdeflate", "flate2", - "miniz_oxide 0.7.4", + "miniz_oxide", ] [[package]] @@ -8768,15 +8903,15 @@ dependencies = [ [[package]] name = "polling" -version = "3.7.3" +version = "3.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" +checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" dependencies = [ "cfg-if", "concurrent-queue", "hermit-abi 0.4.0", "pin-project-lite", - "rustix 0.38.35", + "rustix 0.38.40", "tracing", "windows-sys 0.59.0", ] @@ -8795,13 +8930,13 @@ checksum = "af3fb618632874fb76937c2361a7f22afd393c982a2165595407edc75b06d3c1" dependencies = [ "atomic", "crossbeam-queue", - "futures 0.3.30", + "futures 0.3.31", "log", "parking_lot", "pin-project", "pollster", "static_assertions", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -8868,9 +9003,9 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.22" +version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" +checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033" dependencies = [ "proc-macro2", "syn 2.0.87", @@ -8882,7 +9017,7 @@ version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" dependencies = [ - "toml_edit 0.22.20", + "toml_edit 0.22.22", ] [[package]] @@ -8909,6 +9044,28 @@ dependencies = [ "version_check", ] +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "proc-macro2" version = "1.0.89" @@ -8950,7 +9107,7 @@ dependencies = [ "env_logger 0.11.5", "fancy-regex 0.14.0", "fs", - "futures 0.3.30", + "futures 0.3.31", "fuzzy", "git", "git2", @@ -9034,7 +9191,7 @@ version = "0.1.0" dependencies = [ "anyhow", "editor", - "futures 0.3.30", + "futures 0.3.31", "fuzzy", "gpui", "language", @@ -9062,7 +9219,7 @@ dependencies = [ "memchr", "parking_lot", "protobuf", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -9071,7 +9228,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "444879275cb4fd84958b1a1d5420d15e6fcf7c235fe47f053c9c2a80aceb6001" dependencies = [ - "bytes 1.7.2", + "bytes 1.8.0", "prost-derive", ] @@ -9081,7 +9238,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62941722fb675d463659e49c4f3fe1fe792ff24fe5bbaa9c08cd3b98a1c354f5" dependencies = [ - "bytes 1.7.2", + "bytes 1.8.0", "heck 0.3.3", "itertools 0.10.5", "lazy_static", @@ -9114,7 +9271,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "534b7a0e836e3c482d2693070f982e39e7611da9695d4d1f5a4b186b51faef0a" dependencies = [ - "bytes 1.7.2", + "bytes 1.8.0", "prost", ] @@ -9137,9 +9294,9 @@ checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" [[package]] name = "psm" -version = "0.1.21" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5787f7cda34e3033a72192c018bc5883100330f362ef279a8cbccfce8bb4e874" +checksum = "200b9ff220857e53e184257720a14553b2f4aa02577d2ed9842d45d4b9654810" dependencies = [ "cc", ] @@ -9219,9 +9376,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.34.0" +version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f24d770aeca0eacb81ac29dfbc55ebcc09312fdd1f8bbecdc7e4a84e000e3b4" +checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" dependencies = [ "memchr", ] @@ -9246,45 +9403,49 @@ dependencies = [ [[package]] name = "quinn" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c7c5fdde3cdae7203427dc4f0a68fe0ed09833edc525a03456b153b79828684" +checksum = "62e96808277ec6f97351a2380e6c25114bc9e67037775464979f3037c92d05ef" dependencies = [ - "bytes 1.7.2", + "bytes 1.8.0", "pin-project-lite", "quinn-proto", "quinn-udp", "rustc-hash 2.0.0", - "rustls 0.23.13", + "rustls 0.23.16", "socket2 0.5.7", - "thiserror", + "thiserror 2.0.3", "tokio", "tracing", ] [[package]] name = "quinn-proto" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fadfaed2cd7f389d0161bb73eeb07b7b78f8691047a6f3e73caaeae55310a4a6" +checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d" dependencies = [ - "bytes 1.7.2", + "bytes 1.8.0", + "getrandom 0.2.15", "rand 0.8.5", "ring", "rustc-hash 2.0.0", - "rustls 0.23.13", + "rustls 0.23.16", + "rustls-pki-types", "slab", - "thiserror", + "thiserror 2.0.3", "tinyvec", "tracing", + "web-time", ] [[package]] name = "quinn-udp" -version = "0.5.5" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fe68c2e9e1a1234e218683dbdf9f9dfcb094113c5ac2b938dfcb9bab4c4140b" +checksum = "7d5a626c6807713b15cac82a6acaccd6043c9a5408c24baae07611fec3f243da" dependencies = [ + "cfg_aliases 0.2.1", "libc", "once_cell", "socket2 0.5.7", @@ -9414,22 +9575,23 @@ dependencies = [ "rand_chacha 0.3.1", "simd_helpers", "system-deps", - "thiserror", + "thiserror 1.0.69", "v_frame", "wasm-bindgen", ] [[package]] name = "ravif" -version = "0.11.10" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f0bfd976333248de2078d350bfdf182ff96e168a24d23d2436cef320dd4bdd" +checksum = "2413fd96bd0ea5cdeeb37eaf446a22e6ed7b981d792828721e74ded1980a45c6" dependencies = [ "avif-serialize", "imgref", "loop9", "quick-error", "rav1e", + "rayon", "rgb", ] @@ -9473,9 +9635,9 @@ dependencies = [ [[package]] name = "read-fonts" -version = "0.20.0" +version = "0.22.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c141b9980e1150201b2a3a32879001c8f975fe313ec3df5471a9b5c79a880cd" +checksum = "4a04b892cb6f91951f144c33321843790c8574c825aafdb16d815fd7183b5229" dependencies = [ "bytemuck", "font-types", @@ -9489,7 +9651,7 @@ dependencies = [ "auto_update", "editor", "file_finder", - "futures 0.3.30", + "futures 0.3.31", "fuzzy", "gpui", "itertools 0.13.0", @@ -9526,18 +9688,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.4.1" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_syscall" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" dependencies = [ "bitflags 2.6.0", ] @@ -9550,7 +9703,7 @@ checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ "getrandom 0.2.15", "libredox", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -9575,14 +9728,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.6" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.7", - "regex-syntax 0.8.4", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", ] [[package]] @@ -9596,13 +9749,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.4", + "regex-syntax 0.8.5", ] [[package]] @@ -9619,9 +9772,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "release_channel" @@ -9639,7 +9792,7 @@ dependencies = [ "async-trait", "collections", "fs", - "futures 0.3.30", + "futures 0.3.31", "gpui", "itertools 0.13.0", "log", @@ -9653,7 +9806,7 @@ dependencies = [ "shlex", "smol", "tempfile", - "thiserror", + "thiserror 1.0.69", "util", "which 6.0.3", ] @@ -9673,7 +9826,7 @@ dependencies = [ "env_logger 0.11.5", "fork", "fs", - "futures 0.3.30", + "futures 0.3.31", "git", "git_hosting_providers", "gpui", @@ -9727,11 +9880,13 @@ dependencies = [ "editor", "env_logger 0.11.5", "feature_flags", - "futures 0.3.30", + "futures 0.3.31", "gpui", "http_client", "image", "indoc", + "jupyter-protocol", + "jupyter-websocket-client", "language", "languages", "log", @@ -9767,7 +9922,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" dependencies = [ "base64 0.21.7", - "bytes 1.7.2", + "bytes 1.8.0", "encoding_rs", "futures-core", "futures-util", @@ -9806,7 +9961,7 @@ version = "0.12.8" source = "git+https://github.com/zed-industries/reqwest.git?rev=fd110f6998da16bbca97b6dddda9be7827c50e29#fd110f6998da16bbca97b6dddda9be7827c50e29" dependencies = [ "base64 0.22.1", - "bytes 1.7.2", + "bytes 1.8.0", "encoding_rs", "futures-core", "futures-util", @@ -9814,7 +9969,7 @@ dependencies = [ "http 1.1.0", "http-body 1.0.1", "http-body-util", - "hyper 1.4.1", + "hyper 1.5.0", "hyper-rustls 0.27.3", "hyper-util", "ipnet", @@ -9825,9 +9980,9 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.13", + "rustls 0.23.16", "rustls-native-certs 0.8.0", - "rustls-pemfile 2.1.3", + "rustls-pemfile 2.2.0", "rustls-pki-types", "serde", "serde_json", @@ -9852,8 +10007,8 @@ name = "reqwest_client" version = "0.1.0" dependencies = [ "anyhow", - "bytes 1.7.2", - "futures 0.3.30", + "bytes 1.8.0", + "futures 0.3.31", "gpui", "http_client", "log", @@ -9890,9 +10045,9 @@ dependencies = [ [[package]] name = "rgb" -version = "0.8.49" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cd5a1e95672f201913966f39baf355b53b5d92833431847295ae0346a5b939" +checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" dependencies = [ "bytemuck", ] @@ -9901,7 +10056,7 @@ dependencies = [ name = "rich_text" version = "0.1.0" dependencies = [ - "futures 0.3.30", + "futures 0.3.31", "gpui", "language", "linkify", @@ -9934,7 +10089,7 @@ checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" dependencies = [ "bitvec", "bytecheck", - "bytes 1.7.2", + "bytes 1.8.0", "hashbrown 0.12.3", "ptr_meta", "rend", @@ -9984,7 +10139,7 @@ checksum = "6006a627c1a38d37f3d3a85c6575418cfe34a5392d60a686d0071e1c8d427acb" dependencies = [ "cpal", "hound", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -10016,12 +10171,12 @@ name = "rpc" version = "0.1.0" dependencies = [ "anyhow", - "async-tungstenite", + "async-tungstenite 0.28.0", "base64 0.22.1", "chrono", "collections", "env_logger 0.11.5", - "futures 0.3.30", + "futures 0.3.31", "gpui", "parking_lot", "proto", @@ -10058,22 +10213,22 @@ dependencies = [ [[package]] name = "runtimelib" -version = "0.19.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe23ba9967355bbb1be2fb9a8e51bd239ffdf9c791fad5a9b765122ee2bde2e4" +checksum = "2db079f82c110e25c3202d20c7cd29dcbfa93d96de7c5bb8bb6f294f477567cf" dependencies = [ "anyhow", "async-dispatcher", "async-std", "base64 0.22.1", - "bytes 1.7.2", + "bytes 1.8.0", "chrono", "data-encoding", "dirs 5.0.1", - "futures 0.3.30", + "futures 0.3.31", "glob", + "jupyter-protocol", "jupyter-serde", - "rand 0.8.5", "ring", "serde", "serde_json", @@ -10126,7 +10281,7 @@ checksum = "b082d80e3e3cc52b2ed634388d436fe1f4de6af5786cc2de9ba9737527bdf555" dependencies = [ "arrayvec", "borsh", - "bytes 1.7.2", + "bytes 1.8.0", "num-traits", "rand 0.8.5", "rkyv", @@ -10177,9 +10332,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.35" +version = "0.38.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a85d50532239da68e9addb745ba38ff4612a242c1c7ceea689c4bc7c2f43c36f" +checksum = "99e4ea3e1cdc4b559b8e5650f9c8e5998e3e5c1343b4eaf034565f32318d63c0" dependencies = [ "bitflags 2.6.0", "errno 0.3.9", @@ -10198,7 +10353,7 @@ checksum = "a25c3aad9fc1424eb82c88087789a7d938e1829724f3e4043163baf0d13cfc12" dependencies = [ "errno 0.3.9", "libc", - "rustix 0.38.35", + "rustix 0.38.40", ] [[package]] @@ -10215,9 +10370,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.13" +version = "0.23.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2dabaac7466917e566adb06783a81ca48944c6898a1b08b9374106dd671f4c8" +checksum = "eee87ff5d9b36712a58574e12e9f0ea80f915a5b0ac518d322b24a465617925e" dependencies = [ "once_cell", "ring", @@ -10246,7 +10401,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcaf18a4f2be7326cd874a5fa579fae794320a0f388d365dca7e480e55f83f8a" dependencies = [ "openssl-probe", - "rustls-pemfile 2.1.3", + "rustls-pemfile 2.2.0", "rustls-pki-types", "schannel", "security-framework", @@ -10263,19 +10418,21 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "2.1.3" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" dependencies = [ - "base64 0.22.1", "rustls-pki-types", ] [[package]] name = "rustls-pki-types" -version = "1.8.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" +checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" +dependencies = [ + "web-time", +] [[package]] name = "rustls-webpki" @@ -10300,9 +10457,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" [[package]] name = "rustybuzz" @@ -10347,11 +10504,11 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.23" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -10415,12 +10572,12 @@ dependencies = [ [[package]] name = "sea-bae" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bd3534a9978d0aa7edd2808dc1f8f31c4d0ecd31ddf71d997b3c98e9f3c9114" +checksum = "f694a6ab48f14bc063cfadff30ab551d3c7e46d8f81836c51989d548f44a2a25" dependencies = [ "heck 0.4.1", - "proc-macro-error", + "proc-macro-error2", "proc-macro2", "quote", "syn 2.0.87", @@ -10436,7 +10593,7 @@ dependencies = [ "async-trait", "bigdecimal", "chrono", - "futures 0.3.30", + "futures 0.3.31", "log", "ouroboros", "rust_decimal", @@ -10447,7 +10604,7 @@ dependencies = [ "serde_json", "sqlx", "strum 0.26.3", - "thiserror", + "thiserror 1.0.69", "time", "tracing", "url", @@ -10456,9 +10613,9 @@ dependencies = [ [[package]] name = "sea-orm-macros" -version = "1.1.0-rc.1" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07aadcb2ee9fad78a3bf74f6430ba94865ab4d8ad237f978e99dafa97ee0df57" +checksum = "3a239e3bb1b566ad4ec2654d0d193d6ceddfd733487edc9c21a64d214c773910" dependencies = [ "heck 0.4.1", "proc-macro2", @@ -10470,13 +10627,12 @@ dependencies = [ [[package]] name = "sea-query" -version = "0.32.0-rc.1" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fba498acd58ce434669f273505cd07737065472eb541c3f813c7f4ce33993f5" +checksum = "ff504d13b5e4b52fffcf2fb203d0352a5722fa5151696db768933e41e1e591bb" dependencies = [ "bigdecimal", "chrono", - "educe", "inherent", "ordered-float 3.9.2", "rust_decimal", @@ -10487,9 +10643,9 @@ dependencies = [ [[package]] name = "sea-query-binder" -version = "0.7.0-rc.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edc3296903e60ddc7c9f4601cd6ef31a4b1584bf22480587e00b9ef743071b57" +checksum = "b0019f47430f7995af63deda77e238c17323359af241233ec768aba1faea7608" dependencies = [ "bigdecimal", "chrono", @@ -10529,7 +10685,7 @@ dependencies = [ "client", "collections", "editor", - "futures 0.3.30", + "futures 0.3.31", "gpui", "language", "menu", @@ -10574,9 +10730,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.11.1" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" +checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2" dependencies = [ "core-foundation-sys", "libc", @@ -10601,7 +10757,7 @@ dependencies = [ "env_logger 0.11.5", "feature_flags", "fs", - "futures 0.3.30", + "futures 0.3.31", "futures-batch", "gpui", "heed", @@ -10648,18 +10804,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.209" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.209" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" dependencies = [ "proc-macro2", "quote", @@ -10703,7 +10859,7 @@ version = "1.0.132" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" dependencies = [ - "indexmap 2.4.0", + "indexmap 2.6.0", "itoa", "memchr", "ryu", @@ -10712,12 +10868,13 @@ dependencies = [ [[package]] name = "serde_json_lenient" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d0bae483150302560d7cb52e7932f39b69a6fbdd099e48d33ef060a8c9c078" +checksum = "2bf0c7e21364d0e199dd2f6c339ca18d6fca75b69458a247e8b27ff1c92f5b86" dependencies = [ - "indexmap 2.4.0", + "indexmap 2.6.0", "itoa", + "memchr", "ryu", "serde", ] @@ -10740,7 +10897,7 @@ checksum = "c7715380eec75f029a4ef7de39a9200e0a63823176b759d055b613f5a87df6a6" dependencies = [ "percent-encoding", "serde", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -10751,7 +10908,7 @@ checksum = "8cac3f1e2ca2fe333923a1ae72caca910b98ed0630bb35ef6f8c8517d6e81afa" dependencies = [ "percent-encoding", "serde", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -10767,9 +10924,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.7" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" dependencies = [ "serde", ] @@ -10805,7 +10962,7 @@ dependencies = [ "collections", "ec4rs", "fs", - "futures 0.3.30", + "futures 0.3.31", "gpui", "indoc", "log", @@ -10962,9 +11119,9 @@ dependencies = [ [[package]] name = "simdutf8" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] name = "similar" @@ -10980,7 +11137,7 @@ checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" dependencies = [ "num-bigint", "num-traits", - "thiserror", + "thiserror 1.0.69", "time", ] @@ -11018,9 +11175,9 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "skrifa" -version = "0.20.0" +version = "0.22.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abea4738067b1e628c6ce28b2c216c19e9ea95715cdb332680e821c3bec2ef23" +checksum = "8e1c44ad1f6c5bdd4eefed8326711b7dbda9ea45dfd36068c427d332aa382cbe" dependencies = [ "bytemuck", "read-fonts", @@ -11118,7 +11275,7 @@ dependencies = [ "anyhow", "collections", "fs", - "futures 0.3.30", + "futures 0.3.31", "gpui", "parking_lot", "paths", @@ -11164,9 +11321,9 @@ dependencies = [ [[package]] name = "spdx" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47317bbaf63785b53861e1ae2d11b80d6b624211d42cb20efcd210ee6f8a14bc" +checksum = "bae30cc7bfe3656d60ee99bf6836f472b0c53dddcbf335e253329abb16e535a2" dependencies = [ "smallvec", ] @@ -11221,7 +11378,7 @@ version = "0.1.0" dependencies = [ "anyhow", "collections", - "futures 0.3.30", + "futures 0.3.31", "indoc", "libsqlite3-sys", "parking_lot", @@ -11273,7 +11430,7 @@ dependencies = [ "atoi", "bigdecimal", "byteorder", - "bytes 1.7.2", + "bytes 1.8.0", "chrono", "crc", "crossbeam-queue", @@ -11287,7 +11444,7 @@ dependencies = [ "hashbrown 0.14.5", "hashlink 0.9.1", "hex", - "indexmap 2.4.0", + "indexmap 2.6.0", "log", "memchr", "once_cell", @@ -11301,7 +11458,7 @@ dependencies = [ "sha2", "smallvec", "sqlformat", - "thiserror", + "thiserror 1.0.69", "time", "tokio", "tokio-stream", @@ -11361,7 +11518,7 @@ dependencies = [ "bigdecimal", "bitflags 2.6.0", "byteorder", - "bytes 1.7.2", + "bytes 1.8.0", "chrono", "crc", "digest", @@ -11390,7 +11547,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 1.0.69", "time", "tracing", "uuid", @@ -11434,7 +11591,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 1.0.69", "time", "tracing", "uuid", @@ -11624,7 +11781,7 @@ dependencies = [ "collections", "editor", "env_logger 0.11.5", - "futures 0.3.30", + "futures 0.3.31", "gpui", "http_client", "language", @@ -11649,7 +11806,7 @@ name = "supermaven_api" version = "0.1.0" dependencies = [ "anyhow", - "futures 0.3.30", + "futures 0.3.31", "http_client", "paths", "serde", @@ -11659,15 +11816,15 @@ dependencies = [ [[package]] name = "sval" -version = "2.13.0" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53eb957fbc79a55306d5d25d87daf3627bc3800681491cda0709eef36c748bfe" +checksum = "f6dc0f9830c49db20e73273ffae9b5240f63c42e515af1da1fceefb69fceafd8" [[package]] name = "sval_buffer" -version = "2.13.0" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96e860aef60e9cbf37888d4953a13445abf523c534640d1f6174d310917c410d" +checksum = "429922f7ad43c0ef8fd7309e14d750e38899e32eb7e8da656ea169dd28ee212f" dependencies = [ "sval", "sval_ref", @@ -11675,18 +11832,18 @@ dependencies = [ [[package]] name = "sval_dynamic" -version = "2.13.0" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea3f2b07929a1127d204ed7cb3905049381708245727680e9139dac317ed556f" +checksum = "68f16ff5d839396c11a30019b659b0976348f3803db0626f736764c473b50ff4" dependencies = [ "sval", ] [[package]] name = "sval_fmt" -version = "2.13.0" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4e188677497de274a1367c4bda15bd2296de4070d91729aac8f0a09c1abf64d" +checksum = "c01c27a80b6151b0557f9ccbe89c11db571dc5f68113690c1e028d7e974bae94" dependencies = [ "itoa", "ryu", @@ -11695,9 +11852,9 @@ dependencies = [ [[package]] name = "sval_json" -version = "2.13.0" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f456c07dae652744781f2245d5e3b78e6a9ebad70790ac11eb15dbdbce5282" +checksum = "0deef63c70da622b2a8069d8600cf4b05396459e665862e7bdb290fd6cf3f155" dependencies = [ "itoa", "ryu", @@ -11706,9 +11863,9 @@ dependencies = [ [[package]] name = "sval_nested" -version = "2.13.0" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "886feb24709f0476baaebbf9ac10671a50163caa7e439d7a7beb7f6d81d0a6fb" +checksum = "a39ce5976ae1feb814c35d290cf7cf8cd4f045782fe1548d6bc32e21f6156e9f" dependencies = [ "sval", "sval_buffer", @@ -11717,18 +11874,18 @@ dependencies = [ [[package]] name = "sval_ref" -version = "2.13.0" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be2e7fc517d778f44f8cb64140afa36010999565528d48985f55e64d45f369ce" +checksum = "bb7c6ee3751795a728bc9316a092023529ffea1783499afbc5c66f5fabebb1fa" dependencies = [ "sval", ] [[package]] name = "sval_serde" -version = "2.13.0" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79bf66549a997ff35cd2114a27ac4b0c2843280f2cfa84b240d169ecaa0add46" +checksum = "2a5572d0321b68109a343634e3a5d576bf131b82180c6c442dee06349dfc652a" dependencies = [ "serde", "sval", @@ -11737,9 +11894,9 @@ dependencies = [ [[package]] name = "svg_fmt" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20e16a0f46cf5fd675563ef54f26e83e20f2366bcf027bcb3cc3ed2b98aaf2ca" +checksum = "ce5d813d71d82c4cbc1742135004e4a79fd870214c155443451c139c9470a0aa" [[package]] name = "svgtypes" @@ -11753,9 +11910,9 @@ dependencies = [ [[package]] name = "swash" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93cdc334a50fcc2aa3f04761af3b28196280a6aaadb1ef11215c478ae32615ac" +checksum = "cbd59f3f359ddd2c95af4758c18270eddd9c730dde98598023cdabff472c2ca2" dependencies = [ "skrifa", "yazi", @@ -11784,18 +11941,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "syn_derive" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1329189c02ff984e9736652b1631330da25eaa6bc639089ed4915d25446cbe7b" -dependencies = [ - "proc-macro-error", - "proc-macro2", - "quote", - "syn 2.0.87", -] - [[package]] name = "sync_wrapper" version = "0.1.2" @@ -11820,6 +11965,17 @@ dependencies = [ "crossbeam-queue", ] +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "sys-locale" version = "0.3.2" @@ -11840,7 +11996,7 @@ dependencies = [ "memchr", "ntapi", "rayon", - "windows 0.54.0", + "windows 0.57.0", ] [[package]] @@ -11909,7 +12065,7 @@ dependencies = [ "cap-std", "fd-lock", "io-lifetimes 2.0.3", - "rustix 0.38.35", + "rustix 0.38.40", "windows-sys 0.52.0", "winx", ] @@ -11974,7 +12130,7 @@ version = "0.1.0" dependencies = [ "anyhow", "collections", - "futures 0.3.30", + "futures 0.3.31", "gpui", "hex", "parking_lot", @@ -12021,14 +12177,14 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.12.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" dependencies = [ "cfg-if", - "fastrand 2.1.1", + "fastrand 2.2.0", "once_cell", - "rustix 0.38.35", + "rustix 0.38.40", "windows-sys 0.59.0", ] @@ -12060,7 +12216,7 @@ dependencies = [ "anyhow", "collections", "dirs 4.0.0", - "futures 0.3.30", + "futures 0.3.31", "gpui", "libc", "rand 0.8.5", @@ -12074,7 +12230,7 @@ dependencies = [ "sysinfo", "task", "theme", - "thiserror", + "thiserror 1.0.69", "util", "windows 0.58.0", ] @@ -12085,7 +12241,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f599bd7ca042cfdf8f4512b277c02ba102247820f9d9d4a9f521f496751a6ef" dependencies = [ - "rustix 0.38.35", + "rustix 0.38.40", "windows-sys 0.59.0", ] @@ -12099,7 +12255,7 @@ dependencies = [ "db", "dirs 4.0.0", "editor", - "futures 0.3.30", + "futures 0.3.31", "gpui", "itertools 0.13.0", "language", @@ -12151,7 +12307,7 @@ dependencies = [ "collections", "derive_more", "fs", - "futures 0.3.30", + "futures 0.3.31", "gpui", "indexmap 1.9.3", "log", @@ -12215,7 +12371,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa" +dependencies = [ + "thiserror-impl 2.0.3", ] [[package]] @@ -12229,6 +12394,17 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "thiserror-impl" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "thread_local" version = "1.1.8" @@ -12356,6 +12532,16 @@ dependencies = [ "url", ] +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinytemplate" version = "1.2.1" @@ -12419,12 +12605,12 @@ dependencies = [ [[package]] name = "tokio" -version = "1.40.0" +version = "1.41.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" dependencies = [ "backtrace", - "bytes 1.7.2", + "bytes 1.8.0", "libc", "mio 1.0.2", "parking_lot", @@ -12483,7 +12669,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ - "rustls 0.23.13", + "rustls 0.23.16", "rustls-pki-types", "tokio", ] @@ -12497,15 +12683,15 @@ dependencies = [ "either", "futures-io", "futures-util", - "thiserror", + "thiserror 1.0.69", "tokio", ] [[package]] name = "tokio-stream" -version = "0.1.15" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" dependencies = [ "futures-core", "pin-project-lite", @@ -12542,7 +12728,7 @@ version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" dependencies = [ - "bytes 1.7.2", + "bytes 1.8.0", "futures-core", "futures-io", "futures-sink", @@ -12580,7 +12766,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.20", + "toml_edit 0.22.22", ] [[package]] @@ -12598,7 +12784,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.4.0", + "indexmap 2.6.0", "serde", "serde_spanned", "toml_datetime", @@ -12607,15 +12793,15 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.20" +version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ - "indexmap 2.4.0", + "indexmap 2.6.0", "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.18", + "winnow 0.6.20", ] [[package]] @@ -12662,7 +12848,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f873044bf02dd1e8239e9c1293ea39dad76dc594ec16185d0a1bf31d8dc8d858" dependencies = [ "bitflags 1.3.2", - "bytes 1.7.2", + "bytes 1.8.0", "futures-core", "futures-util", "http 0.2.12", @@ -12680,7 +12866,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140" dependencies = [ "bitflags 2.6.0", - "bytes 1.7.2", + "bytes 1.8.0", "futures-core", "futures-util", "http 0.2.12", @@ -12781,22 +12967,22 @@ dependencies = [ [[package]] name = "tree-sitter" -version = "0.23.0" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20f4cd3642c47a85052a887d86704f4eac272969f61b686bdd3f772122aabaff" +checksum = "0203df02a3b6dd63575cc1d6e609edc2181c9a11867a271b25cfd2abff3ec5ca" dependencies = [ "cc", "regex", - "regex-syntax 0.8.4", + "regex-syntax 0.8.5", "tree-sitter-language", "wasmtime-c-api-impl", ] [[package]] name = "tree-sitter-bash" -version = "0.23.1" +version = "0.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3aa5e1c6bd02c0053f3f68edcf5d8866b38a8640584279e30fca88149ce14dda" +checksum = "329a4d48623ac337d42b1df84e81a1c9dbb2946907c102ca72db158c1964a52e" dependencies = [ "cc", "tree-sitter-language", @@ -12814,9 +13000,9 @@ dependencies = [ [[package]] name = "tree-sitter-cpp" -version = "0.23.1" +version = "0.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d67e862242878d6ee50e1e5814f267ee3eea0168aea2cdbd700ccfb4c74b6d3" +checksum = "df2196ea9d47b4ab4a31b9297eaa5a5d19a0b121dceb9f118f6790ad0ab94743" dependencies = [ "cc", "tree-sitter-language", @@ -12824,9 +13010,9 @@ dependencies = [ [[package]] name = "tree-sitter-css" -version = "0.23.0" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d0018d6b1692a806f9cddaa1e5616951fd58840c39a0b21401b55ab3df12292" +checksum = "25435a275adb3226b6fddab891bbc50d1a500774a44ceb97022a39666ccda75d" dependencies = [ "cc", "tree-sitter-language", @@ -12854,9 +13040,9 @@ dependencies = [ [[package]] name = "tree-sitter-embedded-template" -version = "0.23.0" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9644d7586ebe850c84037ee2f4804dda4a9348eef053be6b1e0d7712342a2495" +checksum = "790063ef14e5b67556abc0b3be0ed863fb41d65ee791cf8c0b20eb42a1fa46af" dependencies = [ "cc", "tree-sitter-language", @@ -12864,9 +13050,9 @@ dependencies = [ [[package]] name = "tree-sitter-go" -version = "0.23.1" +version = "0.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf57626e4c9b6d6efaf8a8d5ee1241c5f178ae7bfdf693713ae6a774f01424e" +checksum = "dc4ee804a89f5c0e606b0b20579c86afc7cd0174aebd45c33b6b9c6237bcd97d" dependencies = [ "cc", "tree-sitter-language", @@ -12911,9 +13097,9 @@ dependencies = [ [[package]] name = "tree-sitter-jsdoc" -version = "0.23.0" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8c4049eb0ad690e34e5f63640f75ce12a2ff8ba18344d0a13926805b139c0c8" +checksum = "a3862dfcb1038fc5e7812d7df14190afdeb7e1415288fd5f51f58395f8cb0faf" dependencies = [ "cc", "tree-sitter-language", @@ -12931,9 +13117,9 @@ dependencies = [ [[package]] name = "tree-sitter-language" -version = "0.1.0" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2545046bd1473dac6c626659cc2567c6c0ff302fc8b84a56c4243378276f7f57" +checksum = "e8ddffe35a0e5eeeadf13ff7350af564c6e73993a24db62caee1822b185c2600" [[package]] name = "tree-sitter-md" @@ -12946,9 +13132,9 @@ dependencies = [ [[package]] name = "tree-sitter-python" -version = "0.23.2" +version = "0.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65661b1a3e24139e2e54207e47d910ab07e28790d78efc7d5dc3a11ce2a110eb" +checksum = "2416de7eea3f2e1bd53c250f2d3f3394fc77f78497680f37f4b87918b8d752e3" dependencies = [ "cc", "tree-sitter-language", @@ -12966,9 +13152,9 @@ dependencies = [ [[package]] name = "tree-sitter-ruby" -version = "0.23.0" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ec5ee842e27791e0adffa0b2a177614de51d2a26e5c7e84d014ed7f097e5ed0" +checksum = "be0484ea4ef6bb9c575b4fdabde7e31340a8d2dbc7d52b321ac83da703249f95" dependencies = [ "cc", "tree-sitter-language", @@ -12976,9 +13162,9 @@ dependencies = [ [[package]] name = "tree-sitter-rust" -version = "0.23.0" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cffbbcb780348fbae8395742ae5b34c1fd794e4085d43aac9f259387f9a84dc8" +checksum = "137ff3de3cc8a98302d048963459ead91135d4a1b423f09d25028b847ec3d3e3" dependencies = [ "cc", "tree-sitter-language", @@ -12986,9 +13172,9 @@ dependencies = [ [[package]] name = "tree-sitter-typescript" -version = "0.23.0" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aecf1585ae2a9dddc2b1d4c0e2140b2ec9876e2a25fd79de47fcf7dae0384685" +checksum = "6c5f76ed8d947a75cc446d5fccd8b602ebf0cde64ccf2ffa434d873d7a575eff" dependencies = [ "cc", "tree-sitter-language", @@ -13017,19 +13203,38 @@ checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8" [[package]] name = "tungstenite" -version = "0.20.1" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" +checksum = "15fba1a6d6bb030745759a9a2a588bfe8490fc8b4751a277db3a0be1c9ebbf67" dependencies = [ "byteorder", - "bytes 1.7.2", + "bytes 1.8.0", "data-encoding", "http 0.2.12", "httparse", "log", "rand 0.8.5", "sha1", - "thiserror", + "thiserror 1.0.69", + "url", + "utf-8", +] + +[[package]] +name = "tungstenite" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" +dependencies = [ + "byteorder", + "bytes 1.8.0", + "data-encoding", + "http 0.2.12", + "httparse", + "log", + "rand 0.8.5", + "sha1", + "thiserror 1.0.69", "url", "utf-8", ] @@ -13041,14 +13246,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" dependencies = [ "byteorder", - "bytes 1.7.2", + "bytes 1.8.0", "data-encoding", "http 1.1.0", "httparse", "log", "rand 0.8.5", "sha1", - "thiserror", + "thiserror 1.0.69", "url", "utf-8", ] @@ -13060,14 +13265,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" dependencies = [ "byteorder", - "bytes 1.7.2", + "bytes 1.8.0", "data-encoding", "http 1.1.0", "httparse", "log", "rand 0.8.5", "sha1", - "thiserror", + "thiserror 1.0.69", "utf-8", ] @@ -13085,9 +13290,9 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "ucd-trie" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "uds_windows" @@ -13141,18 +13346,15 @@ dependencies = [ [[package]] name = "unicase" -version = "2.7.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" -dependencies = [ - "version_check", -] +checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" [[package]] name = "unicode-bidi" -version = "0.3.15" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" +checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" [[package]] name = "unicode-bidi-mirroring" @@ -13168,9 +13370,9 @@ checksum = "1df77b101bcc4ea3d78dafc5ad7e4f58ceffe0b2b16bf446aeb50b6cb4157656" [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" [[package]] name = "unicode-linebreak" @@ -13180,18 +13382,18 @@ checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" [[package]] name = "unicode-normalization" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" dependencies = [ "tinyvec", ] [[package]] name = "unicode-properties" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ea75f83c0137a9b98608359a5f1af8144876eb67bcb1ce837368e906a9f524" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" [[package]] name = "unicode-script" @@ -13201,21 +13403,21 @@ checksum = "9fb421b350c9aff471779e262955939f565ec18b86c15364e6bdf0d662ca7c1f" [[package]] name = "unicode-segmentation" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-width" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-xid" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229730647fbc343e3a80e463c1db7f78f3855d3f3739bee0dda773c9a037c90a" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "unicode_categories" @@ -13237,9 +13439,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.2" +version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada" dependencies = [ "form_urlencoded", "idna", @@ -13281,6 +13483,18 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -13295,7 +13509,7 @@ dependencies = [ "async-fs 1.6.0", "collections", "dirs 4.0.0", - "futures 0.3.30", + "futures 0.3.31", "futures-lite 1.13.0", "git2", "globset", @@ -13313,9 +13527,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" dependencies = [ "getrandom 0.2.15", "serde", @@ -13341,9 +13555,9 @@ checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] name = "value-bag" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a84c137d37ab0142f0f2ddfe332651fdbf252e7b7dbb4e67b6c1f1b2e925101" +checksum = "3ef4c4aa54d5d05a279399bfa921ec387b7aba77caf7a682ae8d86785b8fdad2" dependencies = [ "value-bag-serde1", "value-bag-sval2", @@ -13351,9 +13565,9 @@ dependencies = [ [[package]] name = "value-bag-serde1" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccacf50c5cb077a9abb723c5bcb5e0754c1a433f1e1de89edc328e2760b6328b" +checksum = "4bb773bd36fd59c7ca6e336c94454d9c66386416734817927ac93d81cb3c5b0b" dependencies = [ "erased-serde", "serde", @@ -13362,9 +13576,9 @@ dependencies = [ [[package]] name = "value-bag-sval2" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1785bae486022dfb9703915d42287dcb284c1ee37bd1080eeba78cc04721285b" +checksum = "53a916a702cac43a88694c97657d449775667bcd14b70419441d05b7fea4a83a" dependencies = [ "sval", "sval_buffer", @@ -13419,7 +13633,7 @@ dependencies = [ "command_palette", "command_palette_hooks", "editor", - "futures 0.3.30", + "futures 0.3.31", "gpui", "indoc", "itertools 0.13.0", @@ -13534,7 +13748,7 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4378d202ff965b011c64817db11d5829506d3404edeadb61f190d111da3f231c" dependencies = [ - "bytes 1.7.2", + "bytes 1.8.0", "futures-channel", "futures-util", "headers", @@ -13576,9 +13790,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" dependencies = [ "cfg-if", "once_cell", @@ -13587,9 +13801,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" dependencies = [ "bumpalo", "log", @@ -13602,9 +13816,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.43" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" +checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" dependencies = [ "cfg-if", "js-sys", @@ -13614,9 +13828,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -13624,9 +13838,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", @@ -13637,9 +13851,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" [[package]] name = "wasm-encoder" @@ -13666,7 +13880,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fd83062c17b9f4985d438603cde0a5e8c5c8198201a6937f778b607924c7da2" dependencies = [ "anyhow", - "indexmap 2.4.0", + "indexmap 2.6.0", "serde", "serde_derive", "serde_json", @@ -13677,9 +13891,9 @@ dependencies = [ [[package]] name = "wasm-streams" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e072d4e72f700fb3443d8fe94a39315df013eef1104903cdb0a2abd322bbecd" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" dependencies = [ "futures-util", "js-sys", @@ -13695,7 +13909,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84e5df6dba6c0d7fafc63a450f1738451ed7a0b52295d83e868218fa286bf708" dependencies = [ "bitflags 2.6.0", - "indexmap 2.4.0", + "indexmap 2.6.0", "semver", ] @@ -13708,7 +13922,7 @@ dependencies = [ "ahash 0.8.11", "bitflags 2.6.0", "hashbrown 0.14.5", - "indexmap 2.4.0", + "indexmap 2.6.0", "semver", "serde", ] @@ -13738,7 +13952,7 @@ dependencies = [ "cfg-if", "encoding_rs", "hashbrown 0.14.5", - "indexmap 2.4.0", + "indexmap 2.6.0", "libc", "libm", "log", @@ -13749,7 +13963,7 @@ dependencies = [ "paste", "postcard", "psm", - "rustix 0.38.35", + "rustix 0.38.40", "semver", "serde", "serde_derive", @@ -13781,9 +13995,9 @@ dependencies = [ [[package]] name = "wasmtime-c-api-impl" -version = "24.0.0" +version = "24.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "765e302e7d9125e614aaeec3ad6b6083605393004eca00214106a4ff6b47fc58" +checksum = "4e038dd412700174019867608617127e7cc4f113f764dd10e7488dbf5f47b191" dependencies = [ "anyhow", "log", @@ -13795,9 +14009,9 @@ dependencies = [ [[package]] name = "wasmtime-c-api-macros" -version = "24.0.0" +version = "24.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d09d02eaa84aa2de5babee7b0296557ad6e4903bb10aa8d135e393e753a43d6" +checksum = "bde0ca2263811d980ab676bcb2a190c990737f58969a908976101ad208149a17" dependencies = [ "proc-macro2", "quote", @@ -13842,7 +14056,7 @@ dependencies = [ "log", "object", "target-lexicon", - "thiserror", + "thiserror 1.0.69", "wasmparser 0.215.0", "wasmtime-environ", "wasmtime-versioned-export-macros", @@ -13859,7 +14073,7 @@ dependencies = [ "cranelift-bitset", "cranelift-entity", "gimli 0.29.0", - "indexmap 2.4.0", + "indexmap 2.6.0", "log", "object", "postcard", @@ -13884,7 +14098,7 @@ dependencies = [ "anyhow", "cc", "cfg-if", - "rustix 0.38.35", + "rustix 0.38.40", "wasmtime-asm-macros", "wasmtime-versioned-export-macros", "windows-sys 0.52.0", @@ -13935,27 +14149,27 @@ dependencies = [ [[package]] name = "wasmtime-wasi" -version = "24.0.1" +version = "24.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda03f5bfd5c4cc09f75c7e44846663f25f2c48a2d688fbfb5c7a33af6cf34f5" +checksum = "f88f94e393084426f5055d57ce7ae6346ae623783ee6792f411282d6b9e1e5c3" dependencies = [ "anyhow", "async-trait", "bitflags 2.6.0", - "bytes 1.7.2", + "bytes 1.8.0", "cap-fs-ext", "cap-net-ext", "cap-rand", "cap-std", "cap-time-ext", "fs-set-times", - "futures 0.3.30", + "futures 0.3.31", "io-extras", "io-lifetimes 2.0.3", "once_cell", - "rustix 0.38.35", + "rustix 0.38.40", "system-interface", - "thiserror", + "thiserror 1.0.69", "tokio", "tracing", "url", @@ -13989,7 +14203,7 @@ checksum = "c58b085b2d330e5057dddd31f3ca527569b90fcdd35f6d373420c304927a5190" dependencies = [ "anyhow", "heck 0.4.1", - "indexmap 2.4.0", + "indexmap 2.6.0", "wit-parser 0.215.0", ] @@ -14004,13 +14218,13 @@ dependencies = [ [[package]] name = "wayland-backend" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90e11ce2ca99c97b940ee83edbae9da2d56a08f9ea8158550fd77fa31722993" +checksum = "056535ced7a150d45159d3a8dc30f91a2e2d588ca0b23f70e56033622b8016f6" dependencies = [ "cc", "downcast-rs", - "rustix 0.38.35", + "rustix 0.38.40", "scoped-tls", "smallvec", "wayland-sys", @@ -14018,23 +14232,23 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.5" +version = "0.31.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e321577a0a165911bdcfb39cf029302479d7527b517ee58ab0f6ad09edf0943" +checksum = "b66249d3fc69f76fd74c82cc319300faa554e9d865dab1f7cd66cc20db10b280" dependencies = [ "bitflags 2.6.0", - "rustix 0.38.35", + "rustix 0.38.40", "wayland-backend", "wayland-scanner", ] [[package]] name = "wayland-cursor" -version = "0.31.5" +version = "0.31.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ef9489a8df197ebf3a8ce8a7a7f0a2320035c3743f3c1bd0bdbccf07ce64f95" +checksum = "32b08bc3aafdb0035e7fe0fdf17ba0c09c268732707dca4ae098f60cb28c9e4c" dependencies = [ - "rustix 0.38.35", + "rustix 0.38.40", "wayland-client", "xcursor", ] @@ -14066,20 +14280,20 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.4" +version = "0.31.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7b56f89937f1cf2ee1f1259cf2936a17a1f45d8f0aa1019fae6d470d304cfa6" +checksum = "597f2001b2e5fc1121e3d5b9791d3e78f05ba6bfa4641053846248e3a13661c3" dependencies = [ "proc-macro2", - "quick-xml 0.34.0", + "quick-xml 0.36.2", "quote", ] [[package]] name = "wayland-sys" -version = "0.31.4" +version = "0.31.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43676fe2daf68754ecf1d72026e4e6c15483198b5d24e888b74d3f22f887a148" +checksum = "efa8ac0d8e8ed3e3b5c9fc92c7881406a268e11555abe36493efabe649a29e09" dependencies = [ "dlib", "log", @@ -14089,9 +14303,19 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.70" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ "js-sys", "wasm-bindgen", @@ -14163,7 +14387,7 @@ dependencies = [ "either", "home", "once_cell", - "rustix 0.38.35", + "rustix 0.38.40", ] [[package]] @@ -14174,30 +14398,30 @@ checksum = "b4ee928febd44d98f2f459a4a79bd4d928591333a494a10a868418ac1b39cf1f" dependencies = [ "either", "home", - "rustix 0.38.35", + "rustix 0.38.40", "winsafe", ] [[package]] name = "whoami" -version = "1.5.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" +checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" dependencies = [ - "redox_syscall 0.4.1", + "redox_syscall 0.5.7", "wasite", ] [[package]] name = "wiggle" -version = "24.0.1" +version = "24.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d3b31bd2b4d2d82a4b747b8dbc45f566214214a4ffdc5690429a73bc221dc8a" +checksum = "c72a4c92952216582f55eab27819a1fe8d3c54b292b7b8e5f849b23bfed96e78" dependencies = [ "anyhow", "async-trait", "bitflags 2.6.0", - "thiserror", + "thiserror 1.0.69", "tracing", "wasmtime", "wiggle-macro", @@ -14205,9 +14429,9 @@ dependencies = [ [[package]] name = "wiggle-generate" -version = "24.0.1" +version = "24.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2c6136b195fc12067aa9d4e7a5baf118729394df7bc7cbf8c63119bc9f2a7cd" +checksum = "cb744fb938a9fc38207838829b4a43831c1de499e3526eaea71deeff4d9cbb83" dependencies = [ "anyhow", "heck 0.4.1", @@ -14220,9 +14444,9 @@ dependencies = [ [[package]] name = "wiggle-macro" -version = "24.0.1" +version = "24.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a41eaceee468da976ac43b85c4eb82e482f828d5e8e56f49f90dfac2d9bc3b4" +checksum = "7cef395fff17bf8f9c1dee6c0e12801a3ba24928139af0ecb5ccb82ff87bf9d2" dependencies = [ "proc-macro2", "quote", @@ -14288,6 +14512,16 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +dependencies = [ + "windows-core 0.57.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows" version = "0.58.0" @@ -14317,19 +14551,42 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +dependencies = [ + "windows-implement 0.57.0", + "windows-interface 0.57.0", + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" dependencies = [ - "windows-implement", - "windows-interface", + "windows-implement 0.58.0", + "windows-interface 0.58.0", "windows-result 0.2.0", "windows-strings", "windows-targets 0.52.6", ] +[[package]] +name = "windows-implement" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "windows-implement" version = "0.58.0" @@ -14341,6 +14598,17 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "windows-interface" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "windows-interface" version = "0.58.0" @@ -14616,9 +14884,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.18" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" dependencies = [ "memchr", ] @@ -14713,7 +14981,7 @@ checksum = "d8a39a15d1ae2077688213611209849cad40e9e5cccf6e61951a425850677ff3" dependencies = [ "anyhow", "heck 0.4.1", - "indexmap 2.4.0", + "indexmap 2.6.0", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -14741,7 +15009,7 @@ checksum = "421c0c848a0660a8c22e2fd217929a0191f14476b68962afd2af89fd22e39825" dependencies = [ "anyhow", "bitflags 2.6.0", - "indexmap 2.4.0", + "indexmap 2.6.0", "log", "serde", "serde_derive", @@ -14760,7 +15028,7 @@ checksum = "196d3ecfc4b759a8573bf86a9b3f8996b304b3732e4c7de81655f875f6efdca6" dependencies = [ "anyhow", "id-arena", - "indexmap 2.4.0", + "indexmap 2.6.0", "log", "semver", "serde", @@ -14778,7 +15046,7 @@ checksum = "935a97eaffd57c3b413aa510f8f0b550a4a9fe7d59e79cd8b89a83dcb860321f" dependencies = [ "anyhow", "id-arena", - "indexmap 2.4.0", + "indexmap 2.6.0", "log", "semver", "serde", @@ -14796,7 +15064,7 @@ checksum = "e366f27a5cabcddb2706a78296a40b8fcc451e1a6aba2fc1d94b4a01bdaaef4b" dependencies = [ "anyhow", "log", - "thiserror", + "thiserror 1.0.69", "wast", ] @@ -14816,7 +15084,7 @@ dependencies = [ "derive_more", "env_logger 0.11.5", "fs", - "futures 0.3.30", + "futures 0.3.31", "git", "gpui", "http_client", @@ -14853,7 +15121,7 @@ dependencies = [ "collections", "env_logger 0.11.5", "fs", - "futures 0.3.30", + "futures 0.3.31", "fuzzy", "git", "git2", @@ -14880,6 +15148,18 @@ dependencies = [ "util", ] +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + [[package]] name = "wyz" version = "0.5.1" @@ -14891,9 +15171,9 @@ dependencies = [ [[package]] name = "x11-clipboard" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98785a09322d7446e28a13203d2cae1059a0dd3dfb32cb06d0a225f023d8286" +checksum = "662d74b3d77e396b8e5beb00b9cad6a9eccf40b2ef68cc858784b14c41d535a3" dependencies = [ "libc", "x11rb", @@ -14908,7 +15188,7 @@ dependencies = [ "as-raw-xcb-connection", "gethostname", "libc", - "rustix 0.38.35", + "rustix 0.38.40", "x11rb-protocol", ] @@ -15056,6 +15336,30 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "yoke" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", + "synstructure", +] + [[package]] name = "zbus" version = "4.4.0" @@ -15065,9 +15369,9 @@ dependencies = [ "async-broadcast", "async-executor", "async-fs 2.1.2", - "async-io 2.3.4", + "async-io 2.4.0", "async-lock 3.4.0", - "async-process 2.2.4", + "async-process 2.3.0", "async-recursion 1.1.1", "async-task", "async-trait", @@ -15156,7 +15460,7 @@ dependencies = [ "file_finder", "file_icons", "fs", - "futures 0.3.30", + "futures 0.3.31", "git", "git_hosting_providers", "go_to_line", @@ -15451,6 +15755,27 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "zerofrom" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", + "synstructure", +] + [[package]] name = "zeroize" version = "1.8.1" @@ -15473,15 +15798,15 @@ dependencies = [ [[package]] name = "zeromq" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb0560d00172817b7f7c2265060783519c475702ae290b154115ca75e976d4d0" +checksum = "6a4528179201f6eecf211961a7d3276faa61554c82651ecc66387f68fc3004bd" dependencies = [ "async-dispatcher", "async-std", "async-trait", "asynchronous-codec", - "bytes 1.7.2", + "bytes 1.8.0", "crossbeam-queue", "dashmap 5.5.3", "futures-channel", @@ -15494,10 +15819,32 @@ dependencies = [ "parking_lot", "rand 0.8.5", "regex", - "thiserror", + "thiserror 1.0.69", "uuid", ] +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "zstd" version = "0.11.2+zstd.1.5.2" diff --git a/Cargo.toml b/Cargo.toml index ef5569f72b..98922a7ca2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -368,12 +368,14 @@ indexmap = { version = "1.6.2", features = ["serde"] } indoc = "2" itertools = "0.13.0" jsonwebtoken = "9.3" +jupyter-protocol = { version = "0.2.0" } +jupyter-websocket-client = { version = "0.4.1" } libc = "0.2" linkify = "0.10.0" log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] } markup5ever_rcdom = "0.3.0" nanoid = "0.4" -nbformat = "0.5.0" +nbformat = "0.6.0" nix = "0.29" num-format = "0.4.4" once_cell = "1.19.0" @@ -407,7 +409,7 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "fd110f "stream", ] } rsa = "0.9.6" -runtimelib = { version = "0.19.0", default-features = false, features = [ +runtimelib = { version = "0.21.0", default-features = false, features = [ "async-dispatcher-runtime", ] } rustc-demangle = "0.1.23" diff --git a/crates/quick_action_bar/src/repl_menu.rs b/crates/quick_action_bar/src/repl_menu.rs index d2649d4180..b9ae940579 100644 --- a/crates/quick_action_bar/src/repl_menu.rs +++ b/crates/quick_action_bar/src/repl_menu.rs @@ -402,7 +402,7 @@ fn session_state(session: View, cx: &WindowContext) -> ReplMenuState { status: session.kernel.status(), ..fill_fields() }, - Kernel::RunningKernel(kernel) => match &kernel.execution_state { + Kernel::RunningKernel(kernel) => match &kernel.execution_state() { ExecutionState::Idle => ReplMenuState { tooltip: format!("Run code on {} ({})", kernel_name, kernel_language).into(), indicator: Some(Indicator::dot().color(Color::Success)), diff --git a/crates/repl/Cargo.toml b/crates/repl/Cargo.toml index b170def71f..3f59ca325b 100644 --- a/crates/repl/Cargo.toml +++ b/crates/repl/Cargo.toml @@ -25,6 +25,8 @@ feature_flags.workspace = true futures.workspace = true gpui.workspace = true image.workspace = true +jupyter-websocket-client.workspace = true +jupyter-protocol.workspace = true language.workspace = true log.workspace = true markdown_preview.workspace = true diff --git a/crates/repl/src/kernels/mod.rs b/crates/repl/src/kernels/mod.rs new file mode 100644 index 0000000000..cea5adb59e --- /dev/null +++ b/crates/repl/src/kernels/mod.rs @@ -0,0 +1,227 @@ +mod native_kernel; +use std::{fmt::Debug, future::Future, path::PathBuf}; + +use futures::{ + channel::mpsc::{self, Receiver}, + future::Shared, + stream, +}; +use gpui::{AppContext, Model, Task}; +use language::LanguageName; +pub use native_kernel::*; + +mod remote_kernels; +use project::{Project, WorktreeId}; +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>; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum KernelSpecification { + Remote(RemoteKernelSpecification), + Jupyter(LocalKernelSpecification), + PythonEnv(LocalKernelSpecification), +} + +impl KernelSpecification { + pub fn name(&self) -> SharedString { + match self { + Self::Jupyter(spec) => spec.name.clone().into(), + Self::PythonEnv(spec) => spec.name.clone().into(), + Self::Remote(spec) => spec.name.clone().into(), + } + } + + pub fn type_name(&self) -> SharedString { + match self { + Self::Jupyter(_) => "Jupyter".into(), + Self::PythonEnv(_) => "Python Environment".into(), + Self::Remote(_) => "Remote".into(), + } + } + + pub fn path(&self) -> SharedString { + SharedString::from(match self { + Self::Jupyter(spec) => spec.path.to_string_lossy().to_string(), + Self::PythonEnv(spec) => spec.path.to_string_lossy().to_string(), + Self::Remote(spec) => spec.url.to_string(), + }) + } + + pub fn language(&self) -> SharedString { + SharedString::from(match self { + Self::Jupyter(spec) => spec.kernelspec.language.clone(), + Self::PythonEnv(spec) => spec.kernelspec.language.clone(), + Self::Remote(spec) => spec.kernelspec.language.clone(), + }) + } +} + +pub fn python_env_kernel_specifications( + project: &Model, + worktree_id: WorktreeId, + cx: &mut AppContext, +) -> impl Future>> { + let python_language = LanguageName::new("Python"); + let toolchains = project + .read(cx) + .available_toolchains(worktree_id, python_language, cx); + let background_executor = cx.background_executor().clone(); + + async move { + let toolchains = if let Some(toolchains) = toolchains.await { + toolchains + } else { + return Ok(Vec::new()); + }; + + let kernelspecs = toolchains.toolchains.into_iter().map(|toolchain| { + background_executor.spawn(async move { + let python_path = toolchain.path.to_string(); + + // Check if ipykernel is installed + let ipykernel_check = Command::new(&python_path) + .args(&["-c", "import ipykernel"]) + .output() + .await; + + if ipykernel_check.is_ok() && ipykernel_check.unwrap().status.success() { + // Create a default kernelspec for this environment + let default_kernelspec = JupyterKernelspec { + argv: vec![ + python_path.clone(), + "-m".to_string(), + "ipykernel_launcher".to_string(), + "-f".to_string(), + "{connection_file}".to_string(), + ], + display_name: toolchain.name.to_string(), + language: "python".to_string(), + interrupt_mode: None, + metadata: None, + env: None, + }; + + Some(KernelSpecification::PythonEnv(LocalKernelSpecification { + name: toolchain.name.to_string(), + path: PathBuf::from(&python_path), + kernelspec: default_kernelspec, + })) + } else { + None + } + }) + }); + + let kernel_specs = futures::future::join_all(kernelspecs) + .await + .into_iter() + .flatten() + .collect(); + + anyhow::Ok(kernel_specs) + } +} + +pub trait RunningKernel: Send + Debug { + fn request_tx(&self) -> mpsc::Sender; + fn working_directory(&self) -> &PathBuf; + fn execution_state(&self) -> &ExecutionState; + 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<()>; +} + +#[derive(Debug, Clone)] +pub enum KernelStatus { + Idle, + Busy, + Starting, + Error, + ShuttingDown, + Shutdown, + Restarting, +} + +impl KernelStatus { + pub fn is_connected(&self) -> bool { + match self { + KernelStatus::Idle | KernelStatus::Busy => true, + _ => false, + } + } +} + +impl ToString for KernelStatus { + fn to_string(&self) -> String { + match self { + KernelStatus::Idle => "Idle".to_string(), + KernelStatus::Busy => "Busy".to_string(), + KernelStatus::Starting => "Starting".to_string(), + KernelStatus::Error => "Error".to_string(), + KernelStatus::ShuttingDown => "Shutting Down".to_string(), + KernelStatus::Shutdown => "Shutdown".to_string(), + KernelStatus::Restarting => "Restarting".to_string(), + } + } +} + +#[derive(Debug)] +pub enum Kernel { + RunningKernel(Box), + StartingKernel(Shared>), + ErroredLaunch(String), + ShuttingDown, + Shutdown, + Restarting, +} + +impl From<&Kernel> for KernelStatus { + fn from(kernel: &Kernel) -> Self { + match kernel { + Kernel::RunningKernel(kernel) => match kernel.execution_state() { + ExecutionState::Idle => KernelStatus::Idle, + ExecutionState::Busy => KernelStatus::Busy, + }, + Kernel::StartingKernel(_) => KernelStatus::Starting, + Kernel::ErroredLaunch(_) => KernelStatus::Error, + Kernel::ShuttingDown => KernelStatus::ShuttingDown, + Kernel::Shutdown => KernelStatus::Shutdown, + Kernel::Restarting => KernelStatus::Restarting, + } + } +} + +impl Kernel { + pub fn status(&self) -> KernelStatus { + self.into() + } + + pub fn set_execution_state(&mut self, status: &ExecutionState) { + if let Kernel::RunningKernel(running_kernel) = self { + running_kernel.set_execution_state(status.clone()); + } + } + + pub fn set_kernel_info(&mut self, kernel_info: &KernelInfoReply) { + if let Kernel::RunningKernel(running_kernel) = self { + running_kernel.set_kernel_info(kernel_info.clone()); + } + } + + pub fn is_shutting_down(&self) -> bool { + match self { + Kernel::Restarting | Kernel::ShuttingDown => true, + Kernel::RunningKernel(_) + | Kernel::StartingKernel(_) + | Kernel::ErroredLaunch(_) + | Kernel::Shutdown => false, + } + } +} diff --git a/crates/repl/src/kernels.rs b/crates/repl/src/kernels/native_kernel.rs similarity index 62% rename from crates/repl/src/kernels.rs rename to crates/repl/src/kernels/native_kernel.rs index 8ad8a05648..8a232c3de9 100644 --- a/crates/repl/src/kernels.rs +++ b/crates/repl/src/kernels/native_kernel.rs @@ -1,69 +1,24 @@ use anyhow::{Context as _, Result}; use futures::{ - channel::mpsc::{self, Receiver}, - future::Shared, - stream::{self, SelectAll, StreamExt}, + channel::mpsc::{self}, + stream::{SelectAll, StreamExt}, SinkExt as _, }; -use gpui::{AppContext, EntityId, Model, Task}; -use language::LanguageName; -use project::{Fs, Project, WorktreeId}; -use runtimelib::{ - dirs, ConnectionInfo, ExecutionState, JupyterKernelspec, JupyterMessage, JupyterMessageContent, - KernelInfoReply, -}; +use gpui::{AppContext, EntityId, Task}; +use jupyter_protocol::{JupyterMessage, JupyterMessageContent, KernelInfoReply}; +use project::Fs; +use runtimelib::{dirs, ConnectionInfo, ExecutionState, JupyterKernelspec}; use smol::{net::TcpListener, process::Command}; use std::{ env, fmt::Debug, - future::Future, net::{IpAddr, Ipv4Addr, SocketAddr}, path::PathBuf, sync::Arc, }; -use ui::SharedString; use uuid::Uuid; -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum KernelSpecification { - Remote(RemoteKernelSpecification), - Jupyter(LocalKernelSpecification), - PythonEnv(LocalKernelSpecification), -} - -impl KernelSpecification { - pub fn name(&self) -> SharedString { - match self { - Self::Jupyter(spec) => spec.name.clone().into(), - Self::PythonEnv(spec) => spec.name.clone().into(), - Self::Remote(spec) => spec.name.clone().into(), - } - } - - pub fn type_name(&self) -> SharedString { - match self { - Self::Jupyter(_) => "Jupyter".into(), - Self::PythonEnv(_) => "Python Environment".into(), - Self::Remote(_) => "Remote".into(), - } - } - - pub fn path(&self) -> SharedString { - SharedString::from(match self { - Self::Jupyter(spec) => spec.path.to_string_lossy().to_string(), - Self::PythonEnv(spec) => spec.path.to_string_lossy().to_string(), - Self::Remote(spec) => spec.url.to_string(), - }) - } - - pub fn language(&self) -> SharedString { - SharedString::from(match self { - Self::Jupyter(spec) => spec.kernelspec.language.clone(), - Self::PythonEnv(spec) => spec.kernelspec.language.clone(), - Self::Remote(spec) => spec.kernelspec.language.clone(), - }) - } -} +use super::{JupyterMessageChannel, RunningKernel}; #[derive(Debug, Clone)] pub struct LocalKernelSpecification { @@ -80,22 +35,6 @@ impl PartialEq for LocalKernelSpecification { impl Eq for LocalKernelSpecification {} -#[derive(Debug, Clone)] -pub struct RemoteKernelSpecification { - pub name: String, - pub url: String, - pub token: String, - pub kernelspec: JupyterKernelspec, -} - -impl PartialEq for RemoteKernelSpecification { - fn eq(&self, other: &Self) -> bool { - self.name == other.name && self.url == other.url - } -} - -impl Eq for RemoteKernelSpecification {} - impl LocalKernelSpecification { #[must_use] fn command(&self, connection_path: &PathBuf) -> Result { @@ -147,95 +86,7 @@ async fn peek_ports(ip: IpAddr) -> Result<[u16; 5]> { Ok(ports) } -#[derive(Debug, Clone)] -pub enum KernelStatus { - Idle, - Busy, - Starting, - Error, - ShuttingDown, - Shutdown, - Restarting, -} - -impl KernelStatus { - pub fn is_connected(&self) -> bool { - match self { - KernelStatus::Idle | KernelStatus::Busy => true, - _ => false, - } - } -} - -impl ToString for KernelStatus { - fn to_string(&self) -> String { - match self { - KernelStatus::Idle => "Idle".to_string(), - KernelStatus::Busy => "Busy".to_string(), - KernelStatus::Starting => "Starting".to_string(), - KernelStatus::Error => "Error".to_string(), - KernelStatus::ShuttingDown => "Shutting Down".to_string(), - KernelStatus::Shutdown => "Shutdown".to_string(), - KernelStatus::Restarting => "Restarting".to_string(), - } - } -} - -impl From<&Kernel> for KernelStatus { - fn from(kernel: &Kernel) -> Self { - match kernel { - Kernel::RunningKernel(kernel) => match kernel.execution_state { - ExecutionState::Idle => KernelStatus::Idle, - ExecutionState::Busy => KernelStatus::Busy, - }, - Kernel::StartingKernel(_) => KernelStatus::Starting, - Kernel::ErroredLaunch(_) => KernelStatus::Error, - Kernel::ShuttingDown => KernelStatus::ShuttingDown, - Kernel::Shutdown => KernelStatus::Shutdown, - Kernel::Restarting => KernelStatus::Restarting, - } - } -} - -#[derive(Debug)] -pub enum Kernel { - RunningKernel(RunningKernel), - StartingKernel(Shared>), - ErroredLaunch(String), - ShuttingDown, - Shutdown, - Restarting, -} - -impl Kernel { - pub fn status(&self) -> KernelStatus { - self.into() - } - - pub fn set_execution_state(&mut self, status: &ExecutionState) { - if let Kernel::RunningKernel(running_kernel) = self { - running_kernel.execution_state = status.clone(); - } - } - - pub fn set_kernel_info(&mut self, kernel_info: &KernelInfoReply) { - if let Kernel::RunningKernel(running_kernel) = self { - running_kernel.kernel_info = Some(kernel_info.clone()); - } - } - - pub fn is_shutting_down(&self) -> bool { - match self { - Kernel::Restarting | Kernel::ShuttingDown => true, - Kernel::RunningKernel(_) - | Kernel::StartingKernel(_) - | Kernel::ErroredLaunch(_) - | Kernel::Shutdown => false, - } - } -} - -pub struct RunningKernel { +pub struct NativeRunningKernel { pub process: smol::process::Child, _shell_task: Task>, _iopub_task: Task>, @@ -248,9 +99,7 @@ pub struct RunningKernel { pub kernel_info: Option, } -type JupyterMessageChannel = stream::SelectAll>; - -impl Debug for RunningKernel { +impl Debug for NativeRunningKernel { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("RunningKernel") .field("process", &self.process) @@ -258,25 +107,14 @@ impl Debug for RunningKernel { } } -impl RunningKernel { +impl NativeRunningKernel { pub fn new( - kernel_specification: KernelSpecification, + kernel_specification: LocalKernelSpecification, entity_id: EntityId, working_directory: PathBuf, fs: Arc, cx: &mut AppContext, ) -> Task> { - let kernel_specification = match kernel_specification { - KernelSpecification::Jupyter(spec) => spec, - KernelSpecification::PythonEnv(spec) => spec, - KernelSpecification::Remote(_spec) => { - // todo!(): Implement remote kernel specification - return Task::ready(Err(anyhow::anyhow!( - "Running remote kernels is not supported" - ))); - } - }; - cx.spawn(|cx| async move { let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); let ports = peek_ports(ip).await?; @@ -315,15 +153,13 @@ impl RunningKernel { let session_id = Uuid::new_v4().to_string(); - let mut iopub_socket = connection_info - .create_client_iopub_connection("", &session_id) - .await?; - let mut shell_socket = connection_info - .create_client_shell_connection(&session_id) - .await?; - let mut control_socket = connection_info - .create_client_control_connection(&session_id) - .await?; + let mut iopub_socket = + runtimelib::create_client_iopub_connection(&connection_info, "", &session_id) + .await?; + let mut shell_socket = + runtimelib::create_client_shell_connection(&connection_info, &session_id).await?; + let mut control_socket = + runtimelib::create_client_control_connection(&connection_info, &session_id).await?; let (mut iopub, iosub) = futures::channel::mpsc::channel(100); @@ -410,7 +246,43 @@ impl RunningKernel { } } -impl Drop for RunningKernel { +impl RunningKernel for NativeRunningKernel { + fn request_tx(&self) -> mpsc::Sender { + self.request_tx.clone() + } + + fn working_directory(&self) -> &PathBuf { + &self.working_directory + } + + fn execution_state(&self) -> &ExecutionState { + &self.execution_state + } + + fn set_execution_state(&mut self, state: ExecutionState) { + self.execution_state = state; + } + + fn kernel_info(&self) -> Option<&KernelInfoReply> { + self.kernel_info.as_ref() + } + + fn set_kernel_info(&mut self, info: KernelInfoReply) { + self.kernel_info = Some(info); + } + + fn force_shutdown(&mut self) -> anyhow::Result<()> { + match self.process.kill() { + Ok(_) => Ok(()), + Err(error) => Err(anyhow::anyhow!( + "Failed to kill the kernel process: {}", + error + )), + } + } +} + +impl Drop for NativeRunningKernel { fn drop(&mut self) { std::fs::remove_file(&self.connection_path).ok(); self.request_tx.close_channel(); @@ -467,72 +339,6 @@ async fn read_kernels_dir(path: PathBuf, fs: &dyn Fs) -> Result, - worktree_id: WorktreeId, - cx: &mut AppContext, -) -> impl Future>> { - let python_language = LanguageName::new("Python"); - let toolchains = project - .read(cx) - .available_toolchains(worktree_id, python_language, cx); - let background_executor = cx.background_executor().clone(); - - async move { - let toolchains = if let Some(toolchains) = toolchains.await { - toolchains - } else { - return Ok(Vec::new()); - }; - - let kernelspecs = toolchains.toolchains.into_iter().map(|toolchain| { - background_executor.spawn(async move { - let python_path = toolchain.path.to_string(); - - // Check if ipykernel is installed - let ipykernel_check = Command::new(&python_path) - .args(&["-c", "import ipykernel"]) - .output() - .await; - - if ipykernel_check.is_ok() && ipykernel_check.unwrap().status.success() { - // Create a default kernelspec for this environment - let default_kernelspec = JupyterKernelspec { - argv: vec![ - python_path.clone(), - "-m".to_string(), - "ipykernel_launcher".to_string(), - "-f".to_string(), - "{connection_file}".to_string(), - ], - display_name: toolchain.name.to_string(), - language: "python".to_string(), - interrupt_mode: None, - metadata: None, - env: None, - }; - - Some(KernelSpecification::PythonEnv(LocalKernelSpecification { - name: toolchain.name.to_string(), - path: PathBuf::from(&python_path), - kernelspec: default_kernelspec, - })) - } else { - None - } - }) - }); - - let kernel_specs = futures::future::join_all(kernelspecs) - .await - .into_iter() - .flatten() - .collect(); - - anyhow::Ok(kernel_specs) - } -} - pub async fn local_kernel_specifications(fs: Arc) -> Result> { let mut data_dirs = dirs::data_dirs(); diff --git a/crates/repl/src/kernels/remote_kernels.rs b/crates/repl/src/kernels/remote_kernels.rs new file mode 100644 index 0000000000..9d2d5f2810 --- /dev/null +++ b/crates/repl/src/kernels/remote_kernels.rs @@ -0,0 +1,122 @@ +use futures::{channel::mpsc, StreamExt as _}; +use gpui::AppContext; +use jupyter_protocol::{ExecutionState, JupyterMessage, KernelInfoReply}; +// todo(kyle): figure out if this needs to be different +use runtimelib::JupyterKernelspec; + +use super::RunningKernel; +use jupyter_websocket_client::RemoteServer; +use std::fmt::Debug; + +#[derive(Debug, Clone)] +pub struct RemoteKernelSpecification { + pub name: String, + pub url: String, + pub token: String, + pub kernelspec: JupyterKernelspec, +} + +impl PartialEq for RemoteKernelSpecification { + fn eq(&self, other: &Self) -> bool { + self.name == other.name && self.url == other.url + } +} + +impl Eq for RemoteKernelSpecification {} + +pub struct RemoteRunningKernel { + remote_server: RemoteServer, + pub working_directory: std::path::PathBuf, + pub request_tx: mpsc::Sender, + pub execution_state: ExecutionState, + pub kernel_info: Option, +} + +impl RemoteRunningKernel { + pub async fn new( + kernelspec: RemoteKernelSpecification, + working_directory: std::path::PathBuf, + request_tx: mpsc::Sender, + _cx: &mut AppContext, + ) -> anyhow::Result<( + Self, + (), // Stream + )> { + 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 kernel_socket = remote_server.connect_to_kernel(kernel_id).await?; + + let (mut _w, mut _r) = kernel_socket.split(); + + let (_messages_tx, _messages_rx) = mpsc::channel::(100); + + // 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(); + + anyhow::Ok(( + Self { + remote_server, + working_directory, + request_tx, + execution_state: ExecutionState::Idle, + kernel_info: None, + }, + (), + )) + } +} + +impl Debug for RemoteRunningKernel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RemoteRunningKernel") + // custom debug that keeps tokens out of logs + .field("remote_server url", &self.remote_server.base_url) + .field("working_directory", &self.working_directory) + .field("request_tx", &self.request_tx) + .field("execution_state", &self.execution_state) + .field("kernel_info", &self.kernel_info) + .finish() + } +} + +impl RunningKernel for RemoteRunningKernel { + fn request_tx(&self) -> futures::channel::mpsc::Sender { + self.request_tx.clone() + } + + fn working_directory(&self) -> &std::path::PathBuf { + &self.working_directory + } + + fn execution_state(&self) -> &runtimelib::ExecutionState { + &self.execution_state + } + + fn set_execution_state(&mut self, state: runtimelib::ExecutionState) { + self.execution_state = state; + } + + fn kernel_info(&self) -> Option<&runtimelib::KernelInfoReply> { + self.kernel_info.as_ref() + } + + fn set_kernel_info(&mut self, info: runtimelib::KernelInfoReply) { + self.kernel_info = Some(info); + } + + fn force_shutdown(&mut self) -> anyhow::Result<()> { + unimplemented!("force_shutdown") + } +} diff --git a/crates/repl/src/repl.rs b/crates/repl/src/repl.rs index be187ff16f..4d11734e29 100644 --- a/crates/repl/src/repl.rs +++ b/crates/repl/src/repl.rs @@ -1,6 +1,6 @@ pub mod components; mod jupyter_settings; -mod kernels; +pub mod kernels; pub mod notebook; mod outputs; mod repl_editor; diff --git a/crates/repl/src/session.rs b/crates/repl/src/session.rs index 74ce497572..513e85719d 100644 --- a/crates/repl/src/session.rs +++ b/crates/repl/src/session.rs @@ -1,7 +1,7 @@ use crate::components::KernelListItem; use crate::setup_editor_session_actions; use crate::{ - kernels::{Kernel, KernelSpecification, RunningKernel}, + kernels::{Kernel, KernelSpecification, NativeRunningKernel}, outputs::{ExecutionStatus, ExecutionView}, KernelStatus, }; @@ -246,13 +246,19 @@ impl Session { cx.entity_id().to_string(), ); - let kernel = RunningKernel::new( - self.kernel_specification.clone(), - entity_id, - working_directory, - self.fs.clone(), - cx, - ); + let kernel = match self.kernel_specification.clone() { + KernelSpecification::Jupyter(kernel_specification) + | KernelSpecification::PythonEnv(kernel_specification) => NativeRunningKernel::new( + kernel_specification, + entity_id, + working_directory, + self.fs.clone(), + cx, + ), + KernelSpecification::Remote(_remote_kernel_specification) => { + unimplemented!() + } + }; let pending_kernel = cx .spawn(|this, mut cx| async move { @@ -291,7 +297,7 @@ impl Session { .detach(); let status = kernel.process.status(); - session.kernel(Kernel::RunningKernel(kernel), cx); + 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 { @@ -416,7 +422,7 @@ impl Session { fn send(&mut self, message: JupyterMessage, _cx: &mut ViewContext) -> anyhow::Result<()> { if let Kernel::RunningKernel(kernel) = &mut self.kernel { - kernel.request_tx.try_send(message).ok(); + kernel.request_tx().try_send(message).ok(); } anyhow::Ok(()) @@ -631,7 +637,7 @@ impl Session { match kernel { Kernel::RunningKernel(mut kernel) => { - let mut request_tx = kernel.request_tx.clone(); + let mut request_tx = kernel.request_tx().clone(); cx.spawn(|this, mut cx| async move { let message: JupyterMessage = ShutdownRequest { restart: false }.into(); @@ -646,7 +652,7 @@ impl Session { }) .ok(); - kernel.process.kill().ok(); + kernel.force_shutdown().ok(); this.update(&mut cx, |session, cx| { session.clear_outputs(cx); @@ -674,7 +680,7 @@ impl Session { // Do nothing if already restarting } Kernel::RunningKernel(mut kernel) => { - let mut request_tx = kernel.request_tx.clone(); + let mut request_tx = kernel.request_tx().clone(); cx.spawn(|this, mut cx| async move { // Send shutdown request with restart flag @@ -692,7 +698,7 @@ impl Session { cx.background_executor().timer(Duration::from_secs(1)).await; // Force kill the kernel if it hasn't shut down - kernel.process.kill().ok(); + kernel.force_shutdown().ok(); // Start a new kernel this.update(&mut cx, |session, cx| { @@ -727,7 +733,7 @@ impl Render for Session { let (status_text, interrupt_button) = match &self.kernel { Kernel::RunningKernel(kernel) => ( kernel - .kernel_info + .kernel_info() .as_ref() .map(|info| info.language_info.name.clone()), Some( @@ -747,7 +753,7 @@ impl Render for Session { KernelListItem::new(self.kernel_specification.clone()) .status_color(match &self.kernel { - Kernel::RunningKernel(kernel) => match kernel.execution_state { + Kernel::RunningKernel(kernel) => match kernel.execution_state() { ExecutionState::Idle => Color::Success, ExecutionState::Busy => Color::Modified, }, From 0e26d22fead5a1a366302145d14c810d8804b8a1 Mon Sep 17 00:00:00 2001 From: uncenter <47499684+uncenter@users.noreply.github.com> Date: Mon, 18 Nov 2024 22:19:24 -0500 Subject: [PATCH 025/886] Add HTML injections for markdown (#20527) Closes https://github.com/zed-industries/extensions/issues/1588. | Before | After | | --- | --- | | ![CleanShot 2024-11-11 at 22 48 43](https://github.com/user-attachments/assets/9470e6a8-6a37-4b8f-8daa-5c8c5ed2bb17) | ![CleanShot 2024-11-11 at 22 49 43](https://github.com/user-attachments/assets/f2b858d0-9274-4332-b30e-61c13ac347c6) | Release Notes: - Added HTML injections for markdown syntax highlighting --- crates/languages/src/markdown/injections.scm | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/languages/src/markdown/injections.scm b/crates/languages/src/markdown/injections.scm index 4b2493d4ce..5972a43eb1 100644 --- a/crates/languages/src/markdown/injections.scm +++ b/crates/languages/src/markdown/injections.scm @@ -5,3 +5,6 @@ ((inline) @content (#set! "language" "markdown-inline")) + +((html_block) @content + (#set! "language" "html")) From c0d11be75f0a2febec1edc86c70a2594eca79b44 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 19 Nov 2024 00:24:37 -0500 Subject: [PATCH 026/886] remove usages of `theme::color_alpha` --- crates/assistant/src/assistant_panel.rs | 4 ++-- crates/extensions_ui/src/components/extension_card.rs | 5 +---- crates/outline/src/outline.rs | 4 ++-- crates/theme/src/theme.rs | 8 -------- 4 files changed, 5 insertions(+), 16 deletions(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index b682bfdcca..6e0ff77ef2 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -2625,8 +2625,8 @@ impl ContextEditor { .px_1() .mr_0p5() .border_1() - .border_color(theme::color_alpha(colors.border_variant, 0.6)) - .bg(theme::color_alpha(colors.element_background, 0.6)) + .border_color(colors.border_variant.opacity(0.6)) + .bg(colors.element_background.opacity(0.6)) .child("esc"), ) .child("to cancel") diff --git a/crates/extensions_ui/src/components/extension_card.rs b/crates/extensions_ui/src/components/extension_card.rs index 2dc472f801..c44ac2063a 100644 --- a/crates/extensions_ui/src/components/extension_card.rs +++ b/crates/extensions_ui/src/components/extension_card.rs @@ -52,10 +52,7 @@ impl RenderOnce for ExtensionCard { .size_full() .items_center() .justify_center() - .bg(theme::color_alpha( - cx.theme().colors().elevated_surface_background, - 0.8, - )) + .bg(cx.theme().colors().elevated_surface_background.opacity(0.8)) .child(Label::new("Overridden by dev extension.")), ) }), diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index 154b9297a3..e6ba24f75c 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -17,7 +17,7 @@ use language::{Outline, OutlineItem}; use ordered_float::OrderedFloat; use picker::{Picker, PickerDelegate}; use settings::Settings; -use theme::{color_alpha, ActiveTheme, ThemeSettings}; +use theme::{ActiveTheme, ThemeSettings}; use ui::{prelude::*, ListItem, ListItemSpacing}; use util::ResultExt; use workspace::{DismissDecision, ModalView}; @@ -297,7 +297,7 @@ pub fn render_item( cx: &AppContext, ) -> StyledText { let highlight_style = HighlightStyle { - background_color: Some(color_alpha(cx.theme().colors().text_accent, 0.3)), + background_color: Some(cx.theme().colors().text_accent.opacity(0.3)), ..Default::default() }; let custom_highlights = match_ranges diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index cf860ad452..d501d3c118 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -333,14 +333,6 @@ impl Theme { } } -/// Compounds a color with an alpha value. -/// TODO: Replace this with a method on Hsla. -pub fn color_alpha(color: Hsla, alpha: f32) -> Hsla { - let mut color = color; - color.a = alpha; - color -} - /// Asynchronously reads the user theme from the specified path. pub async fn read_user_theme(theme_path: &Path, fs: Arc) -> Result { let reader = fs.open_sync(theme_path).await?; From a35b73e63e2bc7d2904b2d346adc27c15cf661a0 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 19 Nov 2024 00:24:48 -0500 Subject: [PATCH 027/886] Revert "remove usages of `theme::color_alpha`" This reverts commit c0d11be75f0a2febec1edc86c70a2594eca79b44. --- crates/assistant/src/assistant_panel.rs | 4 ++-- crates/extensions_ui/src/components/extension_card.rs | 5 ++++- crates/outline/src/outline.rs | 4 ++-- crates/theme/src/theme.rs | 8 ++++++++ 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 6e0ff77ef2..b682bfdcca 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -2625,8 +2625,8 @@ impl ContextEditor { .px_1() .mr_0p5() .border_1() - .border_color(colors.border_variant.opacity(0.6)) - .bg(colors.element_background.opacity(0.6)) + .border_color(theme::color_alpha(colors.border_variant, 0.6)) + .bg(theme::color_alpha(colors.element_background, 0.6)) .child("esc"), ) .child("to cancel") diff --git a/crates/extensions_ui/src/components/extension_card.rs b/crates/extensions_ui/src/components/extension_card.rs index c44ac2063a..2dc472f801 100644 --- a/crates/extensions_ui/src/components/extension_card.rs +++ b/crates/extensions_ui/src/components/extension_card.rs @@ -52,7 +52,10 @@ impl RenderOnce for ExtensionCard { .size_full() .items_center() .justify_center() - .bg(cx.theme().colors().elevated_surface_background.opacity(0.8)) + .bg(theme::color_alpha( + cx.theme().colors().elevated_surface_background, + 0.8, + )) .child(Label::new("Overridden by dev extension.")), ) }), diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index e6ba24f75c..154b9297a3 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -17,7 +17,7 @@ use language::{Outline, OutlineItem}; use ordered_float::OrderedFloat; use picker::{Picker, PickerDelegate}; use settings::Settings; -use theme::{ActiveTheme, ThemeSettings}; +use theme::{color_alpha, ActiveTheme, ThemeSettings}; use ui::{prelude::*, ListItem, ListItemSpacing}; use util::ResultExt; use workspace::{DismissDecision, ModalView}; @@ -297,7 +297,7 @@ pub fn render_item( cx: &AppContext, ) -> StyledText { let highlight_style = HighlightStyle { - background_color: Some(cx.theme().colors().text_accent.opacity(0.3)), + background_color: Some(color_alpha(cx.theme().colors().text_accent, 0.3)), ..Default::default() }; let custom_highlights = match_ranges diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index d501d3c118..cf860ad452 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -333,6 +333,14 @@ impl Theme { } } +/// Compounds a color with an alpha value. +/// TODO: Replace this with a method on Hsla. +pub fn color_alpha(color: Hsla, alpha: f32) -> Hsla { + let mut color = color; + color.a = alpha; + color +} + /// Asynchronously reads the user theme from the specified path. pub async fn read_user_theme(theme_path: &Path, fs: Arc) -> Result { let reader = fs.open_sync(theme_path).await?; From aae39071efcf65fd51feb07ab8149b85bfde230b Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Tue, 19 Nov 2024 09:41:44 +0100 Subject: [PATCH 028/886] editor: Show hints for using AI features on empty lines (#20824) Co-Authored-by: Thorsten Co-Authored-by: Antonio Screenshot: ![screenshot-2024-11-18-17 11 08@2x](https://github.com/user-attachments/assets/610fd7db-7476-4b9b-9465-a3d55df12340) TODO: - [x] docs Release Notes: - Added inline hints that guide users on how to invoke the inline assistant and open the assistant panel. (These hints can be disabled by setting `{"assistant": {"show_hints": false}}`.) --------- Co-authored-by: Thorsten Co-authored-by: Antonio Co-authored-by: Thorsten Ball --- assets/settings/default.json | 3 + crates/assistant/src/assistant_settings.rs | 11 ++ crates/editor/src/editor.rs | 36 ++++++ crates/editor/src/element.rs | 114 +++++++++-------- crates/gpui/src/window.rs | 22 +++- crates/outline_panel/src/outline_panel.rs | 6 +- crates/recent_projects/src/recent_projects.rs | 8 +- crates/zed/src/main.rs | 3 +- crates/zed/src/zed.rs | 1 + crates/zed/src/zed/assistant_hints.rs | 115 ++++++++++++++++++ docs/src/assistant/configuration.md | 26 ++-- docs/src/configuring-zed.md | 21 ++-- 12 files changed, 283 insertions(+), 83 deletions(-) create mode 100644 crates/zed/src/zed/assistant_hints.rs diff --git a/assets/settings/default.json b/assets/settings/default.json index 7c4a9a8111..3757dfe119 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -490,6 +490,9 @@ "version": "2", // Whether the assistant is enabled. "enabled": true, + // Whether to show inline hints showing the keybindings to use the inline assistant and the + // assistant panel. + "show_hints": true, // Whether to show the assistant panel button in the status bar. "button": true, // Where to dock the assistant panel. Can be 'left', 'right' or 'bottom'. diff --git a/crates/assistant/src/assistant_settings.rs b/crates/assistant/src/assistant_settings.rs index 5bfd406658..98188305fb 100644 --- a/crates/assistant/src/assistant_settings.rs +++ b/crates/assistant/src/assistant_settings.rs @@ -60,6 +60,7 @@ pub struct AssistantSettings { pub inline_alternatives: Vec, pub using_outdated_settings_version: bool, pub enable_experimental_live_diffs: bool, + pub show_hints: bool, } impl AssistantSettings { @@ -202,6 +203,7 @@ impl AssistantSettingsContent { AssistantSettingsContent::Versioned(settings) => match settings { VersionedAssistantSettingsContent::V1(settings) => AssistantSettingsContentV2 { enabled: settings.enabled, + show_hints: None, button: settings.button, dock: settings.dock, default_width: settings.default_width, @@ -242,6 +244,7 @@ impl AssistantSettingsContent { }, AssistantSettingsContent::Legacy(settings) => AssistantSettingsContentV2 { enabled: None, + show_hints: None, button: settings.button, dock: settings.dock, default_width: settings.default_width, @@ -354,6 +357,7 @@ impl Default for VersionedAssistantSettingsContent { fn default() -> Self { Self::V2(AssistantSettingsContentV2 { enabled: None, + show_hints: None, button: None, dock: None, default_width: None, @@ -371,6 +375,11 @@ pub struct AssistantSettingsContentV2 { /// /// Default: true enabled: Option, + /// Whether to show inline hints that show keybindings for inline assistant + /// and assistant panel. + /// + /// Default: true + show_hints: Option, /// Whether to show the assistant panel button in the status bar. /// /// Default: true @@ -505,6 +514,7 @@ impl Settings for AssistantSettings { let value = value.upgrade(); merge(&mut settings.enabled, value.enabled); + merge(&mut settings.show_hints, value.show_hints); merge(&mut settings.button, value.button); merge(&mut settings.dock, value.dock); merge( @@ -575,6 +585,7 @@ mod tests { }), inline_alternatives: None, enabled: None, + show_hints: None, button: None, dock: None, default_width: None, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 11d47daa6b..6167c24bff 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -540,6 +540,15 @@ pub enum IsVimMode { No, } +pub trait ActiveLineTrailerProvider { + fn render_active_line_trailer( + &mut self, + style: &EditorStyle, + focus_handle: &FocusHandle, + cx: &mut WindowContext, + ) -> Option; +} + /// Zed's primary text input `View`, allowing users to edit a [`MultiBuffer`] /// /// See the [module level documentation](self) for more information. @@ -667,6 +676,7 @@ pub struct Editor { next_scroll_position: NextScrollCursorCenterTopBottom, addons: HashMap>, _scroll_cursor_center_top_bottom_task: Task<()>, + active_line_trailer_provider: Option>, } #[derive(Copy, Clone, Debug, PartialEq, Eq, Default)] @@ -2200,6 +2210,7 @@ impl Editor { addons: HashMap::default(), _scroll_cursor_center_top_bottom_task: Task::ready(()), text_style_refinement: None, + active_line_trailer_provider: None, }; this.tasks_update_task = Some(this.refresh_runnables(cx)); this._subscriptions.extend(project_subscriptions); @@ -2488,6 +2499,16 @@ impl Editor { self.refresh_inline_completion(false, false, cx); } + pub fn set_active_line_trailer_provider( + &mut self, + provider: Option, + _cx: &mut ViewContext, + ) where + T: ActiveLineTrailerProvider + 'static, + { + self.active_line_trailer_provider = provider.map(|provider| Box::new(provider) as Box<_>); + } + pub fn placeholder_text(&self, _cx: &WindowContext) -> Option<&str> { self.placeholder_text.as_deref() } @@ -11844,6 +11865,21 @@ impl Editor { && self.has_blame_entries(cx) } + pub fn render_active_line_trailer( + &mut self, + style: &EditorStyle, + cx: &mut WindowContext, + ) -> Option { + if !self.newest_selection_head_on_empty_line(cx) || self.has_active_inline_completion(cx) { + return None; + } + + let focus_handle = self.focus_handle.clone(); + self.active_line_trailer_provider + .as_mut()? + .render_active_line_trailer(style, &focus_handle, cx) + } + fn has_blame_entries(&self, cx: &mut WindowContext) -> bool { self.blame() .map_or(false, |blame| blame.read(cx).has_generated_entries()) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 7702134409..6e4538ae6d 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1412,7 +1412,7 @@ impl EditorElement { } #[allow(clippy::too_many_arguments)] - fn layout_inline_blame( + fn layout_active_line_trailer( &self, display_row: DisplayRow, display_snapshot: &DisplaySnapshot, @@ -1424,61 +1424,71 @@ impl EditorElement { line_height: Pixels, cx: &mut WindowContext, ) -> Option { - if !self + let render_inline_blame = self .editor - .update(cx, |editor, cx| editor.render_git_blame_inline(cx)) - { - return None; - } + .update(cx, |editor, cx| editor.render_git_blame_inline(cx)); + if render_inline_blame { + let workspace = self + .editor + .read(cx) + .workspace + .as_ref() + .map(|(w, _)| w.clone()); - let workspace = self - .editor - .read(cx) - .workspace - .as_ref() - .map(|(w, _)| w.clone()); + let display_point = DisplayPoint::new(display_row, 0); + let buffer_row = MultiBufferRow(display_point.to_point(display_snapshot).row); - let display_point = DisplayPoint::new(display_row, 0); - let buffer_row = MultiBufferRow(display_point.to_point(display_snapshot).row); + let blame = self.editor.read(cx).blame.clone()?; + let blame_entry = blame + .update(cx, |blame, cx| { + blame.blame_for_rows([Some(buffer_row)], cx).next() + }) + .flatten()?; - let blame = self.editor.read(cx).blame.clone()?; - let blame_entry = blame - .update(cx, |blame, cx| { - blame.blame_for_rows([Some(buffer_row)], cx).next() - }) - .flatten()?; + let mut element = + render_inline_blame_entry(&blame, blame_entry, &self.style, workspace, cx); - let mut element = - render_inline_blame_entry(&blame, blame_entry, &self.style, workspace, cx); + let start_y = content_origin.y + + line_height * (display_row.as_f32() - scroll_pixel_position.y / line_height); - let start_y = content_origin.y - + line_height * (display_row.as_f32() - scroll_pixel_position.y / line_height); + let start_x = { + const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 6.; - let start_x = { - const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 6.; + let line_end = if let Some(crease_trailer) = crease_trailer { + crease_trailer.bounds.right() + } else { + content_origin.x - scroll_pixel_position.x + line_layout.width + }; + let padded_line_end = line_end + em_width * INLINE_BLAME_PADDING_EM_WIDTHS; - let line_end = if let Some(crease_trailer) = crease_trailer { - crease_trailer.bounds.right() - } else { - content_origin.x - scroll_pixel_position.x + line_layout.width + let min_column_in_pixels = ProjectSettings::get_global(cx) + .git + .inline_blame + .and_then(|settings| settings.min_column) + .map(|col| self.column_pixels(col as usize, cx)) + .unwrap_or(px(0.)); + let min_start = content_origin.x - scroll_pixel_position.x + min_column_in_pixels; + + cmp::max(padded_line_end, min_start) }; - let padded_line_end = line_end + em_width * INLINE_BLAME_PADDING_EM_WIDTHS; - let min_column_in_pixels = ProjectSettings::get_global(cx) - .git - .inline_blame - .and_then(|settings| settings.min_column) - .map(|col| self.column_pixels(col as usize, cx)) - .unwrap_or(px(0.)); - let min_start = content_origin.x - scroll_pixel_position.x + min_column_in_pixels; + let absolute_offset = point(start_x, start_y); + element.prepaint_as_root(absolute_offset, AvailableSpace::min_size(), cx); - cmp::max(padded_line_end, min_start) - }; + Some(element) + } else if let Some(mut element) = self.editor.update(cx, |editor, cx| { + editor.render_active_line_trailer(&self.style, cx) + }) { + let start_y = content_origin.y + + line_height * (display_row.as_f32() - scroll_pixel_position.y / line_height); + let start_x = content_origin.x - scroll_pixel_position.x + em_width; + let absolute_offset = point(start_x, start_y); + element.prepaint_as_root(absolute_offset, AvailableSpace::min_size(), cx); - let absolute_offset = point(start_x, start_y); - element.prepaint_as_root(absolute_offset, AvailableSpace::min_size(), cx); - - Some(element) + Some(element) + } else { + None + } } #[allow(clippy::too_many_arguments)] @@ -3454,7 +3464,7 @@ impl EditorElement { self.paint_lines(&invisible_display_ranges, layout, cx); self.paint_redactions(layout, cx); self.paint_cursors(layout, cx); - self.paint_inline_blame(layout, cx); + self.paint_active_line_trailer(layout, cx); cx.with_element_namespace("crease_trailers", |cx| { for trailer in layout.crease_trailers.iter_mut().flatten() { trailer.element.paint(cx); @@ -3936,10 +3946,10 @@ impl EditorElement { } } - fn paint_inline_blame(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) { - if let Some(mut inline_blame) = layout.inline_blame.take() { + fn paint_active_line_trailer(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) { + if let Some(mut element) = layout.active_line_trailer.take() { cx.paint_layer(layout.text_hitbox.bounds, |cx| { - inline_blame.paint(cx); + element.paint(cx); }) } } @@ -5331,14 +5341,14 @@ impl Element for EditorElement { ) }); - let mut inline_blame = None; + let mut active_line_trailer = None; if let Some(newest_selection_head) = newest_selection_head { let display_row = newest_selection_head.row(); if (start_row..end_row).contains(&display_row) { let line_ix = display_row.minus(start_row) as usize; let line_layout = &line_layouts[line_ix]; let crease_trailer_layout = crease_trailers[line_ix].as_ref(); - inline_blame = self.layout_inline_blame( + active_line_trailer = self.layout_active_line_trailer( display_row, &snapshot.display_snapshot, line_layout, @@ -5657,7 +5667,7 @@ impl Element for EditorElement { line_elements, line_numbers, blamed_display_rows, - inline_blame, + active_line_trailer, blocks, cursors, visible_cursors, @@ -5794,7 +5804,7 @@ pub struct EditorLayout { line_numbers: Vec>, display_hunks: Vec<(DisplayDiffHunk, Option)>, blamed_display_rows: Option>, - inline_blame: Option, + active_line_trailer: Option, blocks: Vec, highlighted_ranges: Vec<(Range, Hsla)>, highlighted_gutter_ranges: Vec<(Range, Hsla)>, diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 0f2be2497a..9a028c1f01 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -3050,7 +3050,7 @@ impl<'a> WindowContext<'a> { } /// Represent this action as a key binding string, to display in the UI. - pub fn keystroke_text_for(&self, action: &dyn Action) -> String { + pub fn keystroke_text_for_action(&self, action: &dyn Action) -> String { self.bindings_for_action(action) .into_iter() .next() @@ -3065,6 +3065,26 @@ impl<'a> WindowContext<'a> { .unwrap_or_else(|| action.name().to_string()) } + /// Represent this action as a key binding string, to display in the UI. + pub fn keystroke_text_for_action_in( + &self, + action: &dyn Action, + focus_handle: &FocusHandle, + ) -> String { + self.bindings_for_action_in(action, focus_handle) + .into_iter() + .next() + .map(|binding| { + binding + .keystrokes() + .iter() + .map(ToString::to_string) + .collect::>() + .join(" ") + }) + .unwrap_or_else(|| action.name().to_string()) + } + /// Dispatch a mouse or keyboard event on the window. #[profiling::function] pub fn dispatch_event(&mut self, event: PlatformInput) -> DispatchEventResult { diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index f878b582d9..f378348782 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -3875,13 +3875,13 @@ impl OutlinePanel { .child({ let keystroke = match self.position(cx) { DockPosition::Left => { - cx.keystroke_text_for(&workspace::ToggleLeftDock) + cx.keystroke_text_for_action(&workspace::ToggleLeftDock) } DockPosition::Bottom => { - cx.keystroke_text_for(&workspace::ToggleBottomDock) + cx.keystroke_text_for_action(&workspace::ToggleBottomDock) } DockPosition::Right => { - cx.keystroke_text_for(&workspace::ToggleRightDock) + cx.keystroke_text_for_action(&workspace::ToggleRightDock) } }; Label::new(format!("Toggle this panel with {keystroke}")) diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index 072e8ba695..e01309cacd 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -185,13 +185,13 @@ impl PickerDelegate for RecentProjectsDelegate { fn placeholder_text(&self, cx: &mut WindowContext) -> Arc { let (create_window, reuse_window) = if self.create_new_window { ( - cx.keystroke_text_for(&menu::Confirm), - cx.keystroke_text_for(&menu::SecondaryConfirm), + cx.keystroke_text_for_action(&menu::Confirm), + cx.keystroke_text_for_action(&menu::SecondaryConfirm), ) } else { ( - cx.keystroke_text_for(&menu::SecondaryConfirm), - cx.keystroke_text_for(&menu::Confirm), + cx.keystroke_text_for_action(&menu::SecondaryConfirm), + cx.keystroke_text_for_action(&menu::Confirm), ) }; Arc::from(format!( diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index a5fc52e933..c632843baa 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -66,7 +66,7 @@ use zed::{ OpenRequest, }; -use crate::zed::inline_completion_registry; +use crate::zed::{assistant_hints, inline_completion_registry}; #[cfg(feature = "mimalloc")] #[global_allocator] @@ -401,6 +401,7 @@ fn main() { stdout_is_a_pty(), cx, ); + assistant_hints::init(cx); repl::init( app_state.fs.clone(), app_state.client.telemetry().clone(), diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index e2dc36a21f..0f10f1914b 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1,4 +1,5 @@ mod app_menus; +pub mod assistant_hints; pub mod inline_completion_registry; #[cfg(any(target_os = "linux", target_os = "freebsd"))] pub(crate) mod linux_prompts; diff --git a/crates/zed/src/zed/assistant_hints.rs b/crates/zed/src/zed/assistant_hints.rs new file mode 100644 index 0000000000..244b7fab26 --- /dev/null +++ b/crates/zed/src/zed/assistant_hints.rs @@ -0,0 +1,115 @@ +use assistant::assistant_settings::AssistantSettings; +use collections::HashMap; +use editor::{ActiveLineTrailerProvider, Editor, EditorMode}; +use gpui::{AnyWindowHandle, AppContext, ViewContext, WeakView, WindowContext}; +use settings::{Settings, SettingsStore}; +use std::{cell::RefCell, rc::Rc}; +use theme::ActiveTheme; +use ui::prelude::*; +use workspace::Workspace; + +pub fn init(cx: &mut AppContext) { + let editors: Rc, AnyWindowHandle>>> = Rc::default(); + + cx.observe_new_views({ + let editors = editors.clone(); + move |_: &mut Workspace, cx: &mut ViewContext| { + let workspace_handle = cx.view().clone(); + cx.subscribe(&workspace_handle, { + let editors = editors.clone(); + move |_, _, event, cx| match event { + workspace::Event::ItemAdded { item } => { + if let Some(editor) = item.act_as::(cx) { + if editor.read(cx).mode() != EditorMode::Full { + return; + } + + cx.on_release({ + let editor_handle = editor.downgrade(); + let editors = editors.clone(); + move |_, _, _| { + editors.borrow_mut().remove(&editor_handle); + } + }) + .detach(); + editors + .borrow_mut() + .insert(editor.downgrade(), cx.window_handle()); + + let show_hints = should_show_hints(cx); + editor.update(cx, |editor, cx| { + assign_active_line_trailer_provider(editor, show_hints, cx) + }) + } + } + _ => {} + } + }) + .detach(); + } + }) + .detach(); + + let mut show_hints = AssistantSettings::get_global(cx).show_hints; + cx.observe_global::(move |cx| { + let new_show_hints = should_show_hints(cx); + if new_show_hints != show_hints { + show_hints = new_show_hints; + for (editor, window) in editors.borrow().iter() { + _ = window.update(cx, |_window, cx| { + _ = editor.update(cx, |editor, cx| { + assign_active_line_trailer_provider(editor, show_hints, cx); + }) + }); + } + } + }) + .detach(); +} + +struct AssistantHintsProvider; + +impl ActiveLineTrailerProvider for AssistantHintsProvider { + fn render_active_line_trailer( + &mut self, + style: &editor::EditorStyle, + focus_handle: &gpui::FocusHandle, + cx: &mut WindowContext, + ) -> Option { + if !focus_handle.is_focused(cx) { + return None; + } + + let chat_keybinding = + cx.keystroke_text_for_action_in(&assistant::ToggleFocus, focus_handle); + let generate_keybinding = + cx.keystroke_text_for_action_in(&zed_actions::InlineAssist::default(), focus_handle); + + Some( + h_flex() + .id("inline-assistant-instructions") + .w_full() + .font_family(style.text.font().family) + .text_color(cx.theme().status().hint) + .line_height(style.text.line_height) + .child(format!( + "{chat_keybinding} to chat, {generate_keybinding} to generate" + )) + .into_any(), + ) + } +} + +fn assign_active_line_trailer_provider( + editor: &mut Editor, + show_hints: bool, + cx: &mut ViewContext, +) { + let provider = show_hints.then_some(AssistantHintsProvider); + editor.set_active_line_trailer_provider(provider, cx); +} + +fn should_show_hints(cx: &AppContext) -> bool { + let assistant_settings = AssistantSettings::get_global(cx); + assistant_settings.enabled && assistant_settings.show_hints +} diff --git a/docs/src/assistant/configuration.md b/docs/src/assistant/configuration.md index 2145bd9504..1be96491f4 100644 --- a/docs/src/assistant/configuration.md +++ b/docs/src/assistant/configuration.md @@ -200,18 +200,28 @@ You must provide the model's Context Window in the `max_tokens` parameter, this { "assistant": { "enabled": true, + "show_hints": true, + "button": true, + "dock": "right" + "default_width": 480, "default_model": { "provider": "zed.dev", "model": "claude-3-5-sonnet" }, "version": "2", - "button": true, - "default_width": 480, - "dock": "right" } } ``` +| key | type | default | description | +| -------------- | ------- | ------- | ------------------------------------------------------------------------------------- | +| enabled | boolean | true | Setting this to `false` will completely disable the assistant | +| show_hints | boolean | true | Whether to to show hints in the editor explaining how to use assistant | +| button | boolean | true | Show the assistant icon in the status bar | +| dock | string | "right" | The default dock position for the assistant panel. Can be ["left", "right", "bottom"] | +| default_height | string | null | The pixel height of the assistant panel when docked to the bottom | +| default_width | string | null | The pixel width of the assistant panel when docked to the left or right | + #### Custom endpoints {#custom-endpoint} You can use a custom API endpoint for different providers, as long as it's compatible with the providers API structure. @@ -271,13 +281,3 @@ will generate two outputs for every assist. One with Claude 3.5 Sonnet, and one } } ``` - -#### Common Panel Settings - -| key | type | default | description | -| -------------- | ------- | ------- | ------------------------------------------------------------------------------------- | -| enabled | boolean | true | Setting this to `false` will completely disable the assistant | -| button | boolean | true | Show the assistant icon in the status bar | -| dock | string | "right" | The default dock position for the assistant panel. Can be ["left", "right", "bottom"] | -| default_height | string | null | The pixel height of the assistant panel when docked to the bottom | -| default_width | string | null | The pixel width of the assistant panel when docked to the left or right | diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index ce8068fa3b..b4da7901a1 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -2327,15 +2327,18 @@ Run the `theme selector: toggle` action in the command palette to see a current - Default: ```json -"assistant": { - "enabled": true, - "button": true, - "dock": "right", - "default_width": 640, - "default_height": 320, - "provider": "openai", - "version": "1", -}, +{ + "assistant": { + "enabled": true, + "button": true, + "dock": "right", + "default_width": 640, + "default_height": 320, + "provider": "openai", + "version": "1", + "show_hints": true + } +} ``` ## Outline Panel From 5b0c15d8c436868e62b6123421cb28e2c70fc420 Mon Sep 17 00:00:00 2001 From: Julian de Ruiter <307739+jrderuiter@users.noreply.github.com> Date: Tue, 19 Nov 2024 14:34:56 +0100 Subject: [PATCH 029/886] Add pytest-based test discovery and runnables for Python (#18824) Closes #12080, #18649. Screenshot: image Still in progress: 1. I'd like to add configuration options for selecting a Python test runner (either pytest or unittest) so that users can explicitly choose which runner they'd like to use for running their tests. This preference has to be configured as unittest-style tests can also be run by pytest, meaning we can't rely on auto-discovery to choose the desired test runner. 2. I'd like to add venv auto-discovery similar to the feature currently provided by the terminal using detect_venv. 3. Unit tests. Unfortunately I'm struggling a bit with how to add settings in the appropriate location (e.g. Python language settings). Can anyone provide me with some pointers and/or examples on how to either add extra settings or to re-use the existing ones? My rust programming level is OK-ish but I'm not very familiar with the Zed project structure and could use some help. I'm also open for pair programming as mentioned on the website if that helps! Release Notes: - Added pytest-based test discovery and runnables for Python. - Adds a configurable option for switching between unittest and pytest as a test runner under Python language settings. Set "TASK_RUNNER" to "unittest" under task settings for Python if you wish to use unittest to run Python tasks; the default is pytest. --------- Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> --- crates/languages/src/python.rs | 228 +++++++++++++++++----- crates/languages/src/python/runnables.scm | 39 ++++ 2 files changed, 217 insertions(+), 50 deletions(-) diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 1e855777b2..a29eb1c679 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -4,6 +4,7 @@ use async_trait::async_trait; use collections::HashMap; use gpui::AsyncAppContext; use gpui::{AppContext, Task}; +use language::language_settings::language_settings; use language::LanguageName; use language::LanguageToolchainStore; use language::Toolchain; @@ -21,6 +22,7 @@ use serde_json::{json, Value}; use smol::{lock::OnceCell, process::Command}; use std::cmp::Ordering; +use std::str::FromStr; use std::sync::Mutex; use std::{ any::Any, @@ -35,6 +37,23 @@ use util::ResultExt; const SERVER_PATH: &str = "node_modules/pyright/langserver.index.js"; const NODE_MODULE_RELATIVE_SERVER_PATH: &str = "pyright/langserver.index.js"; +enum TestRunner { + UNITTEST, + PYTEST, +} + +impl FromStr for TestRunner { + type Err = (); + + fn from_str(s: &str) -> std::result::Result { + match s { + "unittest" => Ok(Self::UNITTEST), + "pytest" => Ok(Self::PYTEST), + _ => Err(()), + } + } +} + fn server_binary_arguments(server_path: &Path) -> Vec { vec![server_path.into(), "--stdio".into()] } @@ -265,8 +284,8 @@ async fn get_cached_server_binary( pub(crate) struct PythonContextProvider; -const PYTHON_UNITTEST_TARGET_TASK_VARIABLE: VariableName = - VariableName::Custom(Cow::Borrowed("PYTHON_UNITTEST_TARGET")); +const PYTHON_TEST_TARGET_TASK_VARIABLE: VariableName = + VariableName::Custom(Cow::Borrowed("PYTHON_TEST_TARGET")); const PYTHON_ACTIVE_TOOLCHAIN_PATH: VariableName = VariableName::Custom(Cow::Borrowed("PYTHON_ACTIVE_ZED_TOOLCHAIN")); @@ -279,28 +298,16 @@ impl ContextProvider for PythonContextProvider { toolchains: Arc, cx: &mut gpui::AppContext, ) -> Task> { - let python_module_name = python_module_name_from_relative_path( - variables.get(&VariableName::RelativeFile).unwrap_or(""), - ); - let unittest_class_name = - variables.get(&VariableName::Custom(Cow::Borrowed("_unittest_class_name"))); - let unittest_method_name = variables.get(&VariableName::Custom(Cow::Borrowed( - "_unittest_method_name", - ))); + let test_target = { + let test_runner = selected_test_runner(location.buffer.read(cx).file(), cx); - let unittest_target_str = match (unittest_class_name, unittest_method_name) { - (Some(class_name), Some(method_name)) => { - format!("{}.{}.{}", python_module_name, class_name, method_name) - } - (Some(class_name), None) => format!("{}.{}", python_module_name, class_name), - (None, None) => python_module_name, - (None, Some(_)) => return Task::ready(Ok(task::TaskVariables::default())), // should never happen, a TestCase class is the unit of testing + let runner = match test_runner { + TestRunner::UNITTEST => self.build_unittest_target(variables), + TestRunner::PYTEST => self.build_pytest_target(variables), + }; + runner }; - let unittest_target = ( - PYTHON_UNITTEST_TARGET_TASK_VARIABLE.clone(), - unittest_target_str, - ); let worktree_id = location.buffer.read(cx).file().map(|f| f.worktree_id(cx)); cx.spawn(move |mut cx| async move { let active_toolchain = if let Some(worktree_id) = worktree_id { @@ -312,53 +319,174 @@ impl ContextProvider for PythonContextProvider { String::from("python3") }; let toolchain = (PYTHON_ACTIVE_TOOLCHAIN_PATH, active_toolchain); - Ok(task::TaskVariables::from_iter([unittest_target, toolchain])) + Ok(task::TaskVariables::from_iter([test_target?, toolchain])) }) } fn associated_tasks( &self, - _: Option>, - _: &AppContext, + file: Option>, + cx: &AppContext, ) -> Option { - Some(TaskTemplates(vec![ + let test_runner = selected_test_runner(file.as_ref(), cx); + + let mut tasks = vec![ + // Execute a selection TaskTemplate { label: "execute selection".to_owned(), command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(), args: vec!["-c".to_owned(), VariableName::SelectedText.template_value()], ..TaskTemplate::default() }, + // Execute an entire file TaskTemplate { label: format!("run '{}'", VariableName::File.template_value()), command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(), args: vec![VariableName::File.template_value()], ..TaskTemplate::default() }, - TaskTemplate { - label: format!("unittest '{}'", VariableName::File.template_value()), - command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(), - args: vec![ - "-m".to_owned(), - "unittest".to_owned(), - VariableName::File.template_value(), - ], - ..TaskTemplate::default() - }, - TaskTemplate { - label: "unittest $ZED_CUSTOM_PYTHON_UNITTEST_TARGET".to_owned(), - command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(), - args: vec![ - "-m".to_owned(), - "unittest".to_owned(), - "$ZED_CUSTOM_PYTHON_UNITTEST_TARGET".to_owned(), - ], - tags: vec![ - "python-unittest-class".to_owned(), - "python-unittest-method".to_owned(), - ], - ..TaskTemplate::default() - }, - ])) + ]; + + tasks.extend(match test_runner { + TestRunner::UNITTEST => { + [ + // Run tests for an entire file + TaskTemplate { + label: format!("unittest '{}'", VariableName::File.template_value()), + command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(), + args: vec![ + "-m".to_owned(), + "unittest".to_owned(), + VariableName::File.template_value(), + ], + ..TaskTemplate::default() + }, + // Run test(s) for a specific target within a file + TaskTemplate { + label: "unittest $ZED_CUSTOM_PYTHON_TEST_TARGET".to_owned(), + command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(), + args: vec![ + "-m".to_owned(), + "unittest".to_owned(), + "$ZED_CUSTOM_PYTHON_TEST_TARGET".to_owned(), + ], + tags: vec![ + "python-unittest-class".to_owned(), + "python-unittest-method".to_owned(), + ], + ..TaskTemplate::default() + }, + ] + } + TestRunner::PYTEST => { + [ + // Run tests for an entire file + TaskTemplate { + label: format!("pytest '{}'", VariableName::File.template_value()), + command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(), + args: vec![ + "-m".to_owned(), + "pytest".to_owned(), + VariableName::File.template_value(), + ], + ..TaskTemplate::default() + }, + // Run test(s) for a specific target within a file + TaskTemplate { + label: "pytest $ZED_CUSTOM_PYTHON_TEST_TARGET".to_owned(), + command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(), + args: vec![ + "-m".to_owned(), + "pytest".to_owned(), + "$ZED_CUSTOM_PYTHON_TEST_TARGET".to_owned(), + ], + tags: vec![ + "python-pytest-class".to_owned(), + "python-pytest-method".to_owned(), + ], + ..TaskTemplate::default() + }, + ] + } + }); + + Some(TaskTemplates(tasks)) + } +} + +fn selected_test_runner(location: Option<&Arc>, cx: &AppContext) -> TestRunner { + const TEST_RUNNER_VARIABLE: &str = "TEST_RUNNER"; + language_settings(Some(LanguageName::new("Python")), location, cx) + .tasks + .variables + .get(TEST_RUNNER_VARIABLE) + .and_then(|val| TestRunner::from_str(val).ok()) + .unwrap_or(TestRunner::PYTEST) +} + +impl PythonContextProvider { + fn build_unittest_target( + &self, + variables: &task::TaskVariables, + ) -> Result<(VariableName, String)> { + let python_module_name = python_module_name_from_relative_path( + variables.get(&VariableName::RelativeFile).unwrap_or(""), + ); + + let unittest_class_name = + variables.get(&VariableName::Custom(Cow::Borrowed("_unittest_class_name"))); + + let unittest_method_name = variables.get(&VariableName::Custom(Cow::Borrowed( + "_unittest_method_name", + ))); + + let unittest_target_str = match (unittest_class_name, unittest_method_name) { + (Some(class_name), Some(method_name)) => { + format!("{}.{}.{}", python_module_name, class_name, method_name) + } + (Some(class_name), None) => format!("{}.{}", python_module_name, class_name), + (None, None) => python_module_name, + (None, Some(_)) => return Ok((VariableName::Custom(Cow::Borrowed("")), String::new())), // should never happen, a TestCase class is the unit of testing + }; + + let unittest_target = ( + PYTHON_TEST_TARGET_TASK_VARIABLE.clone(), + unittest_target_str, + ); + + Ok(unittest_target) + } + + fn build_pytest_target( + &self, + variables: &task::TaskVariables, + ) -> Result<(VariableName, String)> { + let file_path = variables + .get(&VariableName::RelativeFile) + .ok_or_else(|| anyhow!("No file path given"))?; + + let pytest_class_name = + variables.get(&VariableName::Custom(Cow::Borrowed("_pytest_class_name"))); + + let pytest_method_name = + variables.get(&VariableName::Custom(Cow::Borrowed("_pytest_method_name"))); + + let pytest_target_str = match (pytest_class_name, pytest_method_name) { + (Some(class_name), Some(method_name)) => { + format!("{}::{}::{}", file_path, class_name, method_name) + } + (Some(class_name), None) => { + format!("{}::{}", file_path, class_name) + } + (None, Some(method_name)) => { + format!("{}::{}", file_path, method_name) + } + (None, None) => file_path.to_string(), + }; + + let pytest_target = (PYTHON_TEST_TARGET_TASK_VARIABLE.clone(), pytest_target_str); + + Ok(pytest_target) } } diff --git a/crates/languages/src/python/runnables.scm b/crates/languages/src/python/runnables.scm index b9bc5e9bf2..31994dfa2c 100644 --- a/crates/languages/src/python/runnables.scm +++ b/crates/languages/src/python/runnables.scm @@ -29,3 +29,42 @@ ) ) ) + +; pytest functions +( + (module + (function_definition + name: (identifier) @run @_pytest_method_name + (#match? @_pytest_method_name "^test_") + ) @python-pytest-method + ) + (#set! tag python-pytest-method) +) + +; pytest classes +( + (module + (class_definition + name: (identifier) @run @_pytest_class_name + (#match? @_pytest_class_name "^Test") + ) + (#set! tag python-pytest-class) + ) +) + +; pytest class methods +( + (module + (class_definition + name: (identifier) @_pytest_class_name + (#match? @_pytest_class_name "^Test") + body: (block + (function_definition + name: (identifier) @run @_pytest_method_name + (#match? @_pytest_method_name "^test") + ) @python-pytest-method + (#set! tag python-pytest-method) + ) + ) + ) +) From 9454f0f1c7361da0344c5c9c3d876084cda1772b Mon Sep 17 00:00:00 2001 From: Egor Krugletsov <74310448+Poldraunic@users.noreply.github.com> Date: Tue, 19 Nov 2024 16:49:21 +0300 Subject: [PATCH 030/886] clangd: Use Url::to_file_path() to get actual file path for header/source (#20856) Using `Url::path()` seems fine on POSIX systems as it will leave forward slash (given hostname is empty). On Windows it will result in error. Release Notes: - N/A --- crates/editor/src/clangd_ext.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/crates/editor/src/clangd_ext.rs b/crates/editor/src/clangd_ext.rs index 501f81b107..c018362068 100644 --- a/crates/editor/src/clangd_ext.rs +++ b/crates/editor/src/clangd_ext.rs @@ -1,5 +1,3 @@ -use std::path::PathBuf; - use anyhow::Context as _; use gpui::{View, ViewContext, WindowContext}; use language::Language; @@ -54,9 +52,9 @@ pub fn switch_source_header( cx.spawn(|_editor, mut cx| async move { let switch_source_header = switch_source_header_task .await - .with_context(|| format!("Switch source/header LSP request for path \"{}\" failed", source_file))?; + .with_context(|| format!("Switch source/header LSP request for path \"{source_file}\" failed"))?; if switch_source_header.0.is_empty() { - log::info!("Clangd returned an empty string when requesting to switch source/header from \"{}\"", source_file); + log::info!("Clangd returned an empty string when requesting to switch source/header from \"{source_file}\"" ); return Ok(()); } @@ -67,14 +65,17 @@ pub fn switch_source_header( ) })?; + let path = goto.to_file_path().map_err(|()| { + anyhow::anyhow!("URL conversion to file path failed for \"{goto}\"") + })?; + workspace .update(&mut cx, |workspace, view_cx| { - workspace.open_abs_path(PathBuf::from(goto.path()), false, view_cx) + workspace.open_abs_path(path, false, view_cx) }) .with_context(|| { format!( - "Switch source/header could not open \"{}\" in workspace", - goto.path() + "Switch source/header could not open \"{goto}\" in workspace" ) })? .await From 6a2c7129908389c772f15a4890e83e2bea4a7e3c Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 19 Nov 2024 08:23:12 -0700 Subject: [PATCH 031/886] Use Instant not chrono for telemetry (#20756) We occasionally see dates in the future appearing in our telemetry. One hypothesis is that this is caused by a clock change while Zed is running causing date math based on chrono to be incorrect. Instant *should* be a more stable source of relative timestamps. Release Notes: - N/A --- Cargo.lock | 1 - crates/channel/src/channel_store_tests.rs | 2 +- crates/client/src/client.rs | 12 +++--- crates/client/src/telemetry.rs | 31 +++++++-------- .../client/src/telemetry/event_coalescer.rs | 39 +++++++------------ crates/clock/Cargo.toml | 1 - crates/clock/src/system_clock.rs | 30 ++++++-------- crates/collab/src/tests/test_server.rs | 2 +- crates/project/src/project.rs | 4 +- .../remote_server/src/remote_editing_tests.rs | 2 +- crates/semantic_index/examples/index.rs | 2 +- crates/workspace/src/workspace.rs | 2 +- 12 files changed, 54 insertions(+), 74 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7f5934fca8..38de2f5c00 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2473,7 +2473,6 @@ dependencies = [ name = "clock" version = "0.1.0" dependencies = [ - "chrono", "parking_lot", "serde", "smallvec", diff --git a/crates/channel/src/channel_store_tests.rs b/crates/channel/src/channel_store_tests.rs index 1cf9fa706d..11f618d196 100644 --- a/crates/channel/src/channel_store_tests.rs +++ b/crates/channel/src/channel_store_tests.rs @@ -343,7 +343,7 @@ fn init_test(cx: &mut AppContext) -> Model { release_channel::init(SemanticVersion::default(), cx); client::init_settings(cx); - let clock = Arc::new(FakeSystemClock::default()); + let clock = Arc::new(FakeSystemClock::new()); let http = FakeHttpClient::with_404_response(); let client = Client::new(clock, http.clone(), cx); let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx)); diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 1e73c7be66..041973e884 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -1780,7 +1780,7 @@ mod tests { let user_id = 5; let client = cx.update(|cx| { Client::new( - Arc::new(FakeSystemClock::default()), + Arc::new(FakeSystemClock::new()), FakeHttpClient::with_404_response(), cx, ) @@ -1821,7 +1821,7 @@ mod tests { let user_id = 5; let client = cx.update(|cx| { Client::new( - Arc::new(FakeSystemClock::default()), + Arc::new(FakeSystemClock::new()), FakeHttpClient::with_404_response(), cx, ) @@ -1900,7 +1900,7 @@ mod tests { let dropped_auth_count = Arc::new(Mutex::new(0)); let client = cx.update(|cx| { Client::new( - Arc::new(FakeSystemClock::default()), + Arc::new(FakeSystemClock::new()), FakeHttpClient::with_404_response(), cx, ) @@ -1943,7 +1943,7 @@ mod tests { let user_id = 5; let client = cx.update(|cx| { Client::new( - Arc::new(FakeSystemClock::default()), + Arc::new(FakeSystemClock::new()), FakeHttpClient::with_404_response(), cx, ) @@ -2003,7 +2003,7 @@ mod tests { let user_id = 5; let client = cx.update(|cx| { Client::new( - Arc::new(FakeSystemClock::default()), + Arc::new(FakeSystemClock::new()), FakeHttpClient::with_404_response(), cx, ) @@ -2038,7 +2038,7 @@ mod tests { let user_id = 5; let client = cx.update(|cx| { Client::new( - Arc::new(FakeSystemClock::default()), + Arc::new(FakeSystemClock::new()), FakeHttpClient::with_404_response(), cx, ) diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 27a49a9816..b472cf768e 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -2,7 +2,6 @@ mod event_coalescer; use crate::{ChannelId, TelemetrySettings}; use anyhow::Result; -use chrono::{DateTime, Utc}; use clock::SystemClock; use collections::{HashMap, HashSet}; use futures::Future; @@ -15,6 +14,7 @@ use settings::{Settings, SettingsStore}; use sha2::{Digest, Sha256}; use std::fs::File; use std::io::Write; +use std::time::Instant; use std::{env, mem, path::PathBuf, sync::Arc, time::Duration}; use sysinfo::{CpuRefreshKind, Pid, ProcessRefreshKind, RefreshKind, System}; use telemetry_events::{ @@ -46,7 +46,7 @@ struct TelemetryState { flush_events_task: Option>, log_file: Option, is_staff: Option, - first_event_date_time: Option>, + first_event_date_time: Option, event_coalescer: EventCoalescer, max_queue_size: usize, worktree_id_map: WorktreeIdMap, @@ -469,7 +469,10 @@ impl Telemetry { if let Some((start, end, environment)) = period_data { let event = Event::Edit(EditEvent { - duration: end.timestamp_millis() - start.timestamp_millis(), + duration: end + .saturating_duration_since(start) + .min(Duration::from_secs(60 * 60 * 24)) + .as_millis() as i64, environment: environment.to_string(), is_via_ssh, }); @@ -567,9 +570,10 @@ impl Telemetry { let date_time = self.clock.utc_now(); let milliseconds_since_first_event = match state.first_event_date_time { - Some(first_event_date_time) => { - date_time.timestamp_millis() - first_event_date_time.timestamp_millis() - } + Some(first_event_date_time) => date_time + .saturating_duration_since(first_event_date_time) + .min(Duration::from_secs(60 * 60 * 24)) + .as_millis() as i64, None => { state.first_event_date_time = Some(date_time); 0 @@ -702,7 +706,6 @@ pub fn calculate_json_checksum(json: &impl AsRef<[u8]>) -> Option { #[cfg(test)] mod tests { use super::*; - use chrono::TimeZone; use clock::FakeSystemClock; use gpui::TestAppContext; use http_client::FakeHttpClient; @@ -710,9 +713,7 @@ mod tests { #[gpui::test] fn test_telemetry_flush_on_max_queue_size(cx: &mut TestAppContext) { init_test(cx); - let clock = Arc::new(FakeSystemClock::new( - Utc.with_ymd_and_hms(1990, 4, 12, 12, 0, 0).unwrap(), - )); + let clock = Arc::new(FakeSystemClock::new()); let http = FakeHttpClient::with_200_response(); let system_id = Some("system_id".to_string()); let installation_id = Some("installation_id".to_string()); @@ -743,7 +744,7 @@ mod tests { Some(first_date_time) ); - clock.advance(chrono::Duration::milliseconds(100)); + clock.advance(Duration::from_millis(100)); let event = telemetry.report_app_event(operation.clone()); assert_eq!( @@ -759,7 +760,7 @@ mod tests { Some(first_date_time) ); - clock.advance(chrono::Duration::milliseconds(100)); + clock.advance(Duration::from_millis(100)); let event = telemetry.report_app_event(operation.clone()); assert_eq!( @@ -775,7 +776,7 @@ mod tests { Some(first_date_time) ); - clock.advance(chrono::Duration::milliseconds(100)); + clock.advance(Duration::from_millis(100)); // Adding a 4th event should cause a flush let event = telemetry.report_app_event(operation.clone()); @@ -796,9 +797,7 @@ mod tests { cx: &mut TestAppContext, ) { init_test(cx); - let clock = Arc::new(FakeSystemClock::new( - Utc.with_ymd_and_hms(1990, 4, 12, 12, 0, 0).unwrap(), - )); + let clock = Arc::new(FakeSystemClock::new()); let http = FakeHttpClient::with_200_response(); let system_id = Some("system_id".to_string()); let installation_id = Some("installation_id".to_string()); diff --git a/crates/client/src/telemetry/event_coalescer.rs b/crates/client/src/telemetry/event_coalescer.rs index 33bcf492f6..e58112ac08 100644 --- a/crates/client/src/telemetry/event_coalescer.rs +++ b/crates/client/src/telemetry/event_coalescer.rs @@ -1,7 +1,6 @@ -use std::sync::Arc; use std::time; +use std::{sync::Arc, time::Instant}; -use chrono::{DateTime, Duration, Utc}; use clock::SystemClock; const COALESCE_TIMEOUT: time::Duration = time::Duration::from_secs(20); @@ -10,8 +9,8 @@ const SIMULATED_DURATION_FOR_SINGLE_EVENT: time::Duration = time::Duration::from #[derive(Debug, PartialEq)] struct PeriodData { environment: &'static str, - start: DateTime, - end: Option>, + start: Instant, + end: Option, } pub struct EventCoalescer { @@ -27,9 +26,8 @@ impl EventCoalescer { pub fn log_event( &mut self, environment: &'static str, - ) -> Option<(DateTime, DateTime, &'static str)> { + ) -> Option<(Instant, Instant, &'static str)> { let log_time = self.clock.utc_now(); - let coalesce_timeout = Duration::from_std(COALESCE_TIMEOUT).unwrap(); let Some(state) = &mut self.state else { self.state = Some(PeriodData { @@ -43,7 +41,7 @@ impl EventCoalescer { let period_end = state .end .unwrap_or(state.start + SIMULATED_DURATION_FOR_SINGLE_EVENT); - let within_timeout = log_time - period_end < coalesce_timeout; + let within_timeout = log_time - period_end < COALESCE_TIMEOUT; let environment_is_same = state.environment == environment; let should_coaelesce = !within_timeout || !environment_is_same; @@ -70,16 +68,13 @@ impl EventCoalescer { #[cfg(test)] mod tests { - use chrono::TimeZone; use clock::FakeSystemClock; use super::*; #[test] fn test_same_context_exceeding_timeout() { - let clock = Arc::new(FakeSystemClock::new( - Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap(), - )); + let clock = Arc::new(FakeSystemClock::new()); let environment_1 = "environment_1"; let mut event_coalescer = EventCoalescer::new(clock.clone()); @@ -98,7 +93,7 @@ mod tests { }) ); - let within_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT / 2).unwrap(); + let within_timeout_adjustment = COALESCE_TIMEOUT / 2; // Ensure that many calls within the timeout don't start a new period for _ in 0..100 { @@ -118,7 +113,7 @@ mod tests { } let period_end = clock.utc_now(); - let exceed_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT * 2).unwrap(); + let exceed_timeout_adjustment = COALESCE_TIMEOUT * 2; // Logging an event exceeding the timeout should start a new period clock.advance(exceed_timeout_adjustment); let new_period_start = clock.utc_now(); @@ -137,9 +132,7 @@ mod tests { #[test] fn test_different_environment_under_timeout() { - let clock = Arc::new(FakeSystemClock::new( - Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap(), - )); + let clock = Arc::new(FakeSystemClock::new()); let environment_1 = "environment_1"; let mut event_coalescer = EventCoalescer::new(clock.clone()); @@ -158,7 +151,7 @@ mod tests { }) ); - let within_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT / 2).unwrap(); + let within_timeout_adjustment = COALESCE_TIMEOUT / 2; clock.advance(within_timeout_adjustment); let period_end = clock.utc_now(); let period_data = event_coalescer.log_event(environment_1); @@ -193,9 +186,7 @@ mod tests { #[test] fn test_switching_environment_while_within_timeout() { - let clock = Arc::new(FakeSystemClock::new( - Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap(), - )); + let clock = Arc::new(FakeSystemClock::new()); let environment_1 = "environment_1"; let mut event_coalescer = EventCoalescer::new(clock.clone()); @@ -214,7 +205,7 @@ mod tests { }) ); - let within_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT / 2).unwrap(); + let within_timeout_adjustment = COALESCE_TIMEOUT / 2; clock.advance(within_timeout_adjustment); let period_end = clock.utc_now(); let environment_2 = "environment_2"; @@ -240,9 +231,7 @@ mod tests { #[test] fn test_switching_environment_while_exceeding_timeout() { - let clock = Arc::new(FakeSystemClock::new( - Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap(), - )); + let clock = Arc::new(FakeSystemClock::new()); let environment_1 = "environment_1"; let mut event_coalescer = EventCoalescer::new(clock.clone()); @@ -261,7 +250,7 @@ mod tests { }) ); - let exceed_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT * 2).unwrap(); + let exceed_timeout_adjustment = COALESCE_TIMEOUT * 2; clock.advance(exceed_timeout_adjustment); let period_end = clock.utc_now(); let environment_2 = "environment_2"; diff --git a/crates/clock/Cargo.toml b/crates/clock/Cargo.toml index 699a50e70d..b6f28741c3 100644 --- a/crates/clock/Cargo.toml +++ b/crates/clock/Cargo.toml @@ -16,7 +16,6 @@ doctest = false test-support = ["dep:parking_lot"] [dependencies] -chrono.workspace = true parking_lot = { workspace = true, optional = true } serde.workspace = true smallvec.workspace = true diff --git a/crates/clock/src/system_clock.rs b/crates/clock/src/system_clock.rs index a462ffc35b..b8e50d0b27 100644 --- a/crates/clock/src/system_clock.rs +++ b/crates/clock/src/system_clock.rs @@ -1,21 +1,21 @@ -use chrono::{DateTime, Utc}; +use std::time::Instant; pub trait SystemClock: Send + Sync { /// Returns the current date and time in UTC. - fn utc_now(&self) -> DateTime; + fn utc_now(&self) -> Instant; } pub struct RealSystemClock; impl SystemClock for RealSystemClock { - fn utc_now(&self) -> DateTime { - Utc::now() + fn utc_now(&self) -> Instant { + Instant::now() } } #[cfg(any(test, feature = "test-support"))] pub struct FakeSystemClockState { - now: DateTime, + now: Instant, } #[cfg(any(test, feature = "test-support"))] @@ -24,36 +24,30 @@ pub struct FakeSystemClock { state: parking_lot::Mutex, } -#[cfg(any(test, feature = "test-support"))] -impl Default for FakeSystemClock { - fn default() -> Self { - Self::new(Utc::now()) - } -} - #[cfg(any(test, feature = "test-support"))] impl FakeSystemClock { - pub fn new(now: DateTime) -> Self { - let state = FakeSystemClockState { now }; + pub fn new() -> Self { + let state = FakeSystemClockState { + now: Instant::now(), + }; Self { state: parking_lot::Mutex::new(state), } } - pub fn set_now(&self, now: DateTime) { + pub fn set_now(&self, now: Instant) { self.state.lock().now = now; } - /// Advances the [`FakeSystemClock`] by the specified [`Duration`](chrono::Duration). - pub fn advance(&self, duration: chrono::Duration) { + pub fn advance(&self, duration: std::time::Duration) { self.state.lock().now += duration; } } #[cfg(any(test, feature = "test-support"))] impl SystemClock for FakeSystemClock { - fn utc_now(&self) -> DateTime { + fn utc_now(&self) -> Instant { self.state.lock().now } } diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index 17cd1b51c4..8a09f06092 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -168,7 +168,7 @@ impl TestServer { client::init_settings(cx); }); - let clock = Arc::new(FakeSystemClock::default()); + let clock = Arc::new(FakeSystemClock::new()); let http = FakeHttpClient::with_404_response(); let user_id = if let Ok(Some(user)) = self.app_state.db.get_user_by_github_login(name).await { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 17f84a6f37..2b18659b7d 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1133,7 +1133,7 @@ impl Project { let fs = Arc::new(RealFs::default()); let languages = LanguageRegistry::test(cx.background_executor().clone()); - let clock = Arc::new(FakeSystemClock::default()); + let clock = Arc::new(FakeSystemClock::new()); let http_client = http_client::FakeHttpClient::with_404_response(); let client = cx .update(|cx| client::Client::new(clock, http_client.clone(), cx)) @@ -1179,7 +1179,7 @@ impl Project { use gpui::Context; let languages = LanguageRegistry::test(cx.executor()); - let clock = Arc::new(FakeSystemClock::default()); + let clock = Arc::new(FakeSystemClock::new()); let http_client = http_client::FakeHttpClient::with_404_response(); let client = cx.update(|cx| client::Client::new(clock, http_client.clone(), cx)); let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx)); diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index e3914c7ae1..3a9803287a 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -1277,7 +1277,7 @@ fn build_project(ssh: Model, cx: &mut TestAppContext) -> Model< let client = cx.update(|cx| { Client::new( - Arc::new(FakeSystemClock::default()), + Arc::new(FakeSystemClock::new()), FakeHttpClient::with_404_response(), cx, ) diff --git a/crates/semantic_index/examples/index.rs b/crates/semantic_index/examples/index.rs index 2efd94cb57..25e03f5b3a 100644 --- a/crates/semantic_index/examples/index.rs +++ b/crates/semantic_index/examples/index.rs @@ -25,7 +25,7 @@ fn main() { store.update_user_settings::(cx, |_| {}); }); - let clock = Arc::new(FakeSystemClock::default()); + let clock = Arc::new(FakeSystemClock::new()); let http = Arc::new(HttpClientWithUrl::new( Arc::new( diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 833a8b15a0..32e441ee50 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -606,7 +606,7 @@ impl AppState { let fs = fs::FakeFs::new(cx.background_executor().clone()); let languages = Arc::new(LanguageRegistry::test(cx.background_executor().clone())); - let clock = Arc::new(clock::FakeSystemClock::default()); + let clock = Arc::new(clock::FakeSystemClock::new()); let http_client = http_client::FakeHttpClient::with_404_response(); let client = Client::new(clock, http_client.clone(), cx); let session = cx.new_model(|cx| AppSession::new(Session::test(), cx)); From f5cbfa718e5bb4a284d0c2bfac6b425e4d7a343d Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 19 Nov 2024 11:20:30 -0500 Subject: [PATCH 032/886] assistant: Fix evaluating slash commands in slash command output (like `/default`) (#20864) This PR fixes an issue where slash commands in the output of other slash commands were not being evaluated when configured to do so. Closes https://github.com/zed-industries/zed/issues/20820. Release Notes: - Fixed slash commands from other slash commands (like `/default`) not being evaluated (Preview only). --- crates/assistant/src/assistant_panel.rs | 55 +++++++++++-------- crates/assistant/src/context.rs | 16 ++++-- .../src/slash_command/default_command.rs | 4 ++ 3 files changed, 45 insertions(+), 30 deletions(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index b682bfdcca..c89595c7da 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -2050,30 +2050,6 @@ impl ContextEditor { ContextEvent::SlashCommandOutputSectionAdded { section } => { self.insert_slash_command_output_sections([section.clone()], false, cx); } - ContextEvent::SlashCommandFinished { - output_range: _output_range, - run_commands_in_ranges, - } => { - for range in run_commands_in_ranges { - let commands = self.context.update(cx, |context, cx| { - context.reparse(cx); - context - .pending_commands_for_range(range.clone(), cx) - .to_vec() - }); - - for command in commands { - self.run_command( - command.source_range, - &command.name, - &command.arguments, - false, - self.workspace.clone(), - cx, - ); - } - } - } ContextEvent::UsePendingTools => { let pending_tool_uses = self .context @@ -2152,6 +2128,37 @@ impl ContextEditor { command_id: InvokedSlashCommandId, cx: &mut ViewContext, ) { + if let Some(invoked_slash_command) = + self.context.read(cx).invoked_slash_command(&command_id) + { + if let InvokedSlashCommandStatus::Finished = invoked_slash_command.status { + let run_commands_in_ranges = invoked_slash_command + .run_commands_in_ranges + .iter() + .cloned() + .collect::>(); + for range in run_commands_in_ranges { + let commands = self.context.update(cx, |context, cx| { + context.reparse(cx); + context + .pending_commands_for_range(range.clone(), cx) + .to_vec() + }); + + for command in commands { + self.run_command( + command.source_range, + &command.name, + &command.arguments, + false, + self.workspace.clone(), + cx, + ); + } + } + } + } + self.editor.update(cx, |editor, cx| { if let Some(invoked_slash_command) = self.context.read(cx).invoked_slash_command(&command_id) diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index d6f0a48868..39c31d7c58 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -381,10 +381,6 @@ pub enum ContextEvent { SlashCommandOutputSectionAdded { section: SlashCommandOutputSection, }, - SlashCommandFinished { - output_range: Range, - run_commands_in_ranges: Vec>, - }, UsePendingTools, ToolFinished { tool_use_id: Arc, @@ -916,6 +912,7 @@ impl Context { InvokedSlashCommand { name: name.into(), range: output_range, + run_commands_in_ranges: Vec::new(), status: InvokedSlashCommandStatus::Running(Task::ready(())), transaction: None, timestamp: id.0, @@ -1914,7 +1911,6 @@ impl Context { } let mut pending_section_stack: Vec = Vec::new(); - let mut run_commands_in_ranges: Vec> = Vec::new(); let mut last_role: Option = None; let mut last_section_range = None; @@ -1980,7 +1976,13 @@ impl Context { let end = this.buffer.read(cx).anchor_before(insert_position); if run_commands_in_text { - run_commands_in_ranges.push(start..end); + if let Some(invoked_slash_command) = + this.invoked_slash_commands.get_mut(&command_id) + { + invoked_slash_command + .run_commands_in_ranges + .push(start..end); + } } } SlashCommandEvent::EndSection => { @@ -2100,6 +2102,7 @@ impl Context { InvokedSlashCommand { name: name.to_string().into(), range: command_range.clone(), + run_commands_in_ranges: Vec::new(), status: InvokedSlashCommandStatus::Running(insert_output_task), transaction: Some(first_transaction), timestamp: command_id.0, @@ -3176,6 +3179,7 @@ pub struct ParsedSlashCommand { pub struct InvokedSlashCommand { pub name: SharedString, pub range: Range, + pub run_commands_in_ranges: Vec>, pub status: InvokedSlashCommandStatus, pub transaction: Option, timestamp: clock::Lamport, diff --git a/crates/assistant/src/slash_command/default_command.rs b/crates/assistant/src/slash_command/default_command.rs index 4d9c9e2ae4..49a7b244e9 100644 --- a/crates/assistant/src/slash_command/default_command.rs +++ b/crates/assistant/src/slash_command/default_command.rs @@ -69,6 +69,10 @@ impl SlashCommand for DefaultSlashCommand { text.push('\n'); } + if !text.ends_with('\n') { + text.push('\n'); + } + Ok(SlashCommandOutput { sections: vec![SlashCommandOutputSection { range: 0..text.len(), From 7853e32f8093ab6195d89a490ff43f0ec62ba0e1 Mon Sep 17 00:00:00 2001 From: Jaagup Averin Date: Tue, 19 Nov 2024 19:53:36 +0200 Subject: [PATCH 033/886] python: Highlight attribute docstrings (#20763) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds more docstring highlights missing from #20486. [PEP257](https://peps.python.org/pep-0257/) defines attribute docstrings as > String literals occurring immediately after a simple assignment at the top level of a module, class, or __init__ method are called “attribute docstrings”. This PR adds `@string.doc` for such cases. Before: ![Screenshot_20241116_162257](https://github.com/user-attachments/assets/6b471cff-717e-4755-9291-d596da927dc6) After: ![Screenshot_20241116_162457](https://github.com/user-attachments/assets/96674157-9c86-45b6-8ce9-e433ca0ae8ea) Release Notes: - Added Python syntax highlighting for attribute docstrings. --------- Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> --- crates/languages/src/python/highlights.scm | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/crates/languages/src/python/highlights.scm b/crates/languages/src/python/highlights.scm index 78e5126d40..6c3f027c19 100644 --- a/crates/languages/src/python/highlights.scm +++ b/crates/languages/src/python/highlights.scm @@ -98,6 +98,25 @@ (parameters)? body: (block (expression_statement (string) @string.doc))) +(module + (expression_statement (assignment)) + . (expression_statement (string) @string.doc)) + +(class_definition + body: (block + (expression_statement (assignment)) + . (expression_statement (string) @string.doc))) + +(class_definition + body: (block + (function_definition + name: (identifier) @function.method.constructor + (#eq? @function.method.constructor "__init__") + body: (block + (expression_statement (assignment)) + . (expression_statement (string) @string.doc))))) + + [ "-" "-=" From 5c6565a9e0d164c5c91f0820aa7da97da23aede3 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 19 Nov 2024 19:49:36 +0100 Subject: [PATCH 034/886] editor: Use completion filter_range for fuzzy matching (#20869) Fixes regression from #13958 Closes #20868 Release Notes: - N/A --- crates/editor/src/editor.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 6167c24bff..d303ecf0f3 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1036,7 +1036,12 @@ impl CompletionsMenu { let match_candidates = completions .iter() .enumerate() - .map(|(id, completion)| StringMatchCandidate::new(id, completion.label.text.clone())) + .map(|(id, completion)| { + StringMatchCandidate::new( + id, + completion.label.text[completion.label.filter_range.clone()].into(), + ) + }) .collect(); Self { From 496dae968b8fc09118bd60ae7299ab9d44936f16 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 19 Nov 2024 12:25:16 -0700 Subject: [PATCH 035/886] Remove old CPU/Memory events (#20865) Release Notes: - Telemetry: stop reporting CPU/RAM on a timer --- .github/workflows/ci.yml | 18 ++++++++- Cargo.lock | 1 - crates/client/Cargo.toml | 1 - crates/client/src/telemetry.rs | 70 +-------------------------------- crates/collab/src/api/events.rs | 32 ++++----------- 5 files changed, 25 insertions(+), 97 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bed52955ad..ee6f81dd84 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -353,7 +353,6 @@ jobs: files: | target/zed-remote-server-linux-x86_64.gz target/release/zed-linux-x86_64.tar.gz - body: "" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -400,6 +399,21 @@ jobs: files: | target/zed-remote-server-linux-aarch64.gz target/release/zed-linux-aarch64.tar.gz - body: "" + 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/Cargo.lock b/Cargo.lock index 38de2f5c00..527190baca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2456,7 +2456,6 @@ dependencies = [ "settings", "sha2", "smol", - "sysinfo", "telemetry_events", "text", "thiserror 1.0.69", diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index 9892011297..23716f0c69 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -42,7 +42,6 @@ serde_json.workspace = true settings.workspace = true sha2.workspace = true smol.workspace = true -sysinfo.workspace = true telemetry_events.workspace = true text.workspace = true thiserror.workspace = true diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index b472cf768e..fcb9ced4e5 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -16,11 +16,9 @@ use std::fs::File; use std::io::Write; use std::time::Instant; use std::{env, mem, path::PathBuf, sync::Arc, time::Duration}; -use sysinfo::{CpuRefreshKind, Pid, ProcessRefreshKind, RefreshKind, System}; use telemetry_events::{ - ActionEvent, AppEvent, AssistantEvent, CallEvent, CpuEvent, EditEvent, EditorEvent, Event, - EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent, MemoryEvent, ReplEvent, - SettingEvent, + ActionEvent, AppEvent, AssistantEvent, CallEvent, EditEvent, EditorEvent, Event, + EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent, ReplEvent, SettingEvent, }; use util::{ResultExt, TryFutureExt}; use worktree::{UpdatedEntriesSet, WorktreeId}; @@ -293,48 +291,6 @@ impl Telemetry { state.session_id = Some(session_id); state.app_version = release_channel::AppVersion::global(cx).to_string(); state.os_name = os_name(); - - drop(state); - - let this = self.clone(); - cx.background_executor() - .spawn(async move { - let mut system = System::new_with_specifics( - RefreshKind::new().with_cpu(CpuRefreshKind::everything()), - ); - - let refresh_kind = ProcessRefreshKind::new().with_cpu().with_memory(); - let current_process = Pid::from_u32(std::process::id()); - system.refresh_processes_specifics( - sysinfo::ProcessesToUpdate::Some(&[current_process]), - refresh_kind, - ); - - // Waiting some amount of time before the first query is important to get a reasonable value - // https://docs.rs/sysinfo/0.29.10/sysinfo/trait.ProcessExt.html#tymethod.cpu_usage - const DURATION_BETWEEN_SYSTEM_EVENTS: Duration = Duration::from_secs(4 * 60); - - loop { - smol::Timer::after(DURATION_BETWEEN_SYSTEM_EVENTS).await; - - let current_process = Pid::from_u32(std::process::id()); - system.refresh_processes_specifics( - sysinfo::ProcessesToUpdate::Some(&[current_process]), - refresh_kind, - ); - let Some(process) = system.process(current_process) else { - log::error!( - "Failed to find own process {current_process:?} in system process table" - ); - // TODO: Fire an error telemetry event - return; - }; - - this.report_memory_event(process.memory(), process.virtual_memory()); - this.report_cpu_event(process.cpu_usage(), system.cpus().len() as u32); - } - }) - .detach(); } pub fn metrics_enabled(self: &Arc) -> bool { @@ -416,28 +372,6 @@ impl Telemetry { self.report_event(event) } - pub fn report_cpu_event(self: &Arc, usage_as_percentage: f32, core_count: u32) { - let event = Event::Cpu(CpuEvent { - usage_as_percentage, - core_count, - }); - - self.report_event(event) - } - - pub fn report_memory_event( - self: &Arc, - memory_in_bytes: u64, - virtual_memory_in_bytes: u64, - ) { - let event = Event::Memory(MemoryEvent { - memory_in_bytes, - virtual_memory_in_bytes, - }); - - self.report_event(event) - } - pub fn report_app_event(self: &Arc, operation: String) -> Event { let event = Event::App(AppEvent { operation }); diff --git a/crates/collab/src/api/events.rs b/crates/collab/src/api/events.rs index 053657a8d1..80f477df30 100644 --- a/crates/collab/src/api/events.rs +++ b/crates/collab/src/api/events.rs @@ -483,20 +483,7 @@ pub async fn post_events( checksum_matched, )) } - Event::Cpu(event) => to_upload.cpu_events.push(CpuEventRow::from_event( - event.clone(), - wrapper, - &request_body, - first_event_at, - checksum_matched, - )), - Event::Memory(event) => to_upload.memory_events.push(MemoryEventRow::from_event( - event.clone(), - wrapper, - &request_body, - first_event_at, - checksum_matched, - )), + Event::Cpu(_) | Event::Memory(_) => continue, Event::App(event) => to_upload.app_events.push(AppEventRow::from_event( event.clone(), wrapper, @@ -947,6 +934,7 @@ pub struct CpuEventRow { } impl CpuEventRow { + #[allow(unused)] fn from_event( event: CpuEvent, wrapper: &EventWrapper, @@ -1001,6 +989,7 @@ pub struct MemoryEventRow { } impl MemoryEventRow { + #[allow(unused)] fn from_event( event: MemoryEvent, wrapper: &EventWrapper, @@ -1393,7 +1382,7 @@ fn for_snowflake( body: EventRequestBody, first_event_at: chrono::DateTime, ) -> impl Iterator { - body.events.into_iter().map(move |event| { + body.events.into_iter().flat_map(move |event| { let timestamp = first_event_at + Duration::milliseconds(event.milliseconds_since_first_event); let (event_type, mut event_properties) = match &event.event { @@ -1450,14 +1439,7 @@ fn for_snowflake( }, serde_json::to_value(e).unwrap(), ), - Event::Cpu(e) => ( - "System CPU Sampled".to_string(), - serde_json::to_value(e).unwrap(), - ), - Event::Memory(e) => ( - "System Memory Sampled".to_string(), - serde_json::to_value(e).unwrap(), - ), + Event::Cpu(_) | Event::Memory(_) => return None, Event::App(e) => { let mut properties = json!({}); let event_type = match e.operation.trim() { @@ -1577,7 +1559,7 @@ fn for_snowflake( "is_staff": body.is_staff, })); - SnowflakeRow { + Some(SnowflakeRow { time: timestamp, user_id: body.metrics_id.clone(), device_id: body.system_id.clone(), @@ -1585,7 +1567,7 @@ fn for_snowflake( event_properties, user_properties, insert_id: Some(Uuid::new_v4().to_string()), - } + }) }) } From 1c2b3ad782fef847d6083d11252e06732f1d2c42 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Tue, 19 Nov 2024 19:33:35 +0000 Subject: [PATCH 036/886] Add editor::SelectAllMatches to SublimeText base keymap (#20866) `alt-f3` on Linux `ctrl-cmd-g` on MacOS Co-authored-by: Roman Seidelsohn --- assets/keymaps/linux/sublime_text.json | 1 + assets/keymaps/macos/sublime_text.json | 1 + 2 files changed, 2 insertions(+) diff --git a/assets/keymaps/linux/sublime_text.json b/assets/keymaps/linux/sublime_text.json index c4bffb56b0..57ef4b876b 100644 --- a/assets/keymaps/linux/sublime_text.json +++ b/assets/keymaps/linux/sublime_text.json @@ -16,6 +16,7 @@ "ctrl-shift-l": "editor::SplitSelectionIntoLines", "ctrl-shift-a": "editor::SelectLargerSyntaxNode", "ctrl-shift-d": "editor::DuplicateLineDown", + "alt-f3": "editor::SelectAllMatches", // find_all_under "f12": "editor::GoToDefinition", "ctrl-f12": "editor::GoToDefinitionSplit", "shift-f12": "editor::FindAllReferences", diff --git a/assets/keymaps/macos/sublime_text.json b/assets/keymaps/macos/sublime_text.json index dd57386424..f4c09b5144 100644 --- a/assets/keymaps/macos/sublime_text.json +++ b/assets/keymaps/macos/sublime_text.json @@ -19,6 +19,7 @@ "cmd-shift-l": "editor::SplitSelectionIntoLines", "cmd-shift-a": "editor::SelectLargerSyntaxNode", "cmd-shift-d": "editor::DuplicateLineDown", + "ctrl-cmd-g": "editor::SelectAllMatches", // find_all_under "shift-f12": "editor::FindAllReferences", "alt-cmd-down": "editor::GoToDefinition", "ctrl-alt-cmd-down": "editor::GoToDefinitionSplit", From ea5131ce0a9d753e81969d6f55efb30882eb5c7c Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 19 Nov 2024 12:52:00 -0700 Subject: [PATCH 037/886] Country Code To Snowflake (#20875) Release Notes: - N/A --------- Co-authored-by: Nathan Sobo --- crates/collab/src/api/events.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/collab/src/api/events.rs b/crates/collab/src/api/events.rs index 80f477df30..57ac43ca56 100644 --- a/crates/collab/src/api/events.rs +++ b/crates/collab/src/api/events.rs @@ -418,7 +418,7 @@ pub async fn post_events( if let Some(kinesis_client) = app.kinesis_client.clone() { if let Some(stream) = app.config.kinesis_stream.clone() { let mut request = kinesis_client.put_records().stream_name(stream); - for row in for_snowflake(request_body.clone(), first_event_at) { + for row in for_snowflake(request_body.clone(), first_event_at, country_code.clone()) { if let Some(data) = serde_json::to_vec(&row).log_err() { request = request.records( aws_sdk_kinesis::types::PutRecordsRequestEntry::builder() @@ -1381,6 +1381,7 @@ pub fn calculate_json_checksum(app: Arc, json: &impl AsRef<[u8]>) -> O fn for_snowflake( body: EventRequestBody, first_event_at: chrono::DateTime, + country_code: Option, ) -> impl Iterator { body.events.into_iter().flat_map(move |event| { let timestamp = @@ -1553,6 +1554,9 @@ fn for_snowflake( body.release_channel.clone().into(), ); 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()); + } } let user_properties = Some(serde_json::json!({ From f77b6ab79c6fc8619b7853934519ce1c19c6b3fd Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 19 Nov 2024 13:43:24 -0700 Subject: [PATCH 038/886] 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 039/886] 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 040/886] 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 041/886] 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 042/886] 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 043/886] 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 044/886] 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 045/886] 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 046/886] 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 047/886] 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 048/886] 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 049/886] 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 050/886] 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 051/886] 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 052/886] 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 053/886] 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 054/886] 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 055/886] 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 056/886] 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 057/886] 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 058/886] 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 059/886] 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 060/886] 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 061/886] 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 062/886] 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 063/886] 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 064/886] 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 065/886] 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 066/886] 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 067/886] 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 068/886] 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 069/886] 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 070/886] 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 071/886] 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 072/886] 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 073/886] 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 074/886] 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 075/886] 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 076/886] 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 077/886] 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 078/886] 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 079/886] 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 080/886] 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 081/886] 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 082/886] 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 083/886] 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 084/886] 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 085/886] 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 086/886] 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 087/886] 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 088/886] 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 089/886] 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 090/886] 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 091/886] 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 092/886] 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 093/886] 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 094/886] 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 095/886] 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 096/886] 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 097/886] 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 098/886] 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 099/886] 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 100/886] 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 101/886] 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 102/886] 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 103/886] 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 104/886] 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 105/886] 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() From ca769480443caf7137466713b15dbda442dadc07 Mon Sep 17 00:00:00 2001 From: william341 Date: Fri, 22 Nov 2024 10:51:26 -0700 Subject: [PATCH 106/886] gpui: Add drop_image (#19772) This PR adds a function, WindowContext::drop_image, to manually remove a RenderImage from the sprite atlas. In addition, PlatformAtlas::remove was added to support this behavior. Previously, there was no way to request a RenderImage to be removed from the sprite atlas, and since they are not removed automatically the sprite would remain in video memory once added until the window was closed. This PR allows a developer to request the image be dropped from memory manually, however it does not add automatic removal. Release Notes: - N/A --------- Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Co-authored-by: Mikayla Maki --- crates/gpui/src/platform.rs | 37 +++++++ crates/gpui/src/platform/blade/blade_atlas.rs | 96 ++++++++++++---- crates/gpui/src/platform/mac/metal_atlas.rs | 104 ++++++++++++++---- crates/gpui/src/platform/test/window.rs | 5 + crates/gpui/src/window.rs | 14 +++ 5 files changed, 208 insertions(+), 48 deletions(-) diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 76a575724f..8228d44bb4 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -46,6 +46,7 @@ use smallvec::SmallVec; use std::borrow::Cow; use std::hash::{Hash, Hasher}; use std::io::Cursor; +use std::ops; use std::time::{Duration, Instant}; use std::{ fmt::{self, Debug}, @@ -561,6 +562,42 @@ pub(crate) trait PlatformAtlas: Send + Sync { key: &AtlasKey, build: &mut dyn FnMut() -> Result, Cow<'a, [u8]>)>>, ) -> Result>; + fn remove(&self, key: &AtlasKey); +} + +struct AtlasTextureList { + textures: Vec>, + free_list: Vec, +} + +impl Default for AtlasTextureList { + fn default() -> Self { + Self { + textures: Vec::default(), + free_list: Vec::default(), + } + } +} + +impl ops::Index for AtlasTextureList { + type Output = Option; + + fn index(&self, index: usize) -> &Self::Output { + &self.textures[index] + } +} + +impl AtlasTextureList { + #[allow(unused)] + fn drain(&mut self) -> std::vec::Drain> { + self.free_list.clear(); + self.textures.drain(..) + } + + #[allow(dead_code)] + fn iter_mut(&mut self) -> impl DoubleEndedIterator { + self.textures.iter_mut().flatten() + } } #[derive(Clone, Debug, PartialEq, Eq)] diff --git a/crates/gpui/src/platform/blade/blade_atlas.rs b/crates/gpui/src/platform/blade/blade_atlas.rs index e6d5dc8ee9..b876d5bb9b 100644 --- a/crates/gpui/src/platform/blade/blade_atlas.rs +++ b/crates/gpui/src/platform/blade/blade_atlas.rs @@ -1,6 +1,6 @@ use crate::{ - AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds, DevicePixels, PlatformAtlas, - Point, Size, + platform::AtlasTextureList, AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds, + DevicePixels, PlatformAtlas, Point, Size, }; use anyhow::Result; use blade_graphics as gpu; @@ -67,7 +67,7 @@ impl BladeAtlas { pub(crate) fn clear_textures(&self, texture_kind: AtlasTextureKind) { let mut lock = self.0.lock(); let textures = &mut lock.storage[texture_kind]; - for texture in textures { + for texture in textures.iter_mut() { texture.clear(); } } @@ -130,19 +130,48 @@ impl PlatformAtlas for BladeAtlas { Ok(Some(tile)) } } + + fn remove(&self, key: &AtlasKey) { + let mut lock = self.0.lock(); + + let Some(id) = lock.tiles_by_key.remove(key).map(|tile| tile.texture_id) else { + return; + }; + + let Some(texture_slot) = lock.storage[id.kind].textures.get_mut(id.index as usize) else { + return; + }; + + if let Some(mut texture) = texture_slot.take() { + texture.decrement_ref_count(); + if texture.is_unreferenced() { + lock.storage[id.kind] + .free_list + .push(texture.id.index as usize); + texture.destroy(&lock.gpu); + } else { + *texture_slot = Some(texture); + } + } + } } impl BladeAtlasState { fn allocate(&mut self, size: Size, texture_kind: AtlasTextureKind) -> AtlasTile { - let textures = &mut self.storage[texture_kind]; - textures - .iter_mut() - .rev() - .find_map(|texture| texture.allocate(size)) - .unwrap_or_else(|| { - let texture = self.push_texture(size, texture_kind); - texture.allocate(size).unwrap() - }) + { + let textures = &mut self.storage[texture_kind]; + + if let Some(tile) = textures + .iter_mut() + .rev() + .find_map(|texture| texture.allocate(size)) + { + return tile; + } + } + + let texture = self.push_texture(size, texture_kind); + texture.allocate(size).unwrap() } fn push_texture( @@ -198,21 +227,30 @@ impl BladeAtlasState { }, ); - let textures = &mut self.storage[kind]; + let texture_list = &mut self.storage[kind]; + let index = texture_list.free_list.pop(); + let atlas_texture = BladeAtlasTexture { id: AtlasTextureId { - index: textures.len() as u32, + index: index.unwrap_or(texture_list.textures.len()) as u32, kind, }, allocator: etagere::BucketedAtlasAllocator::new(size.into()), format, raw, raw_view, + live_atlas_keys: 0, }; self.initializations.push(atlas_texture.id); - textures.push(atlas_texture); - textures.last_mut().unwrap() + + if let Some(ix) = index { + texture_list.textures[ix] = Some(atlas_texture); + texture_list.textures.get_mut(ix).unwrap().as_mut().unwrap() + } else { + texture_list.textures.push(Some(atlas_texture)); + texture_list.textures.last_mut().unwrap().as_mut().unwrap() + } } fn upload_texture(&mut self, id: AtlasTextureId, bounds: Bounds, bytes: &[u8]) { @@ -258,13 +296,13 @@ impl BladeAtlasState { #[derive(Default)] struct BladeAtlasStorage { - monochrome_textures: Vec, - polychrome_textures: Vec, - path_textures: Vec, + monochrome_textures: AtlasTextureList, + polychrome_textures: AtlasTextureList, + path_textures: AtlasTextureList, } impl ops::Index for BladeAtlasStorage { - type Output = Vec; + type Output = AtlasTextureList; fn index(&self, kind: AtlasTextureKind) -> &Self::Output { match kind { crate::AtlasTextureKind::Monochrome => &self.monochrome_textures, @@ -292,19 +330,19 @@ impl ops::Index for BladeAtlasStorage { crate::AtlasTextureKind::Polychrome => &self.polychrome_textures, crate::AtlasTextureKind::Path => &self.path_textures, }; - &textures[id.index as usize] + textures[id.index as usize].as_ref().unwrap() } } impl BladeAtlasStorage { fn destroy(&mut self, gpu: &gpu::Context) { - for mut texture in self.monochrome_textures.drain(..) { + for mut texture in self.monochrome_textures.drain().flatten() { texture.destroy(gpu); } - for mut texture in self.polychrome_textures.drain(..) { + for mut texture in self.polychrome_textures.drain().flatten() { texture.destroy(gpu); } - for mut texture in self.path_textures.drain(..) { + for mut texture in self.path_textures.drain().flatten() { texture.destroy(gpu); } } @@ -316,6 +354,7 @@ struct BladeAtlasTexture { raw: gpu::Texture, raw_view: gpu::TextureView, format: gpu::TextureFormat, + live_atlas_keys: u32, } impl BladeAtlasTexture { @@ -334,6 +373,7 @@ impl BladeAtlasTexture { size, }, }; + self.live_atlas_keys += 1; Some(tile) } @@ -345,6 +385,14 @@ impl BladeAtlasTexture { fn bytes_per_pixel(&self) -> u8 { self.format.block_info().size } + + fn decrement_ref_count(&mut self) { + self.live_atlas_keys -= 1; + } + + fn is_unreferenced(&mut self) -> bool { + self.live_atlas_keys == 0 + } } impl From> for etagere::Size { diff --git a/crates/gpui/src/platform/mac/metal_atlas.rs b/crates/gpui/src/platform/mac/metal_atlas.rs index 89a6987752..ca595c5ce3 100644 --- a/crates/gpui/src/platform/mac/metal_atlas.rs +++ b/crates/gpui/src/platform/mac/metal_atlas.rs @@ -1,6 +1,6 @@ use crate::{ - AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds, DevicePixels, PlatformAtlas, - Point, Size, + platform::AtlasTextureList, AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds, + DevicePixels, PlatformAtlas, Point, Size, }; use anyhow::{anyhow, Result}; use collections::FxHashMap; @@ -42,7 +42,7 @@ impl MetalAtlas { AtlasTextureKind::Polychrome => &mut lock.polychrome_textures, AtlasTextureKind::Path => &mut lock.path_textures, }; - for texture in textures { + for texture in textures.iter_mut() { texture.clear(); } } @@ -50,9 +50,9 @@ impl MetalAtlas { struct MetalAtlasState { device: AssertSend, - monochrome_textures: Vec, - polychrome_textures: Vec, - path_textures: Vec, + monochrome_textures: AtlasTextureList, + polychrome_textures: AtlasTextureList, + path_textures: AtlasTextureList, tiles_by_key: FxHashMap, } @@ -78,6 +78,38 @@ impl PlatformAtlas for MetalAtlas { Ok(Some(tile)) } } + + fn remove(&self, key: &AtlasKey) { + let mut lock = self.0.lock(); + let Some(id) = lock.tiles_by_key.get(key).map(|v| v.texture_id) else { + return; + }; + + let textures = match id.kind { + AtlasTextureKind::Monochrome => &mut lock.monochrome_textures, + AtlasTextureKind::Polychrome => &mut lock.polychrome_textures, + AtlasTextureKind::Path => &mut lock.polychrome_textures, + }; + + let Some(texture_slot) = textures + .textures + .iter_mut() + .find(|texture| texture.as_ref().is_some_and(|v| v.id == id)) + else { + return; + }; + + if let Some(mut texture) = texture_slot.take() { + texture.decrement_ref_count(); + + if texture.is_unreferenced() { + textures.free_list.push(id.index as usize); + lock.tiles_by_key.remove(key); + } else { + *texture_slot = Some(texture); + } + } + } } impl MetalAtlasState { @@ -86,20 +118,24 @@ impl MetalAtlasState { size: Size, texture_kind: AtlasTextureKind, ) -> Option { - let textures = match texture_kind { - AtlasTextureKind::Monochrome => &mut self.monochrome_textures, - AtlasTextureKind::Polychrome => &mut self.polychrome_textures, - AtlasTextureKind::Path => &mut self.path_textures, - }; + { + let textures = match texture_kind { + AtlasTextureKind::Monochrome => &mut self.monochrome_textures, + AtlasTextureKind::Polychrome => &mut self.polychrome_textures, + AtlasTextureKind::Path => &mut self.path_textures, + }; - textures - .iter_mut() - .rev() - .find_map(|texture| texture.allocate(size)) - .or_else(|| { - let texture = self.push_texture(size, texture_kind); - texture.allocate(size) - }) + if let Some(tile) = textures + .iter_mut() + .rev() + .find_map(|texture| texture.allocate(size)) + { + return Some(tile); + } + } + + let texture = self.push_texture(size, texture_kind); + texture.allocate(size) } fn push_texture( @@ -140,21 +176,31 @@ impl MetalAtlasState { texture_descriptor.set_usage(usage); let metal_texture = self.device.new_texture(&texture_descriptor); - let textures = match kind { + let texture_list = match kind { AtlasTextureKind::Monochrome => &mut self.monochrome_textures, AtlasTextureKind::Polychrome => &mut self.polychrome_textures, AtlasTextureKind::Path => &mut self.path_textures, }; + + let index = texture_list.free_list.pop(); + let atlas_texture = MetalAtlasTexture { id: AtlasTextureId { - index: textures.len() as u32, + index: index.unwrap_or(texture_list.textures.len()) as u32, kind, }, allocator: etagere::BucketedAtlasAllocator::new(size.into()), metal_texture: AssertSend(metal_texture), + live_atlas_keys: 0, }; - textures.push(atlas_texture); - textures.last_mut().unwrap() + + if let Some(ix) = index { + texture_list.textures[ix] = Some(atlas_texture); + texture_list.textures.get_mut(ix).unwrap().as_mut().unwrap() + } else { + texture_list.textures.push(Some(atlas_texture)); + texture_list.textures.last_mut().unwrap().as_mut().unwrap() + } } fn texture(&self, id: AtlasTextureId) -> &MetalAtlasTexture { @@ -163,7 +209,7 @@ impl MetalAtlasState { crate::AtlasTextureKind::Polychrome => &self.polychrome_textures, crate::AtlasTextureKind::Path => &self.path_textures, }; - &textures[id.index as usize] + textures[id.index as usize].as_ref().unwrap() } } @@ -171,6 +217,7 @@ struct MetalAtlasTexture { id: AtlasTextureId, allocator: BucketedAtlasAllocator, metal_texture: AssertSend, + live_atlas_keys: u32, } impl MetalAtlasTexture { @@ -189,6 +236,7 @@ impl MetalAtlasTexture { }, padding: 0, }; + self.live_atlas_keys += 1; Some(tile) } @@ -215,6 +263,14 @@ impl MetalAtlasTexture { _ => unimplemented!(), } } + + fn decrement_ref_count(&mut self) { + self.live_atlas_keys -= 1; + } + + fn is_unreferenced(&mut self) -> bool { + self.live_atlas_keys == 0 + } } impl From> for etagere::Size { diff --git a/crates/gpui/src/platform/test/window.rs b/crates/gpui/src/platform/test/window.rs index d8ec6a718b..9c94aeaf2f 100644 --- a/crates/gpui/src/platform/test/window.rs +++ b/crates/gpui/src/platform/test/window.rs @@ -339,4 +339,9 @@ impl PlatformAtlas for TestAtlas { Ok(Some(state.tiles[key].clone())) } + + fn remove(&self, key: &AtlasKey) { + let mut state = self.0.lock(); + state.tiles.remove(key); + } } diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index e4fa74f981..2b6f1d4a99 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -2685,6 +2685,20 @@ impl<'a> WindowContext<'a> { }); } + /// Removes an image from the sprite atlas. + pub fn drop_image(&mut self, data: Arc) -> Result<()> { + for frame_index in 0..data.frame_count() { + let params = RenderImageParams { + image_id: data.id, + frame_index, + }; + + self.window.sprite_atlas.remove(¶ms.clone().into()); + } + + Ok(()) + } + #[must_use] /// Add a node to the layout tree for the current frame. Takes the `Style` of the element for which /// layout is being requested, along with the layout ids of any children. This method is called during From cb8028c0928bb2f0a609201b416565255c2d2453 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 22 Nov 2024 13:21:30 -0500 Subject: [PATCH 107/886] Use `Extension` trait when registering extension context servers (#21070) This PR updates the extension context server registration to go through the `Extension` trait for interacting with extensions rather than going through the `WasmHost` directly. Release Notes: - N/A --- Cargo.lock | 1 - crates/extension/src/extension.rs | 10 ++++ crates/extension/src/types.rs | 1 + crates/extension_host/src/extension_host.rs | 4 +- crates/extension_host/src/wasm_host.rs | 22 ++++++- .../src/wasm_host/wit/since_v0_2_0.rs | 9 +-- crates/extensions_ui/Cargo.toml | 1 - .../src/extension_registration_hooks.rs | 57 ++++++++----------- 8 files changed, 59 insertions(+), 46 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 75d69fdcf9..514338c590 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4237,7 +4237,6 @@ dependencies = [ "ui", "util", "vim_mode_setting", - "wasmtime-wasi", "workspace", "zed_actions", ] diff --git a/crates/extension/src/extension.rs b/crates/extension/src/extension.rs index a3c275c537..fe9b49909b 100644 --- a/crates/extension/src/extension.rs +++ b/crates/extension/src/extension.rs @@ -25,6 +25,10 @@ pub trait WorktreeDelegate: Send + Sync + 'static { async fn shell_env(&self) -> Vec<(String, String)>; } +pub trait ProjectDelegate: Send + Sync + 'static { + fn worktree_ids(&self) -> Vec; +} + pub trait KeyValueStoreDelegate: Send + Sync + 'static { fn insert(&self, key: String, docs: String) -> Task>; } @@ -87,6 +91,12 @@ pub trait Extension: Send + Sync + 'static { worktree: Option>, ) -> Result; + async fn context_server_command( + &self, + context_server_id: Arc, + project: Arc, + ) -> Result; + async fn suggest_docs_packages(&self, provider: Arc) -> Result>; async fn index_docs( diff --git a/crates/extension/src/types.rs b/crates/extension/src/types.rs index f4c37b5daf..f04d31300f 100644 --- a/crates/extension/src/types.rs +++ b/crates/extension/src/types.rs @@ -10,6 +10,7 @@ pub use slash_command::*; pub type EnvVars = Vec<(String, String)>; /// A command. +#[derive(Debug)] pub struct Command { /// The command to execute. pub command: String, diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index 236c8091b4..85da812795 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -149,8 +149,8 @@ pub trait ExtensionRegistrationHooks: Send + Sync + 'static { fn register_context_server( &self, + _extension: Arc, _id: Arc, - _extension: WasmExtension, _cx: &mut AppContext, ) { } @@ -1284,8 +1284,8 @@ impl ExtensionStore { for (id, _context_server_entry) in &manifest.context_servers { this.registration_hooks.register_context_server( + extension.clone(), id.clone(), - wasm_extension.clone(), cx, ); } diff --git a/crates/extension_host/src/wasm_host.rs b/crates/extension_host/src/wasm_host.rs index 54699ac0a1..01c57599a8 100644 --- a/crates/extension_host/src/wasm_host.rs +++ b/crates/extension_host/src/wasm_host.rs @@ -4,7 +4,7 @@ use crate::{ExtensionManifest, ExtensionRegistrationHooks}; use anyhow::{anyhow, bail, Context as _, Result}; use async_trait::async_trait; use extension::{ - CodeLabel, Command, Completion, KeyValueStoreDelegate, SlashCommand, + CodeLabel, Command, Completion, KeyValueStoreDelegate, ProjectDelegate, SlashCommand, SlashCommandArgumentCompletion, SlashCommandOutput, Symbol, WorktreeDelegate, }; use fs::{normalize_path, Fs}; @@ -34,7 +34,6 @@ use wasmtime::{ }; use wasmtime_wasi::{self as wasi, WasiView}; use wit::Extension; -pub use wit::ExtensionProject; pub struct WasmHost { engine: Engine, @@ -238,6 +237,25 @@ impl extension::Extension for WasmExtension { .await } + async fn context_server_command( + &self, + context_server_id: Arc, + project: Arc, + ) -> Result { + self.call(|extension, store| { + async move { + let project_resource = store.data_mut().table().push(project)?; + let command = extension + .call_context_server_command(store, context_server_id.clone(), project_resource) + .await? + .map_err(|err| anyhow!("{err}"))?; + anyhow::Ok(command.into()) + } + .boxed() + }) + .await + } + async fn suggest_docs_packages(&self, provider: Arc) -> Result> { self.call(|extension, store| { async move { diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs index 02577abd0e..234eec26ec 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs @@ -8,7 +8,7 @@ use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; use async_trait::async_trait; use context_servers::manager::ContextServerSettings; -use extension::{KeyValueStoreDelegate, WorktreeDelegate}; +use extension::{KeyValueStoreDelegate, ProjectDelegate, WorktreeDelegate}; use futures::{io::BufReader, FutureExt as _}; use futures::{lock::Mutex, AsyncReadExt}; use language::{language_settings::AllLanguageSettings, LanguageName, LanguageServerBinaryStatus}; @@ -44,13 +44,10 @@ mod settings { } pub type ExtensionWorktree = Arc; +pub type ExtensionProject = Arc; pub type ExtensionKeyValueStore = Arc; pub type ExtensionHttpResponseStream = Arc>>; -pub struct ExtensionProject { - pub worktree_ids: Vec, -} - pub fn linker() -> &'static Linker { static LINKER: OnceLock> = OnceLock::new(); LINKER.get_or_init(|| super::new_linker(Extension::add_to_linker)) @@ -273,7 +270,7 @@ impl HostProject for WasmState { project: Resource, ) -> wasmtime::Result> { let project = self.table.get(&project)?; - Ok(project.worktree_ids.clone()) + Ok(project.worktree_ids()) } fn drop(&mut self, _project: Resource) -> Result<()> { diff --git a/crates/extensions_ui/Cargo.toml b/crates/extensions_ui/Cargo.toml index e8de7c3f12..a219fe4bd4 100644 --- a/crates/extensions_ui/Cargo.toml +++ b/crates/extensions_ui/Cargo.toml @@ -41,7 +41,6 @@ theme.workspace = true ui.workspace = true util.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/extension_registration_hooks.rs b/crates/extensions_ui/src/extension_registration_hooks.rs index 07a4c1455c..1b427cd187 100644 --- a/crates/extensions_ui/src/extension_registration_hooks.rs +++ b/crates/extensions_ui/src/extension_registration_hooks.rs @@ -1,13 +1,11 @@ use std::{path::PathBuf, sync::Arc}; -use anyhow::{anyhow, Result}; +use anyhow::Result; use assistant_slash_command::{ExtensionSlashCommand, SlashCommandRegistry}; use context_servers::manager::ServerCommand; use context_servers::ContextServerFactoryRegistry; -use db::smol::future::FutureExt as _; -use extension::Extension; -use extension_host::wasm_host::ExtensionProject; -use extension_host::{extension_lsp_adapter::ExtensionLspAdapter, wasm_host}; +use extension::{Extension, ProjectDelegate}; +use extension_host::extension_lsp_adapter::ExtensionLspAdapter; use fs::Fs; use gpui::{AppContext, BackgroundExecutor, Model, Task}; use indexed_docs::{ExtensionIndexedDocsProvider, IndexedDocsRegistry, ProviderId}; @@ -16,7 +14,16 @@ use lsp::LanguageServerName; use snippet_provider::SnippetRegistry; use theme::{ThemeRegistry, ThemeSettings}; use ui::SharedString; -use wasmtime_wasi::WasiView as _; + +struct ExtensionProject { + worktree_ids: Vec, +} + +impl ProjectDelegate for ExtensionProject { + fn worktree_ids(&self) -> Vec { + self.worktree_ids.clone() + } +} pub struct ConcreteExtensionRegistrationHooks { slash_command_registry: Arc, @@ -72,8 +79,8 @@ impl extension_host::ExtensionRegistrationHooks for ConcreteExtensionRegistratio fn register_context_server( &self, + extension: Arc, id: Arc, - extension: wasm_host::WasmExtension, cx: &mut AppContext, ) { self.context_server_factory_registry @@ -84,42 +91,24 @@ impl extension_host::ExtensionRegistrationHooks for ConcreteExtensionRegistratio move |project, cx| { log::info!( "loading command for context server {id} from extension {}", - extension.manifest.id + extension.manifest().id ); let id = id.clone(); let extension = extension.clone(); cx.spawn(|mut cx| async move { let extension_project = - project.update(&mut cx, |project, cx| ExtensionProject { - worktree_ids: project - .visible_worktrees(cx) - .map(|worktree| worktree.read(cx).id().to_proto()) - .collect(), + project.update(&mut cx, |project, cx| { + Arc::new(ExtensionProject { + worktree_ids: project + .visible_worktrees(cx) + .map(|worktree| worktree.read(cx).id().to_proto()) + .collect(), + }) })?; let command = extension - .call({ - let id = id.clone(); - |extension, store| { - async move { - let project = store - .data_mut() - .table() - .push(extension_project)?; - let command = extension - .call_context_server_command( - store, - id.clone(), - project, - ) - .await? - .map_err(|e| anyhow!("{}", e))?; - anyhow::Ok(command) - } - .boxed() - } - }) + .context_server_command(id.clone(), extension_project) .await?; log::info!("loaded command for context server {id}: {command:?}"); From 659b1c9dcf42c1a1074b090ae9eabb757a6aab94 Mon Sep 17 00:00:00 2001 From: Hugo Cardante <65104945+omennia@users.noreply.github.com> Date: Fri, 22 Nov 2024 18:45:42 +0000 Subject: [PATCH 108/886] Add the option to hide both the task and command lines in the task output (#20920) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The goal is to be able to hide these lines from a task output: ```sh ⏵ Task `...` finished successfully ⏵ Command: ... ``` --------- Co-authored-by: Peter Tripp --- crates/project/src/terminals.rs | 2 ++ crates/task/src/lib.rs | 4 ++++ crates/task/src/task_template.rs | 9 +++++++++ crates/terminal/src/terminal.rs | 23 ++++++++++++++++++----- docs/src/tasks.md | 6 +++++- 5 files changed, 38 insertions(+), 6 deletions(-) diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index 1320a883f3..111516c82d 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -174,6 +174,8 @@ impl Project { command_label: spawn_task.command_label, hide: spawn_task.hide, status: TaskStatus::Running, + show_summary: spawn_task.show_summary, + show_command: spawn_task.show_command, completion_rx, }); diff --git a/crates/task/src/lib.rs b/crates/task/src/lib.rs index 534b77b743..cf3a414b11 100644 --- a/crates/task/src/lib.rs +++ b/crates/task/src/lib.rs @@ -51,6 +51,10 @@ pub struct SpawnInTerminal { pub hide: HideStrategy, /// Which shell to use when spawning the task. pub shell: Shell, + /// Whether to show the task summary line in the task output (sucess/failure). + pub show_summary: bool, + /// Whether to show the command line in the task output. + pub show_command: bool, } /// A final form of the [`TaskTemplate`], that got resolved with a particualar [`TaskContext`] and now is ready to spawn the actual task. diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs index b72a0d25f8..23a2bc8ca7 100644 --- a/crates/task/src/task_template.rs +++ b/crates/task/src/task_template.rs @@ -1,4 +1,5 @@ use std::path::PathBuf; +use util::serde::default_true; use anyhow::{bail, Context}; use collections::{HashMap, HashSet}; @@ -57,6 +58,12 @@ pub struct TaskTemplate { /// Which shell to use when spawning the task. #[serde(default)] pub shell: Shell, + /// Whether to show the task line in the task output. + #[serde(default = "default_true")] + pub show_summary: bool, + /// Whether to show the command line in the task output. + #[serde(default = "default_true")] + pub show_command: bool, } /// What to do with the terminal pane and tab, after the command was started. @@ -230,6 +237,8 @@ impl TaskTemplate { reveal: self.reveal, hide: self.hide, shell: self.shell.clone(), + show_summary: self.show_summary, + show_command: self.show_command, }), }) } diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 5a15723cee..6610ac567d 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -639,6 +639,8 @@ pub struct TaskState { pub status: TaskStatus, pub completion_rx: Receiver<()>, pub hide: HideStrategy, + pub show_summary: bool, + pub show_command: bool, } /// A status of the current terminal tab's task. @@ -1760,11 +1762,22 @@ impl Terminal { }; let (finished_successfully, task_line, command_line) = task_summary(task, error_code); - // SAFETY: the invocation happens on non `TaskStatus::Running` tasks, once, - // after either `AlacTermEvent::Exit` or `AlacTermEvent::ChildExit` events that are spawned - // when Zed task finishes and no more output is made. - // After the task summary is output once, no more text is appended to the terminal. - unsafe { append_text_to_term(&mut self.term.lock(), &[&task_line, &command_line]) }; + let mut lines_to_show = Vec::new(); + if task.show_summary { + lines_to_show.push(task_line.as_str()); + } + if task.show_command { + lines_to_show.push(command_line.as_str()); + } + + if !lines_to_show.is_empty() { + // SAFETY: the invocation happens on non `TaskStatus::Running` tasks, once, + // after either `AlacTermEvent::Exit` or `AlacTermEvent::ChildExit` events that are spawned + // when Zed task finishes and no more output is made. + // After the task summary is output once, no more text is appended to the terminal. + unsafe { append_text_to_term(&mut self.term.lock(), &lines_to_show) }; + } + match task.hide { HideStrategy::Never => {} HideStrategy::Always => { diff --git a/docs/src/tasks.md b/docs/src/tasks.md index 98cbd6dfc1..f32e5778fc 100644 --- a/docs/src/tasks.md +++ b/docs/src/tasks.md @@ -41,7 +41,11 @@ Zed supports ways to spawn (and rerun) commands using its integrated terminal to // "args": ["--login"] // } // } - "shell": "system" + "shell": "system", + // Whether to show the task line in the output of the spawned task, defaults to `true`. + "show_summary": true, + // Whether to show the command line in the output of the spawned task, defaults to `true`. + "show_output": true } ] ``` From 23321be2ceda03548368ff262f81f905b86314df Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Fri, 22 Nov 2024 18:58:24 +0000 Subject: [PATCH 109/886] docs: Improve Dart language docs (#21071) --- docs/src/languages/dart.md | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/docs/src/languages/dart.md b/docs/src/languages/dart.md index 32f312e5dd..6b7d01c39e 100644 --- a/docs/src/languages/dart.md +++ b/docs/src/languages/dart.md @@ -5,9 +5,22 @@ Dart support is available through the [Dart extension](https://github.com/zed-ex - Tree Sitter: [UserNobody14/tree-sitter-dart](https://github.com/UserNobody14/tree-sitter-dart) - Language Server: [dart language-server](https://github.com/dart-lang/sdk) +## Pre-requisites + +You will need to install the Dart SDK. + +You can install dart from [dart.dev/get-dart](https://dart.dev/get-dart) or via the [Flutter Version Management CLI (fvm)](https://fvm.app/documentation/getting-started/installation) + ## Configuration -The `dart` binary can be configured in a Zed settings file with: +The dart extension requires no configuration if you have `dart` in your path: + +```sh +which dart +dart --version +``` + +If you would like to use a specific dart binary or use dart via FVM you can specify the `dart` binary in your Zed settings.jsons file: ```json { @@ -22,7 +35,4 @@ The `dart` binary can be configured in a Zed settings file with: } ``` - +Please see the Dart documentation for more information on [dart language-server capabilities](https://github.com/dart-lang/sdk/blob/main/pkg/analysis_server/tool/lsp_spec/README.md). From 2fd210bc9aece3c33f958317e3f141ead660569c Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Fri, 22 Nov 2024 21:10:51 +0000 Subject: [PATCH 110/886] Fix stale Discord invite links (#21074) --- crates/gpui/README.md | 2 +- crates/gpui/src/gpui.rs | 2 +- docs/src/remote-development.md | 4 ++-- docs/src/windows.md | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/gpui/README.md b/crates/gpui/README.md index 3ca0dcf7ca..6c0a5b607c 100644 --- a/crates/gpui/README.md +++ b/crates/gpui/README.md @@ -61,4 +61,4 @@ In addition to the systems above, GPUI provides a range of smaller services that - The `[gpui::test]` macro provides a convenient way to write tests for your GPUI applications. Tests also have their own kind of context, a `TestAppContext` which provides ways of simulating common platform input. See `app::test_context` and `test` modules for more details. -Currently, the best way to learn about these APIs is to read the Zed source code, ask us about it at a fireside hack, or drop a question in the [Zed Discord](https://discord.gg/zed-community). We're working on improving the documentation, creating more examples, and will be publishing more guides to GPUI on our [blog](https://zed.dev/blog). +Currently, the best way to learn about these APIs is to read the Zed source code, ask us about it at a fireside hack, or drop a question in the [Zed Discord](https://zed.dev/community-links). We're working on improving the documentation, creating more examples, and will be publishing more guides to GPUI on our [blog](https://zed.dev/blog). diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index 2952f4af8a..51e2c3f173 100644 --- a/crates/gpui/src/gpui.rs +++ b/crates/gpui/src/gpui.rs @@ -56,7 +56,7 @@ //! and [`test`] modules for more details. //! //! Currently, the best way to learn about these APIs is to read the Zed source code, ask us about it at a fireside hack, or drop -//! a question in the [Zed Discord](https://discord.gg/zed-community). We're working on improving the documentation, creating more examples, +//! a question in the [Zed Discord](https://zed.dev/community-links). We're working on improving the documentation, creating more examples, //! and will be publishing more guides to GPUI on our [blog](https://zed.dev/blog). #![deny(missing_docs)] diff --git a/docs/src/remote-development.md b/docs/src/remote-development.md index 17ae23bb63..7ab0cb6b76 100644 --- a/docs/src/remote-development.md +++ b/docs/src/remote-development.md @@ -125,7 +125,7 @@ Each connection tries to run the development server in proxy mode. This mode wil In the case that reconnecting fails, the daemon will not be re-used. That said, unsaved changes are by default persisted locally, so that you do not lose work. You can always reconnect to the project at a later date and Zed will restore unsaved changes. -If you are struggling with connection issues, you should be able to see more information in the Zed log `cmd-shift-p Open Log`. If you are seeing things that are unexpected, please file a [GitHub issue](https://github.com/zed-industries/zed/issues/new) or reach out in the #remoting-feedback channel in the [Zed Discord](https://discord.gg/zed-community). +If you are struggling with connection issues, you should be able to see more information in the Zed log `cmd-shift-p Open Log`. If you are seeing things that are unexpected, please file a [GitHub issue](https://github.com/zed-industries/zed/issues/new) or reach out in the #remoting-feedback channel in the [Zed Discord](https://zed.dev/community-links). ## Supported SSH Options @@ -152,4 +152,4 @@ Note that we deliberately disallow some options (for example `-t` or `-T`) that ## Feedback -Please join the #remoting-feedback channel in the [Zed Discord](https://discord.gg/zed-community). +Please join the #remoting-feedback channel in the [Zed Discord](https://zed.dev/community-links). diff --git a/docs/src/windows.md b/docs/src/windows.md index 47fae7cb9f..f8949f22f3 100644 --- a/docs/src/windows.md +++ b/docs/src/windows.md @@ -10,4 +10,4 @@ Zed Employees are not currently working on the Windows build. However, we welcome contributions from the community to improve Windows support. - [GitHub Issues with 'Windows' label](https://github.com/zed-industries/zed/issues?q=is%3Aissue+is%3Aopen+label%3Awindows) -- [Zed Community Discord](https://discord.gg/zed-community) -> `#windows-port` +- [Zed Community Discord](https://zed.dev/community-links) -> `#windows-port` From 1a0a8a9559cf0a63f4be7c564603b7da78101b57 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Fri, 22 Nov 2024 22:45:03 +0000 Subject: [PATCH 111/886] Fix picker new_path_prompt throwing "file exists" when saving (#21080) Fix for getting File exists "os error 17" with `"use_system_path_prompts": false,` This was reproducible when the first worktree is a non-folder worktree (e.g. setting.json) so we were trying to create the new file with a path under ~/.config/zed/settings.json/newfile.ext Co-authored-by: Conrad Irwin --- crates/file_finder/src/new_path_prompt.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/file_finder/src/new_path_prompt.rs b/crates/file_finder/src/new_path_prompt.rs index d4492857b4..6a1b08e205 100644 --- a/crates/file_finder/src/new_path_prompt.rs +++ b/crates/file_finder/src/new_path_prompt.rs @@ -71,8 +71,16 @@ impl Match { fn project_path(&self, project: &Project, cx: &WindowContext) -> Option { let worktree_id = if let Some(path_match) = &self.path_match { WorktreeId::from_usize(path_match.worktree_id) + } else if let Some(worktree) = project.visible_worktrees(cx).find(|worktree| { + worktree + .read(cx) + .root_entry() + .is_some_and(|entry| entry.is_dir()) + }) { + worktree.read(cx).id() } else { - project.worktrees(cx).next()?.read(cx).id() + // todo(): we should find_or_create a workspace. + return None; }; let path = PathBuf::from(self.relative_path()); From becc36380f19de4148980419a9fdc4ba1b36e5f8 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Fri, 22 Nov 2024 22:46:14 +0000 Subject: [PATCH 112/886] Cleanup file_scan_inclusions in default.json (#21073) --- assets/settings/default.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 819cdcfff6..02527e8e67 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -683,10 +683,7 @@ // 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" - ], + "file_scan_inclusions": [".env*"], // Git gutter behavior configuration. "git": { // Control whether the git gutter is shown. May take 2 values: From 96854c68eadd1dd72aa379368fde3cea791498a4 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 22 Nov 2024 14:49:26 -0800 Subject: [PATCH 113/886] Markdown preview image rendering (#21082) Closes https://github.com/zed-industries/zed/issues/13246 Supersedes: https://github.com/zed-industries/zed/pull/16192 I couldn't push to the git fork this user was using, so here's the exact same PR but with some style nits implemented. Release Notes: - Added image rendering to the Markdown preview --------- Co-authored-by: dovakin0007 Co-authored-by: dovakin0007 <73059450+dovakin0007@users.noreply.github.com> --- crates/language/src/markdown.rs | 14 +- .../markdown_preview/src/markdown_elements.rs | 137 +++++++- .../markdown_preview/src/markdown_parser.rs | 215 +++++++----- .../markdown_preview/src/markdown_renderer.rs | 330 ++++++++++++++---- .../notifications/src/notification_store.rs | 7 +- crates/repl/src/notebook/cell.rs | 2 +- crates/rich_text/src/rich_text.rs | 14 +- 7 files changed, 538 insertions(+), 181 deletions(-) diff --git a/crates/language/src/markdown.rs b/crates/language/src/markdown.rs index b9393a16ab..0221f0f431 100644 --- a/crates/language/src/markdown.rs +++ b/crates/language/src/markdown.rs @@ -239,12 +239,7 @@ pub async fn parse_markdown_block( Event::Start(tag) => match tag { Tag::Paragraph => new_paragraph(text, &mut list_stack), - Tag::Heading { - level: _, - id: _, - classes: _, - attrs: _, - } => { + Tag::Heading { .. } => { new_paragraph(text, &mut list_stack); bold_depth += 1; } @@ -267,12 +262,7 @@ pub async fn parse_markdown_block( Tag::Strikethrough => strikethrough_depth += 1, - Tag::Link { - link_type: _, - dest_url, - title: _, - id: _, - } => link_url = Some(dest_url.to_string()), + Tag::Link { dest_url, .. } => link_url = Some(dest_url.to_string()), Tag::List(number) => { list_stack.push((number, false)); diff --git a/crates/markdown_preview/src/markdown_elements.rs b/crates/markdown_preview/src/markdown_elements.rs index 8423e4ec82..ff43fab08a 100644 --- a/crates/markdown_preview/src/markdown_elements.rs +++ b/crates/markdown_preview/src/markdown_elements.rs @@ -13,7 +13,7 @@ pub enum ParsedMarkdownElement { BlockQuote(ParsedMarkdownBlockQuote), CodeBlock(ParsedMarkdownCodeBlock), /// A paragraph of text and other inline elements. - Paragraph(ParsedMarkdownText), + Paragraph(MarkdownParagraph), HorizontalRule(Range), } @@ -25,7 +25,13 @@ impl ParsedMarkdownElement { Self::Table(table) => table.source_range.clone(), Self::BlockQuote(block_quote) => block_quote.source_range.clone(), Self::CodeBlock(code_block) => code_block.source_range.clone(), - Self::Paragraph(text) => text.source_range.clone(), + Self::Paragraph(text) => match &text[0] { + MarkdownParagraphChunk::Text(t) => t.source_range.clone(), + MarkdownParagraphChunk::Image(image) => match image { + Image::Web { source_range, .. } => source_range.clone(), + Image::Path { source_range, .. } => source_range.clone(), + }, + }, Self::HorizontalRule(range) => range.clone(), } } @@ -35,6 +41,15 @@ impl ParsedMarkdownElement { } } +pub type MarkdownParagraph = Vec; + +#[derive(Debug)] +#[cfg_attr(test, derive(PartialEq))] +pub enum MarkdownParagraphChunk { + Text(ParsedMarkdownText), + Image(Image), +} + #[derive(Debug)] #[cfg_attr(test, derive(PartialEq))] pub struct ParsedMarkdown { @@ -73,7 +88,7 @@ pub struct ParsedMarkdownCodeBlock { pub struct ParsedMarkdownHeading { pub source_range: Range, pub level: HeadingLevel, - pub contents: ParsedMarkdownText, + pub contents: MarkdownParagraph, } #[derive(Debug, PartialEq)] @@ -107,7 +122,7 @@ pub enum ParsedMarkdownTableAlignment { #[derive(Debug)] #[cfg_attr(test, derive(PartialEq))] pub struct ParsedMarkdownTableRow { - pub children: Vec, + pub children: Vec, } impl Default for ParsedMarkdownTableRow { @@ -123,7 +138,7 @@ impl ParsedMarkdownTableRow { } } - pub fn with_children(children: Vec) -> Self { + pub fn with_children(children: Vec) -> Self { Self { children } } } @@ -135,7 +150,7 @@ pub struct ParsedMarkdownBlockQuote { pub children: Vec, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct ParsedMarkdownText { /// Where the text is located in the source Markdown document. pub source_range: Range, @@ -266,10 +281,112 @@ impl Display for Link { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Link::Web { url } => write!(f, "{}", url), - Link::Path { - display_path, - path: _, - } => write!(f, "{}", display_path.display()), + Link::Path { display_path, .. } => write!(f, "{}", display_path.display()), + } + } +} + +/// A Markdown Image +#[derive(Debug, Clone)] +#[cfg_attr(test, derive(PartialEq))] +pub enum Image { + Web { + source_range: Range, + /// The URL of the Image. + url: String, + /// Link URL if exists. + link: Option, + /// alt text if it exists + alt_text: Option, + }, + /// Image path on the filesystem. + Path { + source_range: Range, + /// The path as provided in the Markdown document. + display_path: PathBuf, + /// The absolute path to the item. + path: PathBuf, + /// Link URL if exists. + link: Option, + /// alt text if it exists + alt_text: Option, + }, +} + +impl Image { + pub fn identify( + source_range: Range, + file_location_directory: Option, + text: String, + link: Option, + ) -> Option { + if text.starts_with("http") { + return Some(Image::Web { + source_range, + url: text, + link, + alt_text: None, + }); + } + let path = PathBuf::from(&text); + if path.is_absolute() { + return Some(Image::Path { + source_range, + display_path: path.clone(), + path, + link, + alt_text: None, + }); + } + if let Some(file_location_directory) = file_location_directory { + let display_path = path; + let path = file_location_directory.join(text); + return Some(Image::Path { + source_range, + display_path, + path, + link, + alt_text: None, + }); + } + None + } + + pub fn with_alt_text(&self, alt_text: ParsedMarkdownText) -> Self { + match self { + Image::Web { + ref source_range, + ref url, + ref link, + .. + } => Image::Web { + source_range: source_range.clone(), + url: url.clone(), + link: link.clone(), + alt_text: Some(alt_text), + }, + Image::Path { + ref source_range, + ref display_path, + ref path, + ref link, + .. + } => Image::Path { + source_range: source_range.clone(), + display_path: display_path.clone(), + path: path.clone(), + link: link.clone(), + alt_text: Some(alt_text), + }, + } + } +} + +impl Display for Image { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Image::Web { url, .. } => write!(f, "{}", url), + Image::Path { display_path, .. } => write!(f, "{}", display_path.display()), } } } diff --git a/crates/markdown_preview/src/markdown_parser.rs b/crates/markdown_preview/src/markdown_parser.rs index d514b89e52..211cca2494 100644 --- a/crates/markdown_preview/src/markdown_parser.rs +++ b/crates/markdown_preview/src/markdown_parser.rs @@ -4,7 +4,7 @@ use collections::FxHashMap; use gpui::FontWeight; use language::LanguageRegistry; use pulldown_cmark::{Alignment, Event, Options, Parser, Tag, TagEnd}; -use std::{ops::Range, path::PathBuf, sync::Arc}; +use std::{ops::Range, path::PathBuf, sync::Arc, vec}; pub async fn parse_markdown( markdown_input: &str, @@ -101,11 +101,11 @@ impl<'a> MarkdownParser<'a> { | Event::Code(_) | Event::Html(_) | Event::FootnoteReference(_) - | Event::Start(Tag::Link { link_type: _, dest_url: _, title: _, id: _ }) + | Event::Start(Tag::Link { .. }) | Event::Start(Tag::Emphasis) | Event::Start(Tag::Strong) | Event::Start(Tag::Strikethrough) - | Event::Start(Tag::Image { link_type: _, dest_url: _, title: _, id: _ }) => { + | Event::Start(Tag::Image { .. }) => { true } _ => false, @@ -134,12 +134,7 @@ impl<'a> MarkdownParser<'a> { let text = self.parse_text(false, Some(source_range)); Some(vec![ParsedMarkdownElement::Paragraph(text)]) } - Tag::Heading { - level, - id: _, - classes: _, - attrs: _, - } => { + Tag::Heading { level, .. } => { let level = *level; self.cursor += 1; let heading = self.parse_heading(level); @@ -194,22 +189,23 @@ impl<'a> MarkdownParser<'a> { &mut self, should_complete_on_soft_break: bool, source_range: Option>, - ) -> ParsedMarkdownText { + ) -> MarkdownParagraph { let source_range = source_range.unwrap_or_else(|| { self.current() .map(|(_, range)| range.clone()) .unwrap_or_default() }); + let mut markdown_text_like = Vec::new(); let mut text = String::new(); let mut bold_depth = 0; let mut italic_depth = 0; let mut strikethrough_depth = 0; let mut link: Option = None; + let mut image: Option = None; let mut region_ranges: Vec> = vec![]; let mut regions: Vec = vec![]; let mut highlights: Vec<(Range, MarkdownHighlight)> = vec![]; - let mut link_urls: Vec = vec![]; let mut link_ranges: Vec> = vec![]; @@ -225,8 +221,6 @@ impl<'a> MarkdownParser<'a> { if should_complete_on_soft_break { break; } - - // `Some text\nSome more text` should be treated as a single line. text.push(' '); } @@ -240,7 +234,6 @@ impl<'a> MarkdownParser<'a> { Event::Text(t) => { text.push_str(t.as_ref()); - let mut style = MarkdownHighlightStyle::default(); if bold_depth > 0 { @@ -299,7 +292,6 @@ impl<'a> MarkdownParser<'a> { url: link.as_str().to_string(), }), }); - last_link_len = end; } last_link_len @@ -316,13 +308,63 @@ impl<'a> MarkdownParser<'a> { } } if new_highlight { - highlights - .push((last_run_len..text.len(), MarkdownHighlight::Style(style))); + highlights.push(( + last_run_len..text.len(), + MarkdownHighlight::Style(style.clone()), + )); } } - } + if let Some(mut image) = image.clone() { + let is_valid_image = match image.clone() { + Image::Path { display_path, .. } => { + gpui::ImageSource::try_from(display_path).is_ok() + } + Image::Web { url, .. } => gpui::ImageSource::try_from(url).is_ok(), + }; + if is_valid_image { + text.truncate(text.len() - t.len()); + if !t.is_empty() { + let alt_text = ParsedMarkdownText { + source_range: source_range.clone(), + contents: t.to_string(), + highlights: highlights.clone(), + region_ranges: region_ranges.clone(), + regions: regions.clone(), + }; + image = image.with_alt_text(alt_text); + } else { + let alt_text = ParsedMarkdownText { + source_range: source_range.clone(), + contents: "img".to_string(), + highlights: highlights.clone(), + region_ranges: region_ranges.clone(), + regions: regions.clone(), + }; + image = image.with_alt_text(alt_text); + } + if !text.is_empty() { + let parsed_regions = + MarkdownParagraphChunk::Text(ParsedMarkdownText { + source_range: source_range.clone(), + contents: text.clone(), + highlights: highlights.clone(), + region_ranges: region_ranges.clone(), + regions: regions.clone(), + }); + text = String::new(); + highlights = vec![]; + region_ranges = vec![]; + regions = vec![]; + markdown_text_like.push(parsed_regions); + } - // Note: This event means "inline code" and not "code block" + let parsed_image = MarkdownParagraphChunk::Image(image.clone()); + markdown_text_like.push(parsed_image); + style = MarkdownHighlightStyle::default(); + } + style.underline = true; + }; + } Event::Code(t) => { text.push_str(t.as_ref()); region_ranges.push(prev_len..text.len()); @@ -336,46 +378,44 @@ impl<'a> MarkdownParser<'a> { }), )); } - regions.push(ParsedRegion { code: true, link: link.clone(), }); } - Event::Start(tag) => match tag { Tag::Emphasis => italic_depth += 1, Tag::Strong => bold_depth += 1, Tag::Strikethrough => strikethrough_depth += 1, - Tag::Link { - link_type: _, - dest_url, - title: _, - id: _, - } => { + Tag::Link { dest_url, .. } => { link = Link::identify( self.file_location_directory.clone(), dest_url.to_string(), ); } + Tag::Image { dest_url, .. } => { + image = Image::identify( + source_range.clone(), + self.file_location_directory.clone(), + dest_url.to_string(), + link.clone(), + ); + } _ => { break; } }, Event::End(tag) => match tag { - TagEnd::Emphasis => { - italic_depth -= 1; - } - TagEnd::Strong => { - bold_depth -= 1; - } - TagEnd::Strikethrough => { - strikethrough_depth -= 1; - } + TagEnd::Emphasis => italic_depth -= 1, + TagEnd::Strong => bold_depth -= 1, + TagEnd::Strikethrough => strikethrough_depth -= 1, TagEnd::Link => { link = None; } + TagEnd::Image => { + image = None; + } TagEnd::Paragraph => { self.cursor += 1; break; @@ -384,7 +424,6 @@ impl<'a> MarkdownParser<'a> { break; } }, - _ => { break; } @@ -392,14 +431,16 @@ impl<'a> MarkdownParser<'a> { self.cursor += 1; } - - ParsedMarkdownText { - source_range, - contents: text, - highlights, - regions, - region_ranges, + if !text.is_empty() { + markdown_text_like.push(MarkdownParagraphChunk::Text(ParsedMarkdownText { + source_range: source_range.clone(), + contents: text, + highlights, + regions, + region_ranges, + })); } + markdown_text_like } fn parse_heading(&mut self, level: pulldown_cmark::HeadingLevel) -> ParsedMarkdownHeading { @@ -708,7 +749,6 @@ impl<'a> MarkdownParser<'a> { } } } - let highlights = if let Some(language) = &language { if let Some(registry) = &self.language_registry { let rope: language::Rope = code.as_str().into(); @@ -735,10 +775,14 @@ impl<'a> MarkdownParser<'a> { #[cfg(test)] mod tests { + use core::panic; + use super::*; use gpui::BackgroundExecutor; - use language::{tree_sitter_rust, HighlightId, Language, LanguageConfig, LanguageMatcher}; + use language::{ + tree_sitter_rust, HighlightId, Language, LanguageConfig, LanguageMatcher, LanguageRegistry, + }; use pretty_assertions::assert_eq; use ParsedMarkdownListItemType::*; @@ -810,20 +854,29 @@ mod tests { assert_eq!(parsed.children.len(), 1); assert_eq!( parsed.children[0], - ParsedMarkdownElement::Paragraph(ParsedMarkdownText { - source_range: 0..35, - contents: "Some bostrikethroughld text".to_string(), - highlights: Vec::new(), - region_ranges: Vec::new(), - regions: Vec::new(), - }) + ParsedMarkdownElement::Paragraph(vec![MarkdownParagraphChunk::Text( + ParsedMarkdownText { + source_range: 0..35, + contents: "Some bostrikethroughld text".to_string(), + highlights: Vec::new(), + region_ranges: Vec::new(), + regions: Vec::new(), + } + )]) ); - let paragraph = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] { + let new_text = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] { text } else { panic!("Expected a paragraph"); }; + + let paragraph = if let MarkdownParagraphChunk::Text(text) = &new_text[0] { + text + } else { + panic!("Expected a text"); + }; + assert_eq!( paragraph.highlights, vec![ @@ -871,6 +924,11 @@ mod tests { parsed.children, vec![p("Checkout this https://zed.dev link", 0..34)] ); + } + + #[gpui::test] + async fn test_image_links_detection() { + let parsed = parse("![test](https://blog.logrocket.com/wp-content/uploads/2024/04/exploring-zed-open-source-code-editor-rust-2.png)").await; let paragraph = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] { text @@ -878,25 +936,22 @@ mod tests { panic!("Expected a paragraph"); }; assert_eq!( - paragraph.highlights, - vec![( - 14..29, - MarkdownHighlight::Style(MarkdownHighlightStyle { - underline: true, - ..Default::default() - }), - )] + paragraph[0], + MarkdownParagraphChunk::Image(Image::Web { + source_range: 0..111, + url: "https://blog.logrocket.com/wp-content/uploads/2024/04/exploring-zed-open-source-code-editor-rust-2.png".to_string(), + link: None, + alt_text: Some( + ParsedMarkdownText { + source_range: 0..111, + contents: "test".to_string(), + highlights: vec![], + region_ranges: vec![], + regions: vec![], + }, + ), + },) ); - assert_eq!( - paragraph.regions, - vec![ParsedRegion { - code: false, - link: Some(Link::Web { - url: "https://zed.dev".to_string() - }), - }] - ); - assert_eq!(paragraph.region_ranges, vec![14..29]); } #[gpui::test] @@ -1169,7 +1224,7 @@ Some other content vec![ list_item(0..8, 1, Unordered, vec![p("code", 2..8)]), list_item(9..19, 1, Unordered, vec![p("bold", 11..19)]), - list_item(20..49, 1, Unordered, vec![p("link", 22..49)],) + list_item(20..49, 1, Unordered, vec![p("link", 22..49)],), ], ); } @@ -1312,7 +1367,7 @@ fn main() { )) } - fn h1(contents: ParsedMarkdownText, source_range: Range) -> ParsedMarkdownElement { + fn h1(contents: MarkdownParagraph, source_range: Range) -> ParsedMarkdownElement { ParsedMarkdownElement::Heading(ParsedMarkdownHeading { source_range, level: HeadingLevel::H1, @@ -1320,7 +1375,7 @@ fn main() { }) } - fn h2(contents: ParsedMarkdownText, source_range: Range) -> ParsedMarkdownElement { + fn h2(contents: MarkdownParagraph, source_range: Range) -> ParsedMarkdownElement { ParsedMarkdownElement::Heading(ParsedMarkdownHeading { source_range, level: HeadingLevel::H2, @@ -1328,7 +1383,7 @@ fn main() { }) } - fn h3(contents: ParsedMarkdownText, source_range: Range) -> ParsedMarkdownElement { + fn h3(contents: MarkdownParagraph, source_range: Range) -> ParsedMarkdownElement { ParsedMarkdownElement::Heading(ParsedMarkdownHeading { source_range, level: HeadingLevel::H3, @@ -1340,14 +1395,14 @@ fn main() { ParsedMarkdownElement::Paragraph(text(contents, source_range)) } - fn text(contents: &str, source_range: Range) -> ParsedMarkdownText { - ParsedMarkdownText { + fn text(contents: &str, source_range: Range) -> MarkdownParagraph { + vec![MarkdownParagraphChunk::Text(ParsedMarkdownText { highlights: Vec::new(), region_ranges: Vec::new(), regions: Vec::new(), source_range, contents: contents.to_string(), - } + })] } fn block_quote( @@ -1401,7 +1456,7 @@ fn main() { } } - fn row(children: Vec) -> ParsedMarkdownTableRow { + fn row(children: Vec) -> ParsedMarkdownTableRow { ParsedMarkdownTableRow { children } } diff --git a/crates/markdown_preview/src/markdown_renderer.rs b/crates/markdown_preview/src/markdown_renderer.rs index 37ca5636a6..6140372e0b 100644 --- a/crates/markdown_preview/src/markdown_renderer.rs +++ b/crates/markdown_preview/src/markdown_renderer.rs @@ -1,29 +1,33 @@ use crate::markdown_elements::{ - HeadingLevel, Link, ParsedMarkdown, ParsedMarkdownBlockQuote, ParsedMarkdownCodeBlock, - ParsedMarkdownElement, ParsedMarkdownHeading, ParsedMarkdownListItem, - ParsedMarkdownListItemType, ParsedMarkdownTable, ParsedMarkdownTableAlignment, - ParsedMarkdownTableRow, ParsedMarkdownText, + HeadingLevel, Image, Link, MarkdownParagraph, MarkdownParagraphChunk, ParsedMarkdown, + ParsedMarkdownBlockQuote, ParsedMarkdownCodeBlock, ParsedMarkdownElement, + ParsedMarkdownHeading, ParsedMarkdownListItem, ParsedMarkdownListItemType, ParsedMarkdownTable, + ParsedMarkdownTableAlignment, ParsedMarkdownTableRow, ParsedMarkdownText, }; use gpui::{ - div, px, rems, AbsoluteLength, AnyElement, ClipboardItem, DefiniteLength, Div, Element, - ElementId, HighlightStyle, Hsla, InteractiveText, IntoElement, Keystroke, Length, Modifiers, - ParentElement, SharedString, Styled, StyledText, TextStyle, WeakView, WindowContext, + div, img, px, rems, AbsoluteLength, AnyElement, ClipboardItem, DefiniteLength, Div, Element, + ElementId, HighlightStyle, Hsla, ImageSource, InteractiveText, IntoElement, Keystroke, Length, + Modifiers, ParentElement, Resource, SharedString, Styled, StyledText, TextStyle, WeakView, + WindowContext, }; use settings::Settings; use std::{ ops::{Mul, Range}, + path::Path, sync::Arc, + vec, }; use theme::{ActiveTheme, SyntaxTheme, ThemeSettings}; use ui::{ h_flex, relative, v_flex, Checkbox, Clickable, FluentBuilder, IconButton, IconName, IconSize, - InteractiveElement, LinkPreview, Selection, StatefulInteractiveElement, StyledExt, Tooltip, - VisibleOnHover, + InteractiveElement, LinkPreview, Selection, StatefulInteractiveElement, StyledExt, StyledImage, + Tooltip, VisibleOnHover, }; use workspace::Workspace; type CheckboxClickedCallback = Arc, &mut WindowContext)>>; +#[derive(Clone)] pub struct RenderContext { workspace: Option>, next_id: usize, @@ -153,7 +157,7 @@ fn render_markdown_heading(parsed: &ParsedMarkdownHeading, cx: &mut RenderContex .text_color(color) .pt(rems(0.15)) .pb_1() - .child(render_markdown_text(&parsed.contents, cx)) + .children(render_markdown_text(&parsed.contents, cx)) .whitespace_normal() .into_any() } @@ -231,17 +235,29 @@ fn render_markdown_list_item( cx.with_common_p(item).into_any() } +fn paragraph_len(paragraphs: &MarkdownParagraph) -> usize { + paragraphs + .iter() + .map(|paragraph| match paragraph { + MarkdownParagraphChunk::Text(text) => text.contents.len(), + // TODO: Scale column width based on image size + MarkdownParagraphChunk::Image(_) => 1, + }) + .sum() +} + fn render_markdown_table(parsed: &ParsedMarkdownTable, cx: &mut RenderContext) -> AnyElement { let mut max_lengths: Vec = vec![0; parsed.header.children.len()]; for (index, cell) in parsed.header.children.iter().enumerate() { - let length = cell.contents.len(); + let length = paragraph_len(&cell); max_lengths[index] = length; } for row in &parsed.body { for (index, cell) in row.children.iter().enumerate() { - let length = cell.contents.len(); + let length = paragraph_len(&cell); + if length > max_lengths[index] { max_lengths[index] = length; } @@ -307,11 +323,10 @@ fn render_markdown_table_row( }; let max_width = max_column_widths.get(index).unwrap_or(&0.0); - let mut cell = container .w(Length::Definite(relative(*max_width))) .h_full() - .child(contents) + .children(contents) .px_2() .py_1() .border_color(cx.border_color); @@ -398,18 +413,219 @@ fn render_markdown_code_block( .into_any() } -fn render_markdown_paragraph(parsed: &ParsedMarkdownText, cx: &mut RenderContext) -> AnyElement { +fn render_markdown_paragraph(parsed: &MarkdownParagraph, cx: &mut RenderContext) -> AnyElement { cx.with_common_p(div()) - .child(render_markdown_text(parsed, cx)) + .children(render_markdown_text(parsed, cx)) + .flex() .into_any_element() } -fn render_markdown_text(parsed: &ParsedMarkdownText, cx: &mut RenderContext) -> AnyElement { - let element_id = cx.next_id(&parsed.source_range); +fn render_markdown_text(parsed_new: &MarkdownParagraph, cx: &mut RenderContext) -> Vec { + let mut any_element = vec![]; + // these values are cloned in-order satisfy borrow checker + let syntax_theme = cx.syntax_theme.clone(); + let workspace_clone = cx.workspace.clone(); + let code_span_bg_color = cx.code_span_background_color; + let text_style = cx.text_style.clone(); + + for parsed_region in parsed_new { + match parsed_region { + MarkdownParagraphChunk::Text(parsed) => { + let element_id = cx.next_id(&parsed.source_range); + + let highlights = gpui::combine_highlights( + parsed.highlights.iter().filter_map(|(range, highlight)| { + highlight + .to_highlight_style(&syntax_theme) + .map(|style| (range.clone(), style)) + }), + parsed.regions.iter().zip(&parsed.region_ranges).filter_map( + |(region, range)| { + if region.code { + Some(( + range.clone(), + HighlightStyle { + background_color: Some(code_span_bg_color), + ..Default::default() + }, + )) + } else { + None + } + }, + ), + ); + let mut links = Vec::new(); + let mut link_ranges = Vec::new(); + for (range, region) in parsed.region_ranges.iter().zip(&parsed.regions) { + if let Some(link) = region.link.clone() { + links.push(link); + link_ranges.push(range.clone()); + } + } + let workspace = workspace_clone.clone(); + let element = div() + .child( + InteractiveText::new( + element_id, + StyledText::new(parsed.contents.clone()) + .with_highlights(&text_style, highlights), + ) + .tooltip({ + let links = links.clone(); + let link_ranges = link_ranges.clone(); + move |idx, cx| { + for (ix, range) in link_ranges.iter().enumerate() { + if range.contains(&idx) { + return Some(LinkPreview::new(&links[ix].to_string(), cx)); + } + } + None + } + }) + .on_click( + link_ranges, + move |clicked_range_ix, window_cx| match &links[clicked_range_ix] { + Link::Web { url } => window_cx.open_url(url), + Link::Path { path, .. } => { + if let Some(workspace) = &workspace { + _ = workspace.update(window_cx, |workspace, cx| { + workspace + .open_abs_path(path.clone(), false, cx) + .detach(); + }); + } + } + }, + ), + ) + .into_any(); + any_element.push(element); + } + + MarkdownParagraphChunk::Image(image) => { + let (link, source_range, image_source, alt_text) = match image { + Image::Web { + link, + source_range, + url, + alt_text, + } => ( + link, + source_range, + Resource::Uri(url.clone().into()), + alt_text, + ), + Image::Path { + link, + source_range, + path, + alt_text, + .. + } => { + let image_path = Path::new(path.to_str().unwrap()); + ( + link, + source_range, + Resource::Path(Arc::from(image_path)), + alt_text, + ) + } + }; + + let element_id = cx.next_id(source_range); + + match link { + None => { + let fallback_workspace = workspace_clone.clone(); + let fallback_syntax_theme = syntax_theme.clone(); + let fallback_text_style = text_style.clone(); + let fallback_alt_text = alt_text.clone(); + let element_id_new = element_id.clone(); + let element = div() + .child(img(ImageSource::Resource(image_source)).with_fallback({ + move || { + fallback_text( + fallback_alt_text.clone().unwrap(), + element_id.clone(), + &fallback_syntax_theme, + code_span_bg_color, + fallback_workspace.clone(), + &fallback_text_style, + ) + } + })) + .id(element_id_new) + .into_any(); + any_element.push(element); + } + Some(link) => { + let link_click = link.clone(); + let link_tooltip = link.clone(); + let fallback_workspace = workspace_clone.clone(); + let fallback_syntax_theme = syntax_theme.clone(); + let fallback_text_style = text_style.clone(); + let fallback_alt_text = alt_text.clone(); + let element_id_new = element_id.clone(); + let image_element = div() + .child(img(ImageSource::Resource(image_source)).with_fallback({ + move || { + fallback_text( + fallback_alt_text.clone().unwrap(), + element_id.clone(), + &fallback_syntax_theme, + code_span_bg_color, + fallback_workspace.clone(), + &fallback_text_style, + ) + } + })) + .id(element_id_new) + .tooltip(move |cx| LinkPreview::new(&link_tooltip.to_string(), cx)) + .on_click({ + let workspace = workspace_clone.clone(); + move |_event, window_cx| match &link_click { + Link::Web { url } => window_cx.open_url(url), + Link::Path { path, .. } => { + if let Some(workspace) = &workspace { + _ = workspace.update(window_cx, |workspace, cx| { + workspace + .open_abs_path(path.clone(), false, cx) + .detach(); + }); + } + } + } + }) + .into_any(); + any_element.push(image_element); + } + } + } + } + } + + any_element +} + +fn render_markdown_rule(cx: &mut RenderContext) -> AnyElement { + let rule = div().w_full().h(px(2.)).bg(cx.border_color); + div().pt_3().pb_3().child(rule).into_any() +} + +fn fallback_text( + parsed: ParsedMarkdownText, + source_range: ElementId, + syntax_theme: &theme::SyntaxTheme, + code_span_bg_color: Hsla, + workspace: Option>, + text_style: &TextStyle, +) -> AnyElement { + let element_id = source_range; let highlights = gpui::combine_highlights( parsed.highlights.iter().filter_map(|(range, highlight)| { - let highlight = highlight.to_highlight_style(&cx.syntax_theme)?; + let highlight = highlight.to_highlight_style(syntax_theme)?; Some((range.clone(), highlight)) }), parsed @@ -421,7 +637,7 @@ fn render_markdown_text(parsed: &ParsedMarkdownText, cx: &mut RenderContext) -> Some(( range.clone(), HighlightStyle { - background_color: Some(cx.code_span_background_color), + background_color: Some(code_span_bg_color), ..Default::default() }, )) @@ -430,7 +646,6 @@ fn render_markdown_text(parsed: &ParsedMarkdownText, cx: &mut RenderContext) -> } }), ); - let mut links = Vec::new(); let mut link_ranges = Vec::new(); for (range, region) in parsed.region_ranges.iter().zip(&parsed.regions) { @@ -439,45 +654,38 @@ fn render_markdown_text(parsed: &ParsedMarkdownText, cx: &mut RenderContext) -> link_ranges.push(range.clone()); } } - - let workspace = cx.workspace.clone(); - - InteractiveText::new( - element_id, - StyledText::new(parsed.contents.clone()).with_highlights(&cx.text_style, highlights), - ) - .tooltip({ - let links = links.clone(); - let link_ranges = link_ranges.clone(); - move |idx, cx| { - for (ix, range) in link_ranges.iter().enumerate() { - if range.contains(&idx) { - return Some(LinkPreview::new(&links[ix].to_string(), cx)); + let element = div() + .child( + InteractiveText::new( + element_id, + StyledText::new(parsed.contents.clone()).with_highlights(text_style, highlights), + ) + .tooltip({ + let links = links.clone(); + let link_ranges = link_ranges.clone(); + move |idx, cx| { + for (ix, range) in link_ranges.iter().enumerate() { + if range.contains(&idx) { + return Some(LinkPreview::new(&links[ix].to_string(), cx)); + } + } + None } - } - None - } - }) - .on_click( - link_ranges, - move |clicked_range_ix, window_cx| match &links[clicked_range_ix] { - Link::Web { url } => window_cx.open_url(url), - Link::Path { - path, - display_path: _, - } => { - if let Some(workspace) = &workspace { - _ = workspace.update(window_cx, |workspace, cx| { - workspace.open_abs_path(path.clone(), false, cx).detach(); - }); - } - } - }, - ) - .into_any_element() -} - -fn render_markdown_rule(cx: &mut RenderContext) -> AnyElement { - let rule = div().w_full().h(px(2.)).bg(cx.border_color); - div().pt_3().pb_3().child(rule).into_any() + }) + .on_click( + link_ranges, + move |clicked_range_ix, window_cx| match &links[clicked_range_ix] { + Link::Web { url } => window_cx.open_url(url), + Link::Path { path, .. } => { + if let Some(workspace) = &workspace { + _ = workspace.update(window_cx, |workspace, cx| { + workspace.open_abs_path(path.clone(), false, cx).detach(); + }); + } + } + }, + ), + ) + .into_any(); + return element; } diff --git a/crates/notifications/src/notification_store.rs b/crates/notifications/src/notification_store.rs index 5c3de53ee1..a61f1da1c4 100644 --- a/crates/notifications/src/notification_store.rs +++ b/crates/notifications/src/notification_store.rs @@ -238,11 +238,8 @@ impl NotificationStore { ) -> Result<()> { this.update(&mut cx, |this, cx| { if let Some(notification) = envelope.payload.notification { - if let Some(rpc::Notification::ChannelMessageMention { - message_id, - sender_id: _, - channel_id: _, - }) = Notification::from_proto(¬ification) + if let Some(rpc::Notification::ChannelMessageMention { message_id, .. }) = + Notification::from_proto(¬ification) { let fetch_message_task = this.channel_store.update(cx, |this, cx| { this.fetch_channel_messages(vec![message_id], cx) diff --git a/crates/repl/src/notebook/cell.rs b/crates/repl/src/notebook/cell.rs index 055e4c09f8..12d11853fb 100644 --- a/crates/repl/src/notebook/cell.rs +++ b/crates/repl/src/notebook/cell.rs @@ -114,7 +114,7 @@ impl Cell { id, metadata, source, - attachments: _, + .. } => { let source = source.join(""); diff --git a/crates/rich_text/src/rich_text.rs b/crates/rich_text/src/rich_text.rs index 80b7786c24..df830419d3 100644 --- a/crates/rich_text/src/rich_text.rs +++ b/crates/rich_text/src/rich_text.rs @@ -310,12 +310,7 @@ pub fn render_markdown_mut( } Event::Start(tag) => match tag { Tag::Paragraph => new_paragraph(text, &mut list_stack), - Tag::Heading { - level: _, - id: _, - classes: _, - attrs: _, - } => { + Tag::Heading { .. } => { new_paragraph(text, &mut list_stack); bold_depth += 1; } @@ -333,12 +328,7 @@ pub fn render_markdown_mut( Tag::Emphasis => italic_depth += 1, Tag::Strong => bold_depth += 1, Tag::Strikethrough => strikethrough_depth += 1, - Tag::Link { - link_type: _, - dest_url, - title: _, - id: _, - } => link_url = Some(dest_url.to_string()), + Tag::Link { dest_url, .. } => link_url = Some(dest_url.to_string()), Tag::List(number) => { list_stack.push((number, false)); } From c28f5b11f82e9e67901659462aed9ace54de6cd9 Mon Sep 17 00:00:00 2001 From: teapo <75266237+4teapo@users.noreply.github.com> Date: Fri, 22 Nov 2024 22:50:25 +0000 Subject: [PATCH 114/886] Allow overrides for json-language-server settings (#20748) Closes #20739 The JSON LSP adapter now merges user settings with cached settings, and util::merge_json_value_into pushes array contents from source to target. --- crates/languages/src/json.rs | 21 ++++++++++++++++----- crates/util/src/util.rs | 6 ++++++ 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index c0c7e6f453..9c6315d2e2 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -9,7 +9,7 @@ use http_client::github::{latest_github_release, GitHubLspBinaryVersion}; use language::{LanguageRegistry, LanguageToolchainStore, LspAdapter, LspAdapterDelegate}; use lsp::{LanguageServerBinary, LanguageServerName}; use node_runtime::NodeRuntime; -use project::ContextProviderWithTasks; +use project::{lsp_store::language_server_settings, ContextProviderWithTasks}; use serde_json::{json, Value}; use settings::{KeymapFile, SettingsJsonSchemaParams, SettingsStore}; use smol::{ @@ -25,7 +25,7 @@ use std::{ sync::{Arc, OnceLock}, }; use task::{TaskTemplate, TaskTemplates, VariableName}; -use util::{fs::remove_matching, maybe, ResultExt}; +use util::{fs::remove_matching, maybe, merge_json_value_into, ResultExt}; const SERVER_PATH: &str = "node_modules/vscode-langservers-extracted/bin/vscode-json-language-server"; @@ -194,15 +194,26 @@ impl LspAdapter for JsonLspAdapter { async fn workspace_configuration( self: Arc, - _: &Arc, + delegate: &Arc, _: Arc, cx: &mut AsyncAppContext, ) -> Result { - cx.update(|cx| { + let mut config = cx.update(|cx| { self.workspace_config .get_or_init(|| Self::get_workspace_config(self.languages.language_names(), cx)) .clone() - }) + })?; + + let project_options = cx.update(|cx| { + language_server_settings(delegate.as_ref(), &self.name(), cx) + .and_then(|s| s.settings.clone()) + })?; + + if let Some(override_options) = project_options { + merge_json_value_into(override_options, &mut config); + } + + Ok(config) } fn language_ids(&self) -> HashMap { diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index 5141f85797..fe3f7ef9a0 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -149,6 +149,12 @@ pub fn merge_json_value_into(source: serde_json::Value, target: &mut serde_json: } } + (Value::Array(source), Value::Array(target)) => { + for value in source { + target.push(value); + } + } + (source, target) => *target = source, } } From 8240a52a392cc920fe6d545b5f2a0d4b90df4697 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 22 Nov 2024 14:59:40 -0800 Subject: [PATCH 115/886] Prevent panels from being resized past the edge of the workspace (#20637) Closes #20593 Release Notes: - Fixed a bug where it is possible to get in near-unrecoverable panel state by resizing the panel past the edge of the workspace. Co-authored-by: Trace --- crates/workspace/src/dock.rs | 12 +++- crates/workspace/src/workspace.rs | 97 ++++++++++++++++++++++--------- 2 files changed, 79 insertions(+), 30 deletions(-) diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 30ab109879..acc47cd11e 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -15,7 +15,7 @@ use std::sync::Arc; use ui::{h_flex, ContextMenu, IconButton, Tooltip}; use ui::{prelude::*, right_click_menu}; -const RESIZE_HANDLE_SIZE: Pixels = Pixels(6.); +pub(crate) const RESIZE_HANDLE_SIZE: Pixels = Pixels(6.); pub enum PanelEvent { ZoomIn, @@ -574,6 +574,7 @@ impl Dock { pub fn resize_active_panel(&mut self, size: Option, cx: &mut ViewContext) { if let Some(entry) = self.panel_entries.get_mut(self.active_panel_index) { let size = size.map(|size| size.max(RESIZE_HANDLE_SIZE).round()); + entry.panel.set_size(size, cx); cx.notify(); } @@ -593,6 +594,15 @@ impl Dock { dispatch_context } + + pub fn clamp_panel_size(&mut self, max_size: Pixels, cx: &mut WindowContext) { + let max_size = px((max_size.0 - RESIZE_HANDLE_SIZE.0).abs()); + for panel in self.panel_entries.iter().map(|entry| &entry.panel) { + if panel.size(cx) > max_size { + panel.set_size(Some(max_size.max(RESIZE_HANDLE_SIZE)), cx); + } + } + } } impl Render for Dock { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 45de781577..42db3183bd 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -21,7 +21,7 @@ use client::{ }; use collections::{hash_map, HashMap, HashSet}; use derive_more::{Deref, DerefMut}; -use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle}; +use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle, RESIZE_HANDLE_SIZE}; use futures::{ channel::{ mpsc::{self, UnboundedReceiver, UnboundedSender}, @@ -4824,7 +4824,27 @@ impl Render for Workspace { let this = cx.view().clone(); canvas( move |bounds, cx| { - this.update(cx, |this, _cx| this.bounds = bounds) + this.update(cx, |this, cx| { + let bounds_changed = this.bounds != bounds; + this.bounds = bounds; + + if bounds_changed { + this.left_dock.update(cx, |dock, cx| { + dock.clamp_panel_size(bounds.size.width, cx) + }); + + this.right_dock.update(cx, |dock, cx| { + dock.clamp_panel_size(bounds.size.width, cx) + }); + + this.bottom_dock.update(cx, |dock, cx| { + dock.clamp_panel_size( + bounds.size.height, + cx, + ) + }); + } + }) }, |_, _, _| {}, ) @@ -4836,42 +4856,27 @@ impl Render for Workspace { |workspace, e: &DragMoveEvent, cx| { match e.drag(cx).0 { DockPosition::Left => { - let size = e.event.position.x - - workspace.bounds.left(); - workspace.left_dock.update( + resize_left_dock( + e.event.position.x + - workspace.bounds.left(), + workspace, cx, - |left_dock, cx| { - left_dock.resize_active_panel( - Some(size), - cx, - ); - }, ); } DockPosition::Right => { - let size = workspace.bounds.right() - - e.event.position.x; - workspace.right_dock.update( + resize_right_dock( + workspace.bounds.right() + - e.event.position.x, + workspace, cx, - |right_dock, cx| { - right_dock.resize_active_panel( - Some(size), - cx, - ); - }, ); } DockPosition::Bottom => { - let size = workspace.bounds.bottom() - - e.event.position.y; - workspace.bottom_dock.update( + resize_bottom_dock( + workspace.bounds.bottom() + - e.event.position.y, + workspace, cx, - |bottom_dock, cx| { - bottom_dock.resize_active_panel( - Some(size), - cx, - ); - }, ); } } @@ -4959,6 +4964,40 @@ impl Render for Workspace { } } +fn resize_bottom_dock( + new_size: Pixels, + workspace: &mut Workspace, + cx: &mut ViewContext<'_, Workspace>, +) { + let size = new_size.min(workspace.bounds.bottom() - RESIZE_HANDLE_SIZE); + workspace.bottom_dock.update(cx, |bottom_dock, cx| { + bottom_dock.resize_active_panel(Some(size), cx); + }); +} + +fn resize_right_dock( + new_size: Pixels, + workspace: &mut Workspace, + cx: &mut ViewContext<'_, Workspace>, +) { + let size = new_size.max(workspace.bounds.left() - RESIZE_HANDLE_SIZE); + workspace.right_dock.update(cx, |right_dock, cx| { + right_dock.resize_active_panel(Some(size), cx); + }); +} + +fn resize_left_dock( + new_size: Pixels, + workspace: &mut Workspace, + cx: &mut ViewContext<'_, Workspace>, +) { + let size = new_size.min(workspace.bounds.right() - RESIZE_HANDLE_SIZE); + + workspace.left_dock.update(cx, |left_dock, cx| { + left_dock.resize_active_panel(Some(size), cx); + }); +} + impl WorkspaceStore { pub fn new(client: Arc, cx: &mut ModelContext) -> Self { Self { From c9f2c2792c365577daedae5586a3e047dbcd338e Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Fri, 22 Nov 2024 16:03:46 -0700 Subject: [PATCH 116/886] Improve error handling and resource cleanup in `linux/x11/window.rs` (#21079) * Fixes registration of event handler for xinput-2 device changes, revealed by this improvement. * Pushes `.unwrap()` panic-ing outwards to callers. * Includes a description of what the X11 call was doing when a failure was encountered. * Fixes a variety of places where the X11 reply wasn't being inspected for failures. * Destroys windows on failure during setup. New structure makes it possible for the caller of `open_window` to carry on despite failures, and so partially initialized window should be removed (though all calls I looked at also panic currently). Considered pushing this through `linux/x11/client.rs` too but figured it'd be nice to minimize merge conflicts with #20853. Release Notes: - N/A --- crates/gpui/src/platform/linux/x11/client.rs | 20 +- crates/gpui/src/platform/linux/x11/display.rs | 13 +- crates/gpui/src/platform/linux/x11/window.rs | 770 ++++++++++-------- 3 files changed, 454 insertions(+), 349 deletions(-) diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index f6c3af0348..1fd0e9aa66 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -776,11 +776,11 @@ impl X11Client { }, }; let window = self.get_window(event.window)?; - window.configure(bounds); + window.configure(bounds).unwrap(); } Event::PropertyNotify(event) => { let window = self.get_window(event.window)?; - window.property_notify(event); + window.property_notify(event).unwrap(); } Event::FocusIn(event) => { let window = self.get_window(event.event)?; @@ -1258,11 +1258,9 @@ impl LinuxClient for X11Client { .iter() .enumerate() .filter_map(|(root_id, _)| { - Some(Rc::new(X11Display::new( - &state.xcb_connection, - state.scale_factor, - root_id, - )?) as Rc) + Some(Rc::new( + X11Display::new(&state.xcb_connection, state.scale_factor, root_id).ok()?, + ) as Rc) }) .collect() } @@ -1283,11 +1281,9 @@ impl LinuxClient for X11Client { fn display(&self, id: DisplayId) -> Option> { let state = self.0.borrow(); - Some(Rc::new(X11Display::new( - &state.xcb_connection, - state.scale_factor, - id.0 as usize, - )?)) + Some(Rc::new( + X11Display::new(&state.xcb_connection, state.scale_factor, id.0 as usize).ok()?, + )) } fn open_window( diff --git a/crates/gpui/src/platform/linux/x11/display.rs b/crates/gpui/src/platform/linux/x11/display.rs index 871d709fa9..4983e2f5a3 100644 --- a/crates/gpui/src/platform/linux/x11/display.rs +++ b/crates/gpui/src/platform/linux/x11/display.rs @@ -13,12 +13,17 @@ pub(crate) struct X11Display { impl X11Display { pub(crate) fn new( - xc: &XCBConnection, + xcb: &XCBConnection, scale_factor: f32, x_screen_index: usize, - ) -> Option { - let screen = xc.setup().roots.get(x_screen_index).unwrap(); - Some(Self { + ) -> anyhow::Result { + let Some(screen) = xcb.setup().roots.get(x_screen_index) else { + return Err(anyhow::anyhow!( + "No screen found with index {}", + x_screen_index + )); + }; + Ok(Self { x_screen_index, bounds: Bounds { origin: Default::default(), diff --git a/crates/gpui/src/platform/linux/x11/window.rs b/crates/gpui/src/platform/linux/x11/window.rs index 4df1b50f3f..ae9abe7146 100644 --- a/crates/gpui/src/platform/linux/x11/window.rs +++ b/crates/gpui/src/platform/linux/x11/window.rs @@ -1,4 +1,4 @@ -use anyhow::Context; +use anyhow::{anyhow, Context}; use crate::{ platform::blade::{BladeRenderer, BladeSurfaceConfig}, @@ -14,6 +14,8 @@ use raw_window_handle as rwh; use util::{maybe, ResultExt}; use x11rb::{ connection::Connection, + cookie::{Cookie, VoidCookie}, + errors::ConnectionError, properties::WmSizeHints, protocol::{ sync, @@ -25,7 +27,7 @@ use x11rb::{ }; use std::{ - cell::RefCell, ffi::c_void, mem::size_of, num::NonZeroU32, ops::Div, ptr::NonNull, rc::Rc, + cell::RefCell, ffi::c_void, fmt::Display, num::NonZeroU32, ops::Div, ptr::NonNull, rc::Rc, sync::Arc, }; @@ -77,17 +79,16 @@ x11rb::atom_manager! { } } -fn query_render_extent(xcb_connection: &XCBConnection, x_window: xproto::Window) -> gpu::Extent { - let reply = xcb_connection - .get_geometry(x_window) - .unwrap() - .reply() - .unwrap(); - gpu::Extent { +fn query_render_extent( + xcb: &Rc, + x_window: xproto::Window, +) -> anyhow::Result { + let reply = get_reply(|| "X11 GetGeometry failed.", xcb.get_geometry(x_window))?; + Ok(gpu::Extent { width: reply.width as u32, height: reply.height as u32, depth: 1, - } + }) } impl ResizeEdge { @@ -148,7 +149,7 @@ impl EdgeConstraints { } } -#[derive(Debug)] +#[derive(Copy, Clone, Debug)] struct Visual { id: xproto::Visualid, colormap: u32, @@ -163,8 +164,8 @@ struct VisualSet { black_pixel: u32, } -fn find_visuals(xcb_connection: &XCBConnection, screen_index: usize) -> VisualSet { - let screen = &xcb_connection.setup().roots[screen_index]; +fn find_visuals(xcb: &XCBConnection, screen_index: usize) -> VisualSet { + let screen = &xcb.setup().roots[screen_index]; let mut set = VisualSet { inherit: Visual { id: screen.root_visual, @@ -277,13 +278,16 @@ impl X11WindowState { pub(crate) struct X11WindowStatePtr { pub state: Rc>, pub(crate) callbacks: Rc>, - xcb_connection: Rc, + xcb: Rc, x_window: xproto::Window, } impl rwh::HasWindowHandle for RawWindow { fn window_handle(&self) -> Result { - let non_zero = NonZeroU32::new(self.window_id).unwrap(); + let Some(non_zero) = NonZeroU32::new(self.window_id) else { + log::error!("RawWindow.window_id zero when getting window handle."); + return Err(rwh::HandleError::Unavailable); + }; let mut handle = rwh::XcbWindowHandle::new(non_zero); handle.visual_id = NonZeroU32::new(self.visual_id); Ok(unsafe { rwh::WindowHandle::borrow_raw(handle.into()) }) @@ -291,7 +295,10 @@ impl rwh::HasWindowHandle for RawWindow { } impl rwh::HasDisplayHandle for RawWindow { fn display_handle(&self) -> Result { - let non_zero = NonNull::new(self.connection).unwrap(); + let Some(non_zero) = NonNull::new(self.connection) else { + log::error!("Null RawWindow.connection when getting display handle."); + return Err(rwh::HandleError::Unavailable); + }; let handle = rwh::XcbDisplayHandle::new(Some(non_zero), self.screen_id as i32); Ok(unsafe { rwh::DisplayHandle::borrow_raw(handle.into()) }) } @@ -308,6 +315,43 @@ impl rwh::HasDisplayHandle for X11Window { } } +fn check_reply( + failure_context: F, + result: Result>, ConnectionError>, +) -> anyhow::Result<()> +where + C: Display + Send + Sync + 'static, + F: FnOnce() -> C, +{ + result + .map_err(|connection_error| anyhow!(connection_error)) + .and_then(|response| { + response + .check() + .map_err(|error_response| anyhow!(error_response)) + }) + .with_context(failure_context) +} + +fn get_reply( + failure_context: F, + result: Result, O>, ConnectionError>, +) -> anyhow::Result +where + C: Display + Send + Sync + 'static, + F: FnOnce() -> C, + O: x11rb::x11_utils::TryParse, +{ + result + .map_err(|connection_error| anyhow!(connection_error)) + .and_then(|response| { + response + .reply() + .map_err(|error_response| anyhow!(error_response)) + }) + .with_context(failure_context) +} + impl X11WindowState { #[allow(clippy::too_many_arguments)] pub fn new( @@ -315,7 +359,7 @@ impl X11WindowState { client: X11ClientStatePtr, executor: ForegroundExecutor, params: WindowParams, - xcb_connection: &Rc, + xcb: &Rc, client_side_decorations_supported: bool, x_main_screen_index: usize, x_window: xproto::Window, @@ -327,7 +371,7 @@ impl X11WindowState { .display_id .map_or(x_main_screen_index, |did| did.0 as usize); - let visual_set = find_visuals(&xcb_connection, x_screen_index); + let visual_set = find_visuals(&xcb, x_screen_index); let visual = match visual_set.transparent { Some(visual) => visual, @@ -341,12 +385,12 @@ impl X11WindowState { let colormap = if visual.colormap != 0 { visual.colormap } else { - let id = xcb_connection.generate_id().unwrap(); + let id = xcb.generate_id()?; log::info!("Creating colormap {}", id); - xcb_connection - .create_colormap(xproto::ColormapAlloc::NONE, id, visual_set.root, visual.id) - .unwrap() - .check()?; + check_reply( + || format!("X11 CreateColormap failed. id: {}", id), + xcb.create_colormap(xproto::ColormapAlloc::NONE, id, visual_set.root, visual.id), + )?; id }; @@ -370,8 +414,12 @@ impl X11WindowState { bounds.size.height = 600.into(); } - xcb_connection - .create_window( + check_reply( + || { + format!("X11 CreateWindow failed. depth: {}, x_window: {}, visual_set.root: {}, bounds.origin.x.0: {}, bounds.origin.y.0: {}, bounds.size.width.0: {}, bounds.size.height.0: {}", + visual.depth, x_window, visual_set.root, bounds.origin.x.0 + 2, bounds.origin.y.0, bounds.size.width.0, bounds.size.height.0) + }, + xcb.create_window( visual.depth, x_window, visual_set.root, @@ -383,189 +431,205 @@ impl X11WindowState { xproto::WindowClass::INPUT_OUTPUT, visual.id, &win_aux, - ) - .unwrap() - .check().with_context(|| { - format!("CreateWindow request to X server failed. depth: {}, x_window: {}, visual_set.root: {}, bounds.origin.x.0: {}, bounds.origin.y.0: {}, bounds.size.width.0: {}, bounds.size.height.0: {}", - visual.depth, x_window, visual_set.root, bounds.origin.x.0 + 2, bounds.origin.y.0, bounds.size.width.0, bounds.size.height.0) - })?; + ), + )?; - if let Some(size) = params.window_min_size { - let mut size_hints = WmSizeHints::new(); - size_hints.min_size = Some((size.width.0 as i32, size.height.0 as i32)); - size_hints - .set_normal_hints(xcb_connection, x_window) - .unwrap(); - } + // Collect errors during setup, so that window can be destroyed on failure. + let setup_result = maybe!({ + if let Some(size) = params.window_min_size { + let mut size_hints = WmSizeHints::new(); + let min_size = (size.width.0 as i32, size.height.0 as i32); + size_hints.min_size = Some(min_size); + check_reply( + || { + format!( + "X11 change of WM_SIZE_HINTS failed. min_size: {:?}", + min_size + ) + }, + size_hints.set_normal_hints(xcb, x_window), + )?; + } - let reply = xcb_connection - .get_geometry(x_window) - .unwrap() - .reply() - .unwrap(); - if reply.x == 0 && reply.y == 0 { - bounds.origin.x.0 += 2; - // Work around a bug where our rendered content appears - // outside the window bounds when opened at the default position - // (14px, 49px on X + Gnome + Ubuntu 22). - xcb_connection - .configure_window( - x_window, - &xproto::ConfigureWindowAux::new() - .x(bounds.origin.x.0) - .y(bounds.origin.y.0), - ) - .unwrap(); - } - if let Some(titlebar) = params.titlebar { - if let Some(title) = titlebar.title { - xcb_connection - .change_property8( + let reply = get_reply(|| "X11 GetGeometry failed.", xcb.get_geometry(x_window))?; + if reply.x == 0 && reply.y == 0 { + bounds.origin.x.0 += 2; + // Work around a bug where our rendered content appears + // outside the window bounds when opened at the default position + // (14px, 49px on X + Gnome + Ubuntu 22). + let x = bounds.origin.x.0; + let y = bounds.origin.y.0; + check_reply( + || format!("X11 ConfigureWindow failed. x: {}, y: {}", x, y), + xcb.configure_window(x_window, &xproto::ConfigureWindowAux::new().x(x).y(y)), + )?; + } + if let Some(titlebar) = params.titlebar { + if let Some(title) = titlebar.title { + check_reply( + || "X11 ChangeProperty8 on window title failed.", + xcb.change_property8( + xproto::PropMode::REPLACE, + x_window, + xproto::AtomEnum::WM_NAME, + xproto::AtomEnum::STRING, + title.as_bytes(), + ), + )?; + } + } + if params.kind == WindowKind::PopUp { + check_reply( + || "X11 ChangeProperty32 setting window type for pop-up failed.", + xcb.change_property32( xproto::PropMode::REPLACE, x_window, - xproto::AtomEnum::WM_NAME, - xproto::AtomEnum::STRING, - title.as_bytes(), - ) - .unwrap(); + atoms._NET_WM_WINDOW_TYPE, + xproto::AtomEnum::ATOM, + &[atoms._NET_WM_WINDOW_TYPE_NOTIFICATION], + ), + )?; } - } - if params.kind == WindowKind::PopUp { - xcb_connection - .change_property32( + + check_reply( + || "X11 ChangeProperty32 setting protocols failed.", + xcb.change_property32( xproto::PropMode::REPLACE, x_window, - atoms._NET_WM_WINDOW_TYPE, + atoms.WM_PROTOCOLS, xproto::AtomEnum::ATOM, - &[atoms._NET_WM_WINDOW_TYPE_NOTIFICATION], - ) - .unwrap(); + &[atoms.WM_DELETE_WINDOW, atoms._NET_WM_SYNC_REQUEST], + ), + )?; + + get_reply( + || "X11 sync protocol initialize failed.", + sync::initialize(xcb, 3, 1), + )?; + let sync_request_counter = xcb.generate_id()?; + check_reply( + || "X11 sync CreateCounter failed.", + sync::create_counter(xcb, sync_request_counter, sync::Int64 { lo: 0, hi: 0 }), + )?; + + check_reply( + || "X11 ChangeProperty32 setting sync request counter failed.", + xcb.change_property32( + xproto::PropMode::REPLACE, + x_window, + atoms._NET_WM_SYNC_REQUEST_COUNTER, + xproto::AtomEnum::CARDINAL, + &[sync_request_counter], + ), + )?; + + check_reply( + || "X11 XiSelectEvents failed.", + xcb.xinput_xi_select_events( + x_window, + &[xinput::EventMask { + deviceid: XINPUT_ALL_DEVICE_GROUPS, + mask: vec![ + xinput::XIEventMask::MOTION + | xinput::XIEventMask::BUTTON_PRESS + | xinput::XIEventMask::BUTTON_RELEASE + | xinput::XIEventMask::ENTER + | xinput::XIEventMask::LEAVE, + ], + }], + ), + )?; + + check_reply( + || "X11 XiSelectEvents for device changes failed.", + xcb.xinput_xi_select_events( + x_window, + &[xinput::EventMask { + deviceid: XINPUT_ALL_DEVICES, + mask: vec![ + xinput::XIEventMask::HIERARCHY | xinput::XIEventMask::DEVICE_CHANGED, + ], + }], + ), + )?; + + xcb.flush().with_context(|| "X11 Flush failed.")?; + + let raw = RawWindow { + connection: as_raw_xcb_connection::AsRawXcbConnection::as_raw_xcb_connection(xcb) + as *mut _, + screen_id: x_screen_index, + window_id: x_window, + visual_id: visual.id, + }; + let gpu = Arc::new( + unsafe { + gpu::Context::init_windowed( + &raw, + gpu::ContextDesc { + validation: false, + capture: false, + overlay: false, + }, + ) + } + .map_err(|e| anyhow!("{:?}", e))?, + ); + + let config = BladeSurfaceConfig { + // Note: this has to be done after the GPU init, or otherwise + // the sizes are immediately invalidated. + size: query_render_extent(xcb, x_window)?, + // We set it to transparent by default, even if we have client-side + // decorations, since those seem to work on X11 even without `true` here. + // If the window appearance changes, then the renderer will get updated + // too + transparent: false, + }; + check_reply(|| "X11 MapWindow failed.", xcb.map_window(x_window))?; + + let display = Rc::new(X11Display::new(xcb, scale_factor, x_screen_index)?); + + Ok(Self { + client, + executor, + display, + _raw: raw, + x_root_window: visual_set.root, + bounds: bounds.to_pixels(scale_factor), + scale_factor, + renderer: BladeRenderer::new(gpu, config), + atoms: *atoms, + input_handler: None, + active: false, + hovered: false, + fullscreen: false, + maximized_vertical: false, + maximized_horizontal: false, + hidden: false, + appearance, + handle, + background_appearance: WindowBackgroundAppearance::Opaque, + destroyed: false, + client_side_decorations_supported, + decorations: WindowDecorations::Server, + last_insets: [0, 0, 0, 0], + edge_constraints: None, + counter_id: sync_request_counter, + last_sync_counter: None, + }) + }); + + if setup_result.is_err() { + check_reply( + || "X11 DestroyWindow failed while cleaning it up after setup failure.", + xcb.destroy_window(x_window), + )?; + xcb.flush() + .with_context(|| "X11 Flush failed while cleaning it up after setup failure.")?; } - xcb_connection - .change_property32( - xproto::PropMode::REPLACE, - x_window, - atoms.WM_PROTOCOLS, - xproto::AtomEnum::ATOM, - &[atoms.WM_DELETE_WINDOW, atoms._NET_WM_SYNC_REQUEST], - ) - .unwrap(); - - sync::initialize(xcb_connection, 3, 1).unwrap(); - let sync_request_counter = xcb_connection.generate_id().unwrap(); - sync::create_counter( - xcb_connection, - sync_request_counter, - sync::Int64 { lo: 0, hi: 0 }, - ) - .unwrap(); - - xcb_connection - .change_property32( - xproto::PropMode::REPLACE, - x_window, - atoms._NET_WM_SYNC_REQUEST_COUNTER, - xproto::AtomEnum::CARDINAL, - &[sync_request_counter], - ) - .unwrap(); - - xcb_connection - .xinput_xi_select_events( - x_window, - &[xinput::EventMask { - deviceid: XINPUT_ALL_DEVICE_GROUPS, - mask: vec![ - xinput::XIEventMask::MOTION - | xinput::XIEventMask::BUTTON_PRESS - | xinput::XIEventMask::BUTTON_RELEASE - | xinput::XIEventMask::ENTER - | xinput::XIEventMask::LEAVE, - ], - }], - ) - .unwrap(); - - xcb_connection - .xinput_xi_select_events( - x_window, - &[xinput::EventMask { - deviceid: XINPUT_ALL_DEVICES, - mask: vec![ - xinput::XIEventMask::HIERARCHY, - xinput::XIEventMask::DEVICE_CHANGED, - ], - }], - ) - .unwrap(); - - xcb_connection.flush().unwrap(); - - let raw = RawWindow { - connection: as_raw_xcb_connection::AsRawXcbConnection::as_raw_xcb_connection( - xcb_connection, - ) as *mut _, - screen_id: x_screen_index, - window_id: x_window, - visual_id: visual.id, - }; - let gpu = Arc::new( - unsafe { - gpu::Context::init_windowed( - &raw, - gpu::ContextDesc { - validation: false, - capture: false, - overlay: false, - }, - ) - } - .map_err(|e| anyhow::anyhow!("{:?}", e))?, - ); - - let config = BladeSurfaceConfig { - // Note: this has to be done after the GPU init, or otherwise - // the sizes are immediately invalidated. - size: query_render_extent(xcb_connection, x_window), - // We set it to transparent by default, even if we have client-side - // decorations, since those seem to work on X11 even without `true` here. - // If the window appearance changes, then the renderer will get updated - // too - transparent: false, - }; - xcb_connection.map_window(x_window).unwrap(); - - Ok(Self { - client, - executor, - display: Rc::new( - X11Display::new(xcb_connection, scale_factor, x_screen_index).unwrap(), - ), - _raw: raw, - x_root_window: visual_set.root, - bounds: bounds.to_pixels(scale_factor), - scale_factor, - renderer: BladeRenderer::new(gpu, config), - atoms: *atoms, - input_handler: None, - active: false, - hovered: false, - fullscreen: false, - maximized_vertical: false, - maximized_horizontal: false, - hidden: false, - appearance, - handle, - background_appearance: WindowBackgroundAppearance::Opaque, - destroyed: false, - client_side_decorations_supported, - decorations: WindowDecorations::Server, - last_insets: [0, 0, 0, 0], - edge_constraints: None, - counter_id: sync_request_counter, - last_sync_counter: None, - }) + setup_result } fn content_size(&self) -> Size { @@ -577,6 +641,28 @@ impl X11WindowState { } } +/// A handle to an X11 window which destroys it on Drop. +pub struct X11WindowHandle { + id: xproto::Window, + xcb: Rc, +} + +impl Drop for X11WindowHandle { + fn drop(&mut self) { + maybe!({ + check_reply( + || "X11 DestroyWindow failed while dropping X11WindowHandle.", + self.xcb.destroy_window(self.id), + )?; + self.xcb + .flush() + .with_context(|| "X11 Flush failed while dropping X11WindowHandle.")?; + anyhow::Ok(()) + }) + .log_err(); + } +} + pub(crate) struct X11Window(pub X11WindowStatePtr); impl Drop for X11Window { @@ -585,13 +671,17 @@ impl Drop for X11Window { state.renderer.destroy(); let destroy_x_window = maybe!({ - self.0.xcb_connection.unmap_window(self.0.x_window)?; - self.0.xcb_connection.destroy_window(self.0.x_window)?; - self.0.xcb_connection.flush()?; + check_reply( + || "X11 DestroyWindow failure.", + self.0.xcb.destroy_window(self.0.x_window), + )?; + self.0 + .xcb + .flush() + .with_context(|| "X11 Flush failed after calling DestroyWindow.")?; anyhow::Ok(()) }) - .context("unmapping and destroying X11 window") .log_err(); if destroy_x_window.is_some() { @@ -627,7 +717,7 @@ impl X11Window { client: X11ClientStatePtr, executor: ForegroundExecutor, params: WindowParams, - xcb_connection: &Rc, + xcb: &Rc, client_side_decorations_supported: bool, x_main_screen_index: usize, x_window: xproto::Window, @@ -641,7 +731,7 @@ impl X11Window { client, executor, params, - xcb_connection, + xcb, client_side_decorations_supported, x_main_screen_index, x_window, @@ -650,17 +740,23 @@ impl X11Window { appearance, )?)), callbacks: Rc::new(RefCell::new(Callbacks::default())), - xcb_connection: xcb_connection.clone(), + xcb: xcb.clone(), x_window, }; let state = ptr.state.borrow_mut(); - ptr.set_wm_properties(state); + ptr.set_wm_properties(state)?; Ok(Self(ptr)) } - fn set_wm_hints(&self, wm_hint_property_state: WmHintPropertyState, prop1: u32, prop2: u32) { + fn set_wm_hints C>( + &self, + failure_context: F, + wm_hint_property_state: WmHintPropertyState, + prop1: u32, + prop2: u32, + ) -> anyhow::Result<()> { let state = self.0.state.borrow(); let message = ClientMessageEvent::new( 32, @@ -668,51 +764,45 @@ impl X11Window { state.atoms._NET_WM_STATE, [wm_hint_property_state as u32, prop1, prop2, 1, 0], ); - self.0 - .xcb_connection - .send_event( + check_reply( + failure_context, + self.0.xcb.send_event( false, state.x_root_window, EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY, message, - ) - .unwrap() - .check() - .unwrap(); + ), + ) } - fn get_root_position(&self, position: Point) -> TranslateCoordinatesReply { + fn get_root_position( + &self, + position: Point, + ) -> anyhow::Result { let state = self.0.state.borrow(); - self.0 - .xcb_connection - .translate_coordinates( + get_reply( + || "X11 TranslateCoordinates failed.", + self.0.xcb.translate_coordinates( self.0.x_window, state.x_root_window, (position.x.0 * state.scale_factor) as i16, (position.y.0 * state.scale_factor) as i16, - ) - .unwrap() - .reply() - .unwrap() + ), + ) } - fn send_moveresize(&self, flag: u32) { + fn send_moveresize(&self, flag: u32) -> anyhow::Result<()> { let state = self.0.state.borrow(); - self.0 - .xcb_connection - .ungrab_pointer(x11rb::CURRENT_TIME) - .unwrap() - .check() - .unwrap(); + check_reply( + || "X11 UngrabPointer before move/resize of window ailed.", + self.0.xcb.ungrab_pointer(x11rb::CURRENT_TIME), + )?; - let pointer = self - .0 - .xcb_connection - .query_pointer(self.0.x_window) - .unwrap() - .reply() - .unwrap(); + let pointer = get_reply( + || "X11 QueryPointer before move/resize of window failed.", + self.0.xcb.query_pointer(self.0.x_window), + )?; let message = ClientMessageEvent::new( 32, self.0.x_window, @@ -725,17 +815,21 @@ impl X11Window { 0, ], ); - self.0 - .xcb_connection - .send_event( + check_reply( + || "X11 SendEvent to move/resize window failed.", + self.0.xcb.send_event( false, state.x_root_window, EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY, message, - ) - .unwrap(); + ), + )?; - self.0.xcb_connection.flush().unwrap(); + self.flush() + } + + fn flush(&self) -> anyhow::Result<()> { + self.0.xcb.flush().with_context(|| "X11 Flush failed.") } } @@ -751,51 +845,56 @@ impl X11WindowStatePtr { } } - pub fn property_notify(&self, event: xproto::PropertyNotifyEvent) { + pub fn property_notify(&self, event: xproto::PropertyNotifyEvent) -> anyhow::Result<()> { let mut state = self.state.borrow_mut(); if event.atom == state.atoms._NET_WM_STATE { - self.set_wm_properties(state); + self.set_wm_properties(state)?; } else if event.atom == state.atoms._GTK_EDGE_CONSTRAINTS { - self.set_edge_constraints(state); + self.set_edge_constraints(state)?; } + Ok(()) } - fn set_edge_constraints(&self, mut state: std::cell::RefMut) { - let reply = self - .xcb_connection - .get_property( + fn set_edge_constraints( + &self, + mut state: std::cell::RefMut, + ) -> anyhow::Result<()> { + let reply = get_reply( + || "X11 GetProperty for _GTK_EDGE_CONSTRAINTS failed.", + self.xcb.get_property( false, self.x_window, state.atoms._GTK_EDGE_CONSTRAINTS, xproto::AtomEnum::CARDINAL, 0, 4, - ) - .unwrap() - .reply() - .unwrap(); + ), + )?; if reply.value_len != 0 { let atom = u32::from_ne_bytes(reply.value[0..4].try_into().unwrap()); let edge_constraints = EdgeConstraints::from_atom(atom); state.edge_constraints.replace(edge_constraints); } + + Ok(()) } - fn set_wm_properties(&self, mut state: std::cell::RefMut) { - let reply = self - .xcb_connection - .get_property( + fn set_wm_properties( + &self, + mut state: std::cell::RefMut, + ) -> anyhow::Result<()> { + let reply = get_reply( + || "X11 GetProperty for _NET_WM_STATE failed.", + self.xcb.get_property( false, self.x_window, state.atoms._NET_WM_STATE, xproto::AtomEnum::ATOM, 0, u32::MAX, - ) - .unwrap() - .reply() - .unwrap(); + ), + )?; let atoms = reply .value @@ -821,6 +920,8 @@ impl X11WindowStatePtr { state.hidden = true; } } + + Ok(()) } pub fn close(&self) { @@ -912,7 +1013,7 @@ impl X11WindowStatePtr { bounds } - pub fn configure(&self, bounds: Bounds) { + pub fn configure(&self, bounds: Bounds) -> anyhow::Result<()> { let mut resize_args = None; let is_resize; { @@ -930,7 +1031,7 @@ impl X11WindowStatePtr { state.bounds = bounds; } - let gpu_size = query_render_extent(&self.xcb_connection, self.x_window); + let gpu_size = query_render_extent(&self.xcb, self.x_window)?; if true { state.renderer.update_drawable_size(size( DevicePixels(gpu_size.width as i32), @@ -939,7 +1040,10 @@ impl X11WindowStatePtr { resize_args = Some((state.content_size(), state.scale_factor)); } if let Some(value) = state.last_sync_counter.take() { - sync::set_counter(&self.xcb_connection, state.counter_id, value).unwrap(); + check_reply( + || "X11 sync SetCounter failed.", + sync::set_counter(&self.xcb, state.counter_id, value), + )?; } } @@ -951,9 +1055,11 @@ impl X11WindowStatePtr { } if !is_resize { if let Some(ref mut fun) = callbacks.moved { - fun() + fun(); } } + + Ok(()) } pub fn set_active(&self, focus: bool) { @@ -1025,13 +1131,11 @@ impl PlatformWindow for X11Window { } fn mouse_position(&self) -> Point { - let reply = self - .0 - .xcb_connection - .query_pointer(self.0.x_window) - .unwrap() - .reply() - .unwrap(); + let reply = get_reply( + || "X11 QueryPointer failed.", + self.0.xcb.query_pointer(self.0.x_window), + ) + .unwrap(); Point::new((reply.root_x as u32).into(), (reply.root_y as u32).into()) } @@ -1073,7 +1177,7 @@ impl PlatformWindow for X11Window { data, ); self.0 - .xcb_connection + .xcb .send_event( false, self.0.state.borrow().x_root_window, @@ -1082,14 +1186,14 @@ impl PlatformWindow for X11Window { ) .log_err(); self.0 - .xcb_connection + .xcb .set_input_focus( xproto::InputFocus::POINTER_ROOT, self.0.x_window, xproto::Time::CURRENT_TIME, ) .log_err(); - self.0.xcb_connection.flush().unwrap(); + self.flush().unwrap(); } fn is_active(&self) -> bool { @@ -1101,28 +1205,30 @@ impl PlatformWindow for X11Window { } fn set_title(&mut self, title: &str) { - self.0 - .xcb_connection - .change_property8( + check_reply( + || "X11 ChangeProperty8 on WM_NAME failed.", + self.0.xcb.change_property8( xproto::PropMode::REPLACE, self.0.x_window, xproto::AtomEnum::WM_NAME, xproto::AtomEnum::STRING, title.as_bytes(), - ) - .unwrap(); + ), + ) + .unwrap(); - self.0 - .xcb_connection - .change_property8( + check_reply( + || "X11 ChangeProperty8 on _NET_WM_NAME failed.", + self.0.xcb.change_property8( xproto::PropMode::REPLACE, self.0.x_window, self.0.state.borrow().atoms._NET_WM_NAME, self.0.state.borrow().atoms.UTF8_STRING, title.as_bytes(), - ) - .unwrap(); - self.0.xcb_connection.flush().unwrap(); + ), + ) + .unwrap(); + self.flush().unwrap(); } fn set_app_id(&mut self, app_id: &str) { @@ -1131,18 +1237,17 @@ impl PlatformWindow for X11Window { data.push(b'\0'); data.extend(app_id.bytes()); // class - self.0 - .xcb_connection - .change_property8( + check_reply( + || "X11 ChangeProperty8 for WM_CLASS failed.", + self.0.xcb.change_property8( xproto::PropMode::REPLACE, self.0.x_window, xproto::AtomEnum::WM_CLASS, xproto::AtomEnum::STRING, &data, - ) - .unwrap() - .check() - .unwrap(); + ), + ) + .unwrap(); } fn set_edited(&mut self, _edited: bool) { @@ -1169,35 +1274,38 @@ impl PlatformWindow for X11Window { state.atoms.WM_CHANGE_STATE, [WINDOW_ICONIC_STATE, 0, 0, 0, 0], ); - self.0 - .xcb_connection - .send_event( + check_reply( + || "X11 SendEvent to minimize window failed.", + self.0.xcb.send_event( false, state.x_root_window, EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY, message, - ) - .unwrap() - .check() - .unwrap(); + ), + ) + .unwrap(); } fn zoom(&self) { let state = self.0.state.borrow(); self.set_wm_hints( + || "X11 SendEvent to maximize a window failed.", WmHintPropertyState::Toggle, state.atoms._NET_WM_STATE_MAXIMIZED_VERT, state.atoms._NET_WM_STATE_MAXIMIZED_HORZ, - ); + ) + .unwrap(); } fn toggle_fullscreen(&self) { let state = self.0.state.borrow(); self.set_wm_hints( + || "X11 SendEvent to fullscreen a window failed.", WmHintPropertyState::Toggle, state.atoms._NET_WM_STATE_FULLSCREEN, xproto::AtomEnum::NONE.into(), - ); + ) + .unwrap(); } fn is_fullscreen(&self) -> bool { @@ -1253,14 +1361,13 @@ impl PlatformWindow for X11Window { fn show_window_menu(&self, position: Point) { let state = self.0.state.borrow(); - self.0 - .xcb_connection - .ungrab_pointer(x11rb::CURRENT_TIME) - .unwrap() - .check() - .unwrap(); + check_reply( + || "X11 UngrabPointer failed.", + self.0.xcb.ungrab_pointer(x11rb::CURRENT_TIME), + ) + .unwrap(); - let coords = self.get_root_position(position); + let coords = self.get_root_position(position).unwrap(); let message = ClientMessageEvent::new( 32, self.0.x_window, @@ -1273,26 +1380,25 @@ impl PlatformWindow for X11Window { 0, ], ); - self.0 - .xcb_connection - .send_event( + check_reply( + || "X11 SendEvent to show window menu failed.", + self.0.xcb.send_event( false, state.x_root_window, EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY, message, - ) - .unwrap() - .check() - .unwrap(); + ), + ) + .unwrap(); } fn start_window_move(&self) { const MOVERESIZE_MOVE: u32 = 8; - self.send_moveresize(MOVERESIZE_MOVE); + self.send_moveresize(MOVERESIZE_MOVE).unwrap(); } fn start_window_resize(&self, edge: ResizeEdge) { - self.send_moveresize(edge.to_moveresize()); + self.send_moveresize(edge.to_moveresize()).unwrap(); } fn window_decorations(&self) -> crate::Decorations { @@ -1355,9 +1461,9 @@ impl PlatformWindow for X11Window { if state.last_insets != insets { state.last_insets = insets; - self.0 - .xcb_connection - .change_property( + check_reply( + || "X11 ChangeProperty for _GTK_FRAME_EXTENTS failed.", + self.0.xcb.change_property( xproto::PropMode::REPLACE, self.0.x_window, state.atoms._GTK_FRAME_EXTENTS, @@ -1365,10 +1471,9 @@ impl PlatformWindow for X11Window { size_of::() as u8 * 8, 4, bytemuck::cast_slice::(&insets), - ) - .unwrap() - .check() - .unwrap(); + ), + ) + .unwrap(); } } @@ -1390,20 +1495,19 @@ impl PlatformWindow for X11Window { WindowDecorations::Client => [1 << 1, 0, 0, 0, 0], }; - self.0 - .xcb_connection - .change_property( + check_reply( + || "X11 ChangeProperty for _MOTIF_WM_HINTS failed.", + self.0.xcb.change_property( xproto::PropMode::REPLACE, self.0.x_window, state.atoms._MOTIF_WM_HINTS, state.atoms._MOTIF_WM_HINTS, - std::mem::size_of::() as u8 * 8, + size_of::() as u8 * 8, 5, bytemuck::cast_slice::(&hints_data), - ) - .unwrap() - .check() - .unwrap(); + ), + ) + .unwrap(); match decorations { WindowDecorations::Server => { From 1cfcdfa7ac06665c73f2a15bcdbfb65629563a15 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 22 Nov 2024 19:02:32 -0500 Subject: [PATCH 117/886] Overhaul extension registration (#21083) This PR overhauls extension registration in order to make it more modular. The `extension` crate now contains an `ExtensionHostProxy` that can be used to register various proxies that the extension host can use to interact with the rest of the system. There are now a number of different proxy traits representing the various pieces of functionality that can be provided by an extension. The respective crates that provide this functionality can implement their corresponding proxy trait in order to register a proxy that the extension host will use to register the bits of functionality provided by the extension. Release Notes: - N/A --- Cargo.lock | 49 ++- Cargo.toml | 4 + crates/assistant/src/assistant.rs | 3 +- .../src/assistant_slash_command.rs | 1 + .../src/extension_slash_command.rs | 28 +- crates/collab/Cargo.toml | 1 + .../remote_editing_collaboration_tests.rs | 4 + crates/context_servers/Cargo.toml | 1 + crates/context_servers/src/context_servers.rs | 2 + .../src/extension_context_server.rs | 78 +++++ crates/extension/Cargo.toml | 1 + crates/extension/src/extension.rs | 9 +- crates/extension/src/extension_host_proxy.rs | 324 ++++++++++++++++++ crates/extension_host/Cargo.toml | 2 + crates/extension_host/src/extension_host.rs | 146 ++------ .../src/extension_store_test.rs | 121 +------ crates/extension_host/src/headless_host.rs | 121 ++----- crates/extension_host/src/wasm_host.rs | 12 +- .../src/wasm_host/wit/since_v0_0_1.rs | 7 +- .../src/wasm_host/wit/since_v0_1_0.rs | 7 +- .../src/wasm_host/wit/since_v0_2_0.rs | 9 +- crates/extensions_ui/Cargo.toml | 7 - .../src/extension_registration_hooks.rs | 209 ----------- crates/extensions_ui/src/extensions_ui.rs | 3 - .../src/extension_indexed_docs_provider.rs | 28 +- crates/indexed_docs/src/indexed_docs.rs | 7 + crates/indexed_docs/src/registry.rs | 2 +- crates/language_extension/Cargo.toml | 25 ++ crates/language_extension/LICENSE-GPL | 1 + .../src/extension_lsp_adapter.rs | 58 +++- .../src/language_extension.rs | 51 +++ crates/remote_server/Cargo.toml | 2 + crates/remote_server/src/headless_project.rs | 6 +- .../remote_server/src/remote_editing_tests.rs | 3 + crates/remote_server/src/unix.rs | 5 + crates/snippet_provider/Cargo.toml | 1 + .../snippet_provider/src/extension_snippet.rs | 26 ++ crates/snippet_provider/src/lib.rs | 2 + crates/theme_extension/Cargo.toml | 19 + crates/theme_extension/LICENSE-GPL | 1 + crates/theme_extension/src/theme_extension.rs | 47 +++ crates/zed/Cargo.toml | 6 +- crates/zed/src/main.rs | 26 +- 43 files changed, 874 insertions(+), 591 deletions(-) create mode 100644 crates/context_servers/src/extension_context_server.rs create mode 100644 crates/extension/src/extension_host_proxy.rs delete mode 100644 crates/extensions_ui/src/extension_registration_hooks.rs create mode 100644 crates/language_extension/Cargo.toml create mode 120000 crates/language_extension/LICENSE-GPL rename crates/{extension_host => language_extension}/src/extension_lsp_adapter.rs (93%) create mode 100644 crates/language_extension/src/language_extension.rs create mode 100644 crates/snippet_provider/src/extension_snippet.rs create mode 100644 crates/theme_extension/Cargo.toml create mode 120000 crates/theme_extension/LICENSE-GPL create mode 100644 crates/theme_extension/src/theme_extension.rs diff --git a/Cargo.lock b/Cargo.lock index 514338c590..9881c23e84 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2601,6 +2601,7 @@ dependencies = [ "editor", "env_logger 0.11.5", "envy", + "extension", "file_finder", "fs", "futures 0.3.31", @@ -2842,6 +2843,7 @@ dependencies = [ "anyhow", "collections", "command_palette_hooks", + "extension", "futures 0.3.31", "gpui", "log", @@ -4127,6 +4129,7 @@ dependencies = [ "language", "log", "lsp", + "parking_lot", "semantic_version", "serde", "serde_json", @@ -4178,6 +4181,7 @@ dependencies = [ "gpui", "http_client", "language", + "language_extension", "log", "lsp", "node_runtime", @@ -4196,6 +4200,7 @@ dependencies = [ "task", "tempfile", "theme", + "theme_extension", "toml 0.8.19", "url", "util", @@ -4209,21 +4214,15 @@ name = "extensions_ui" version = "0.1.0" dependencies = [ "anyhow", - "assistant_slash_command", "client", "collections", - "context_servers", "db", "editor", - "extension", "extension_host", "fs", "fuzzy", "gpui", - "indexed_docs", "language", - "log", - "lsp", "num-format", "picker", "project", @@ -4232,7 +4231,6 @@ dependencies = [ "serde", "settings", "smallvec", - "snippet_provider", "theme", "ui", "util", @@ -6533,6 +6531,23 @@ dependencies = [ "util", ] +[[package]] +name = "language_extension" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "collections", + "extension", + "futures 0.3.31", + "gpui", + "language", + "lsp", + "serde", + "serde_json", + "util", +] + [[package]] name = "language_model" version = "0.1.0" @@ -9853,6 +9868,7 @@ dependencies = [ "client", "clock", "env_logger 0.11.5", + "extension", "extension_host", "fork", "fs", @@ -9862,6 +9878,7 @@ dependencies = [ "gpui", "http_client", "language", + "language_extension", "languages", "libc", "log", @@ -11304,6 +11321,7 @@ version = "0.1.0" dependencies = [ "anyhow", "collections", + "extension", "fs", "futures 0.3.31", "gpui", @@ -12357,6 +12375,17 @@ dependencies = [ "uuid", ] +[[package]] +name = "theme_extension" +version = "0.1.0" +dependencies = [ + "anyhow", + "extension", + "fs", + "gpui", + "theme", +] + [[package]] name = "theme_importer" version = "0.1.0" @@ -15466,7 +15495,6 @@ dependencies = [ "ashpd", "assets", "assistant", - "assistant_slash_command", "async-watch", "audio", "auto_update", @@ -15483,12 +15511,12 @@ dependencies = [ "collections", "command_palette", "command_palette_hooks", - "context_servers", "copilot", "db", "diagnostics", "editor", "env_logger 0.11.5", + "extension", "extension_host", "extensions_ui", "feature_flags", @@ -15503,11 +15531,11 @@ dependencies = [ "gpui", "http_client", "image_viewer", - "indexed_docs", "inline_completion_button", "install_cli", "journal", "language", + "language_extension", "language_model", "language_models", "language_selector", @@ -15556,6 +15584,7 @@ dependencies = [ "telemetry_events", "terminal_view", "theme", + "theme_extension", "theme_selector", "time", "toolchain_selector", diff --git a/Cargo.toml b/Cargo.toml index c12079a26a..b071ca19d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,7 @@ members = [ "crates/install_cli", "crates/journal", "crates/language", + "crates/language_extension", "crates/language_model", "crates/language_models", "crates/language_selector", @@ -116,6 +117,7 @@ members = [ "crates/terminal_view", "crates/text", "crates/theme", + "crates/theme_extension", "crates/theme_importer", "crates/theme_selector", "crates/time_format", @@ -230,6 +232,7 @@ inline_completion_button = { path = "crates/inline_completion_button" } install_cli = { path = "crates/install_cli" } journal = { path = "crates/journal" } language = { path = "crates/language" } +language_extension = { path = "crates/language_extension" } language_model = { path = "crates/language_model" } language_models = { path = "crates/language_models" } language_selector = { path = "crates/language_selector" } @@ -292,6 +295,7 @@ terminal = { path = "crates/terminal" } terminal_view = { path = "crates/terminal_view" } text = { path = "crates/text" } theme = { path = "crates/theme" } +theme_extension = { path = "crates/theme_extension" } theme_importer = { path = "crates/theme_importer" } theme_selector = { path = "crates/theme_selector" } time_format = { path = "crates/time_format" } diff --git a/crates/assistant/src/assistant.rs b/crates/assistant/src/assistant.rs index 88500247c3..b891c3da2a 100644 --- a/crates/assistant/src/assistant.rs +++ b/crates/assistant/src/assistant.rs @@ -33,7 +33,6 @@ use feature_flags::FeatureFlagAppExt; use fs::Fs; use gpui::impl_actions; use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal}; -use indexed_docs::IndexedDocsRegistry; pub(crate) use inline_assistant::*; use language_model::{ LanguageModelId, LanguageModelProviderId, LanguageModelRegistry, LanguageModelResponseMessage, @@ -275,7 +274,7 @@ pub fn init( client.telemetry().clone(), cx, ); - IndexedDocsRegistry::init_global(cx); + indexed_docs::init(cx); CommandPaletteFilter::update_global(cx, |filter, _cx| { filter.hide_namespace(Assistant::NAMESPACE); diff --git a/crates/assistant_slash_command/src/assistant_slash_command.rs b/crates/assistant_slash_command/src/assistant_slash_command.rs index 3fb2dc66b2..59d98ee770 100644 --- a/crates/assistant_slash_command/src/assistant_slash_command.rs +++ b/crates/assistant_slash_command/src/assistant_slash_command.rs @@ -18,6 +18,7 @@ use workspace::{ui::IconName, Workspace}; pub fn init(cx: &mut AppContext) { SlashCommandRegistry::default_global(cx); + extension_slash_command::init(cx); } #[derive(Clone, Copy, Debug, PartialEq, Eq)] diff --git a/crates/assistant_slash_command/src/extension_slash_command.rs b/crates/assistant_slash_command/src/extension_slash_command.rs index bfb2688066..2279f93b1c 100644 --- a/crates/assistant_slash_command/src/extension_slash_command.rs +++ b/crates/assistant_slash_command/src/extension_slash_command.rs @@ -3,17 +3,39 @@ use std::sync::{atomic::AtomicBool, Arc}; use anyhow::Result; use async_trait::async_trait; -use extension::{Extension, WorktreeDelegate}; -use gpui::{Task, WeakView, WindowContext}; +use extension::{Extension, ExtensionHostProxy, ExtensionSlashCommandProxy, WorktreeDelegate}; +use gpui::{AppContext, Task, WeakView, WindowContext}; use language::{BufferSnapshot, LspAdapterDelegate}; use ui::prelude::*; use workspace::Workspace; use crate::{ ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection, - SlashCommandResult, + SlashCommandRegistry, SlashCommandResult, }; +pub fn init(cx: &mut AppContext) { + let proxy = ExtensionHostProxy::default_global(cx); + proxy.register_slash_command_proxy(SlashCommandRegistryProxy { + slash_command_registry: SlashCommandRegistry::global(cx), + }); +} + +struct SlashCommandRegistryProxy { + slash_command_registry: Arc, +} + +impl ExtensionSlashCommandProxy for SlashCommandRegistryProxy { + fn register_slash_command( + &self, + extension: Arc, + command: extension::SlashCommand, + ) { + self.slash_command_registry + .register_command(ExtensionSlashCommand::new(extension, command), false) + } +} + /// An adapter that allows an [`LspAdapterDelegate`] to be used as a [`WorktreeDelegate`]. struct WorktreeDelegateAdapter(Arc); diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index a69eb53740..d3da1c2816 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -90,6 +90,7 @@ collections = { workspace = true, features = ["test-support"] } ctor.workspace = true editor = { workspace = true, features = ["test-support"] } env_logger.workspace = true +extension.workspace = true file_finder.workspace = true fs = { workspace = true, features = ["test-support"] } git = { workspace = true, features = ["test-support"] } diff --git a/crates/collab/src/tests/remote_editing_collaboration_tests.rs b/crates/collab/src/tests/remote_editing_collaboration_tests.rs index 00f52e9972..5b8d57a12a 100644 --- a/crates/collab/src/tests/remote_editing_collaboration_tests.rs +++ b/crates/collab/src/tests/remote_editing_collaboration_tests.rs @@ -1,6 +1,7 @@ use crate::tests::TestServer; use call::ActiveCall; use collections::HashSet; +use extension::ExtensionHostProxy; use fs::{FakeFs, Fs as _}; use futures::StreamExt as _; use gpui::{BackgroundExecutor, Context as _, SemanticVersion, TestAppContext, UpdateGlobal as _}; @@ -81,6 +82,7 @@ async fn test_sharing_an_ssh_remote_project( http_client: remote_http_client, node_runtime: node, languages, + extension_host_proxy: Arc::new(ExtensionHostProxy::new()), }, cx, ) @@ -243,6 +245,7 @@ async fn test_ssh_collaboration_git_branches( http_client: remote_http_client, node_runtime: node, languages, + extension_host_proxy: Arc::new(ExtensionHostProxy::new()), }, cx, ) @@ -400,6 +403,7 @@ async fn test_ssh_collaboration_formatting_with_prettier( http_client: remote_http_client, node_runtime: NodeRuntime::unavailable(), languages, + extension_host_proxy: Arc::new(ExtensionHostProxy::new()), }, cx, ) diff --git a/crates/context_servers/Cargo.toml b/crates/context_servers/Cargo.toml index de1e991887..cbd762c8c4 100644 --- a/crates/context_servers/Cargo.toml +++ b/crates/context_servers/Cargo.toml @@ -15,6 +15,7 @@ path = "src/context_servers.rs" anyhow.workspace = true collections.workspace = true command_palette_hooks.workspace = true +extension.workspace = true futures.workspace = true gpui.workspace = true log.workspace = true diff --git a/crates/context_servers/src/context_servers.rs b/crates/context_servers/src/context_servers.rs index 87a98ca14f..e6b52aaee2 100644 --- a/crates/context_servers/src/context_servers.rs +++ b/crates/context_servers/src/context_servers.rs @@ -1,4 +1,5 @@ pub mod client; +mod extension_context_server; pub mod manager; pub mod protocol; mod registry; @@ -19,6 +20,7 @@ pub const CONTEXT_SERVERS_NAMESPACE: &'static str = "context_servers"; pub fn init(cx: &mut AppContext) { ContextServerSettings::register(cx); ContextServerFactoryRegistry::default_global(cx); + extension_context_server::init(cx); CommandPaletteFilter::update_global(cx, |filter, _cx| { filter.hide_namespace(CONTEXT_SERVERS_NAMESPACE); diff --git a/crates/context_servers/src/extension_context_server.rs b/crates/context_servers/src/extension_context_server.rs new file mode 100644 index 0000000000..092816b5e6 --- /dev/null +++ b/crates/context_servers/src/extension_context_server.rs @@ -0,0 +1,78 @@ +use std::sync::Arc; + +use extension::{Extension, ExtensionContextServerProxy, ExtensionHostProxy, ProjectDelegate}; +use gpui::{AppContext, Model}; + +use crate::manager::ServerCommand; +use crate::ContextServerFactoryRegistry; + +struct ExtensionProject { + worktree_ids: Vec, +} + +impl ProjectDelegate for ExtensionProject { + fn worktree_ids(&self) -> Vec { + self.worktree_ids.clone() + } +} + +pub fn init(cx: &mut AppContext) { + let proxy = ExtensionHostProxy::default_global(cx); + proxy.register_context_server_proxy(ContextServerFactoryRegistryProxy { + context_server_factory_registry: ContextServerFactoryRegistry::global(cx), + }); +} + +struct ContextServerFactoryRegistryProxy { + context_server_factory_registry: Model, +} + +impl ExtensionContextServerProxy for ContextServerFactoryRegistryProxy { + fn register_context_server( + &self, + extension: Arc, + id: Arc, + cx: &mut AppContext, + ) { + self.context_server_factory_registry + .update(cx, |registry, _| { + registry.register_server_factory( + id.clone(), + Arc::new({ + move |project, cx| { + log::info!( + "loading command for context server {id} from extension {}", + extension.manifest().id + ); + + let id = id.clone(); + let extension = extension.clone(); + cx.spawn(|mut cx| async move { + let extension_project = + project.update(&mut cx, |project, cx| { + Arc::new(ExtensionProject { + worktree_ids: project + .visible_worktrees(cx) + .map(|worktree| worktree.read(cx).id().to_proto()) + .collect(), + }) + })?; + + let command = extension + .context_server_command(id.clone(), extension_project) + .await?; + + log::info!("loaded command for context server {id}: {command:?}"); + + Ok(ServerCommand { + path: command.command, + args: command.args, + env: Some(command.env.into_iter().collect()), + }) + }) + } + }), + ) + }); + } +} diff --git a/crates/extension/Cargo.toml b/crates/extension/Cargo.toml index a96cf7155a..b92771d09d 100644 --- a/crates/extension/Cargo.toml +++ b/crates/extension/Cargo.toml @@ -24,6 +24,7 @@ http_client.workspace = true language.workspace = true log.workspace = true lsp.workspace = true +parking_lot.workspace = true semantic_version.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/crates/extension/src/extension.rs b/crates/extension/src/extension.rs index fe9b49909b..2eb067ca40 100644 --- a/crates/extension/src/extension.rs +++ b/crates/extension/src/extension.rs @@ -1,4 +1,5 @@ pub mod extension_builder; +mod extension_host_proxy; mod extension_manifest; mod types; @@ -9,13 +10,19 @@ use ::lsp::LanguageServerName; use anyhow::{anyhow, bail, Context as _, Result}; use async_trait::async_trait; use fs::normalize_path; -use gpui::Task; +use gpui::{AppContext, Task}; use language::LanguageName; use semantic_version::SemanticVersion; +pub use crate::extension_host_proxy::*; pub use crate::extension_manifest::*; pub use crate::types::*; +/// Initializes the `extension` crate. +pub fn init(cx: &mut AppContext) { + ExtensionHostProxy::default_global(cx); +} + #[async_trait] pub trait WorktreeDelegate: Send + Sync + 'static { fn id(&self) -> u64; diff --git a/crates/extension/src/extension_host_proxy.rs b/crates/extension/src/extension_host_proxy.rs new file mode 100644 index 0000000000..8909a6082d --- /dev/null +++ b/crates/extension/src/extension_host_proxy.rs @@ -0,0 +1,324 @@ +use std::path::PathBuf; +use std::sync::Arc; + +use anyhow::Result; +use fs::Fs; +use gpui::{AppContext, Global, ReadGlobal, SharedString, Task}; +use language::{LanguageMatcher, LanguageName, LanguageServerBinaryStatus, LoadedLanguage}; +use lsp::LanguageServerName; +use parking_lot::RwLock; + +use crate::{Extension, SlashCommand}; + +#[derive(Default)] +struct GlobalExtensionHostProxy(Arc); + +impl Global for GlobalExtensionHostProxy {} + +/// A proxy for interacting with the extension host. +/// +/// This object implements each of the individual proxy types so that their +/// methods can be called directly on it. +#[derive(Default)] +pub struct ExtensionHostProxy { + theme_proxy: RwLock>>, + grammar_proxy: RwLock>>, + language_proxy: RwLock>>, + language_server_proxy: RwLock>>, + snippet_proxy: RwLock>>, + slash_command_proxy: RwLock>>, + context_server_proxy: RwLock>>, + indexed_docs_provider_proxy: RwLock>>, +} + +impl ExtensionHostProxy { + /// Returns the global [`ExtensionHostProxy`]. + pub fn global(cx: &AppContext) -> Arc { + GlobalExtensionHostProxy::global(cx).0.clone() + } + + /// Returns the global [`ExtensionHostProxy`]. + /// + /// Inserts a default [`ExtensionHostProxy`] if one does not yet exist. + pub fn default_global(cx: &mut AppContext) -> Arc { + cx.default_global::().0.clone() + } + + pub fn new() -> Self { + Self { + theme_proxy: RwLock::default(), + grammar_proxy: RwLock::default(), + language_proxy: RwLock::default(), + language_server_proxy: RwLock::default(), + snippet_proxy: RwLock::default(), + slash_command_proxy: RwLock::default(), + context_server_proxy: RwLock::default(), + indexed_docs_provider_proxy: RwLock::default(), + } + } + + pub fn register_theme_proxy(&self, proxy: impl ExtensionThemeProxy) { + self.theme_proxy.write().replace(Arc::new(proxy)); + } + + pub fn register_grammar_proxy(&self, proxy: impl ExtensionGrammarProxy) { + self.grammar_proxy.write().replace(Arc::new(proxy)); + } + + pub fn register_language_proxy(&self, proxy: impl ExtensionLanguageProxy) { + self.language_proxy.write().replace(Arc::new(proxy)); + } + + pub fn register_language_server_proxy(&self, proxy: impl ExtensionLanguageServerProxy) { + self.language_server_proxy.write().replace(Arc::new(proxy)); + } + + pub fn register_snippet_proxy(&self, proxy: impl ExtensionSnippetProxy) { + self.snippet_proxy.write().replace(Arc::new(proxy)); + } + + pub fn register_slash_command_proxy(&self, proxy: impl ExtensionSlashCommandProxy) { + self.slash_command_proxy.write().replace(Arc::new(proxy)); + } + + pub fn register_context_server_proxy(&self, proxy: impl ExtensionContextServerProxy) { + self.context_server_proxy.write().replace(Arc::new(proxy)); + } + + pub fn register_indexed_docs_provider_proxy( + &self, + proxy: impl ExtensionIndexedDocsProviderProxy, + ) { + self.indexed_docs_provider_proxy + .write() + .replace(Arc::new(proxy)); + } +} + +pub trait ExtensionThemeProxy: Send + Sync + 'static { + fn list_theme_names(&self, theme_path: PathBuf, fs: Arc) -> Task>>; + + fn remove_user_themes(&self, themes: Vec); + + fn load_user_theme(&self, theme_path: PathBuf, fs: Arc) -> Task>; + + fn reload_current_theme(&self, cx: &mut AppContext); +} + +impl ExtensionThemeProxy for ExtensionHostProxy { + fn list_theme_names(&self, theme_path: PathBuf, fs: Arc) -> Task>> { + let Some(proxy) = self.theme_proxy.read().clone() else { + return Task::ready(Ok(Vec::new())); + }; + + proxy.list_theme_names(theme_path, fs) + } + + fn remove_user_themes(&self, themes: Vec) { + let Some(proxy) = self.theme_proxy.read().clone() else { + return; + }; + + proxy.remove_user_themes(themes) + } + + fn load_user_theme(&self, theme_path: PathBuf, fs: Arc) -> Task> { + let Some(proxy) = self.theme_proxy.read().clone() else { + return Task::ready(Ok(())); + }; + + proxy.load_user_theme(theme_path, fs) + } + + fn reload_current_theme(&self, cx: &mut AppContext) { + let Some(proxy) = self.theme_proxy.read().clone() else { + return; + }; + + proxy.reload_current_theme(cx) + } +} + +pub trait ExtensionGrammarProxy: Send + Sync + 'static { + fn register_grammars(&self, grammars: Vec<(Arc, PathBuf)>); +} + +impl ExtensionGrammarProxy for ExtensionHostProxy { + fn register_grammars(&self, grammars: Vec<(Arc, PathBuf)>) { + let Some(proxy) = self.grammar_proxy.read().clone() else { + return; + }; + + proxy.register_grammars(grammars) + } +} + +pub trait ExtensionLanguageProxy: Send + Sync + 'static { + fn register_language( + &self, + language: LanguageName, + grammar: Option>, + matcher: LanguageMatcher, + load: Arc Result + Send + Sync + 'static>, + ); + + fn remove_languages( + &self, + languages_to_remove: &[LanguageName], + grammars_to_remove: &[Arc], + ); +} + +impl ExtensionLanguageProxy for ExtensionHostProxy { + fn register_language( + &self, + language: LanguageName, + grammar: Option>, + matcher: LanguageMatcher, + load: Arc Result + Send + Sync + 'static>, + ) { + let Some(proxy) = self.language_proxy.read().clone() else { + return; + }; + + proxy.register_language(language, grammar, matcher, load) + } + + fn remove_languages( + &self, + languages_to_remove: &[LanguageName], + grammars_to_remove: &[Arc], + ) { + let Some(proxy) = self.language_proxy.read().clone() else { + return; + }; + + proxy.remove_languages(languages_to_remove, grammars_to_remove) + } +} + +pub trait ExtensionLanguageServerProxy: Send + Sync + 'static { + fn register_language_server( + &self, + extension: Arc, + language_server_id: LanguageServerName, + language: LanguageName, + ); + + fn remove_language_server( + &self, + language: &LanguageName, + language_server_id: &LanguageServerName, + ); + + fn update_language_server_status( + &self, + language_server_id: LanguageServerName, + status: LanguageServerBinaryStatus, + ); +} + +impl ExtensionLanguageServerProxy for ExtensionHostProxy { + fn register_language_server( + &self, + extension: Arc, + language_server_id: LanguageServerName, + language: LanguageName, + ) { + let Some(proxy) = self.language_server_proxy.read().clone() else { + return; + }; + + proxy.register_language_server(extension, language_server_id, language) + } + + fn remove_language_server( + &self, + language: &LanguageName, + language_server_id: &LanguageServerName, + ) { + let Some(proxy) = self.language_server_proxy.read().clone() else { + return; + }; + + proxy.remove_language_server(language, language_server_id) + } + + fn update_language_server_status( + &self, + language_server_id: LanguageServerName, + status: LanguageServerBinaryStatus, + ) { + let Some(proxy) = self.language_server_proxy.read().clone() else { + return; + }; + + proxy.update_language_server_status(language_server_id, status) + } +} + +pub trait ExtensionSnippetProxy: Send + Sync + 'static { + fn register_snippet(&self, path: &PathBuf, snippet_contents: &str) -> Result<()>; +} + +impl ExtensionSnippetProxy for ExtensionHostProxy { + fn register_snippet(&self, path: &PathBuf, snippet_contents: &str) -> Result<()> { + let Some(proxy) = self.snippet_proxy.read().clone() else { + return Ok(()); + }; + + proxy.register_snippet(path, snippet_contents) + } +} + +pub trait ExtensionSlashCommandProxy: Send + Sync + 'static { + fn register_slash_command(&self, extension: Arc, command: SlashCommand); +} + +impl ExtensionSlashCommandProxy for ExtensionHostProxy { + fn register_slash_command(&self, extension: Arc, command: SlashCommand) { + let Some(proxy) = self.slash_command_proxy.read().clone() else { + return; + }; + + proxy.register_slash_command(extension, command) + } +} + +pub trait ExtensionContextServerProxy: Send + Sync + 'static { + fn register_context_server( + &self, + extension: Arc, + server_id: Arc, + cx: &mut AppContext, + ); +} + +impl ExtensionContextServerProxy for ExtensionHostProxy { + fn register_context_server( + &self, + extension: Arc, + server_id: Arc, + cx: &mut AppContext, + ) { + let Some(proxy) = self.context_server_proxy.read().clone() else { + return; + }; + + proxy.register_context_server(extension, server_id, cx) + } +} + +pub trait ExtensionIndexedDocsProviderProxy: Send + Sync + 'static { + fn register_indexed_docs_provider(&self, extension: Arc, provider_id: Arc); +} + +impl ExtensionIndexedDocsProviderProxy for ExtensionHostProxy { + fn register_indexed_docs_provider(&self, extension: Arc, provider_id: Arc) { + let Some(proxy) = self.indexed_docs_provider_proxy.read().clone() else { + return; + }; + + proxy.register_indexed_docs_provider(extension, provider_id) + } +} diff --git a/crates/extension_host/Cargo.toml b/crates/extension_host/Cargo.toml index 31d3df88aa..6e78654b7e 100644 --- a/crates/extension_host/Cargo.toml +++ b/crates/extension_host/Cargo.toml @@ -57,7 +57,9 @@ env_logger.workspace = true fs = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] } language = { workspace = true, features = ["test-support"] } +language_extension.workspace = true parking_lot.workspace = true project = { workspace = true, features = ["test-support"] } reqwest_client.workspace = true theme = { workspace = true, features = ["test-support"] } +theme_extension.workspace = true diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index 85da812795..aab5c258f5 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -1,4 +1,3 @@ -pub mod extension_lsp_adapter; pub mod extension_settings; pub mod headless_host; pub mod wasm_host; @@ -12,8 +11,12 @@ use async_tar::Archive; 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; +use extension::{ + ExtensionContextServerProxy, ExtensionGrammarProxy, ExtensionHostProxy, + ExtensionIndexedDocsProviderProxy, ExtensionLanguageProxy, ExtensionLanguageServerProxy, + ExtensionSlashCommandProxy, ExtensionSnippetProxy, ExtensionThemeProxy, +}; use fs::{Fs, RemoveOptions}; use futures::{ channel::{ @@ -24,15 +27,14 @@ use futures::{ select_biased, AsyncReadExt as _, Future, FutureExt as _, StreamExt as _, }; use gpui::{ - actions, AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, ModelContext, - SharedString, Task, WeakModel, + actions, AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, ModelContext, Task, + WeakModel, }; use http_client::{AsyncBody, HttpClient, HttpClientWithUrl}; use language::{ LanguageConfig, LanguageMatcher, LanguageName, LanguageQueries, LoadedLanguage, QUERY_FILENAME_PREFIXES, }; -use lsp::LanguageServerName; use node_runtime::NodeRuntime; use project::ContextProviderWithTasks; use release_channel::ReleaseChannel; @@ -95,82 +97,8 @@ pub fn is_version_compatible( true } -pub trait ExtensionRegistrationHooks: Send + Sync + 'static { - fn remove_user_themes(&self, _themes: Vec) {} - - fn load_user_theme(&self, _theme_path: PathBuf, _fs: Arc) -> Task> { - Task::ready(Ok(())) - } - - fn list_theme_names( - &self, - _theme_path: PathBuf, - _fs: Arc, - ) -> Task>> { - Task::ready(Ok(Vec::new())) - } - - fn reload_current_theme(&self, _cx: &mut AppContext) {} - - fn register_language( - &self, - _language: LanguageName, - _grammar: Option>, - _matcher: language::LanguageMatcher, - _load: Arc Result + 'static + Send + Sync>, - ) { - } - - fn register_lsp_adapter( - &self, - _extension: Arc, - _language_server_id: LanguageServerName, - _language: LanguageName, - ) { - } - - fn remove_lsp_adapter(&self, _language: &LanguageName, _server_name: &LanguageServerName) {} - - fn register_wasm_grammars(&self, _grammars: Vec<(Arc, PathBuf)>) {} - - fn remove_languages( - &self, - _languages_to_remove: &[LanguageName], - _grammars_to_remove: &[Arc], - ) { - } - - fn register_slash_command( - &self, - _extension: Arc, - _command: extension::SlashCommand, - ) { - } - - fn register_context_server( - &self, - _extension: Arc, - _id: Arc, - _cx: &mut AppContext, - ) { - } - - fn register_docs_provider(&self, _extension: Arc, _provider_id: Arc) {} - - fn register_snippets(&self, _path: &PathBuf, _snippet_contents: &str) -> Result<()> { - Ok(()) - } - - fn update_lsp_status( - &self, - _server_name: lsp::LanguageServerName, - _status: language::LanguageServerBinaryStatus, - ) { - } -} - pub struct ExtensionStore { - pub registration_hooks: Arc, + pub proxy: Arc, pub builder: Arc, pub extension_index: ExtensionIndex, pub fs: Arc, @@ -240,7 +168,7 @@ pub struct ExtensionIndexLanguageEntry { actions!(zed, [ReloadExtensions]); pub fn init( - registration_hooks: Arc, + extension_host_proxy: Arc, fs: Arc, client: Arc, node_runtime: NodeRuntime, @@ -252,7 +180,7 @@ pub fn init( ExtensionStore::new( paths::extensions_dir().clone(), None, - registration_hooks, + extension_host_proxy, fs, client.http_client().clone(), client.http_client().clone(), @@ -284,7 +212,7 @@ impl ExtensionStore { pub fn new( extensions_dir: PathBuf, build_dir: Option, - extension_api: Arc, + extension_host_proxy: Arc, fs: Arc, http_client: Arc, builder_client: Arc, @@ -300,7 +228,7 @@ impl ExtensionStore { 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(), + proxy: extension_host_proxy.clone(), extension_index: Default::default(), installed_dir, index_path, @@ -312,7 +240,7 @@ impl ExtensionStore { fs.clone(), http_client.clone(), node_runtime, - extension_api, + extension_host_proxy, work_dir, cx, ), @@ -1113,16 +1041,16 @@ impl ExtensionStore { grammars_to_remove.extend(extension.manifest.grammars.keys().cloned()); for (language_server_name, config) in extension.manifest.language_servers.iter() { for language in config.languages() { - self.registration_hooks - .remove_lsp_adapter(&language, language_server_name); + self.proxy + .remove_language_server(&language, language_server_name); } } } self.wasm_extensions .retain(|(extension, _)| !extensions_to_unload.contains(&extension.id)); - self.registration_hooks.remove_user_themes(themes_to_remove); - self.registration_hooks + self.proxy.remove_user_themes(themes_to_remove); + self.proxy .remove_languages(&languages_to_remove, &grammars_to_remove); let languages_to_add = new_index @@ -1157,8 +1085,7 @@ impl ExtensionStore { })); } - self.registration_hooks - .register_wasm_grammars(grammars_to_add); + self.proxy.register_grammars(grammars_to_add); for (language_name, language) in languages_to_add { let mut language_path = self.installed_dir.clone(); @@ -1166,7 +1093,7 @@ impl ExtensionStore { Path::new(language.extension.as_ref()), language.path.as_path(), ]); - self.registration_hooks.register_language( + self.proxy.register_language( language_name.clone(), language.grammar.clone(), language.matcher.clone(), @@ -1196,7 +1123,7 @@ impl ExtensionStore { let fs = self.fs.clone(); let wasm_host = self.wasm_host.clone(); let root_dir = self.installed_dir.clone(); - let api = self.registration_hooks.clone(); + let proxy = self.proxy.clone(); let extension_entries = extensions_to_load .iter() .filter_map(|name| new_index.extensions.get(name).cloned()) @@ -1212,13 +1139,17 @@ impl ExtensionStore { let fs = fs.clone(); async move { for theme_path in themes_to_add.into_iter() { - api.load_user_theme(theme_path, fs.clone()).await.log_err(); + proxy + .load_user_theme(theme_path, fs.clone()) + .await + .log_err(); } for snippets_path in &snippets_to_add { if let Some(snippets_contents) = fs.load(snippets_path).await.log_err() { - api.register_snippets(snippets_path, &snippets_contents) + proxy + .register_snippet(snippets_path, &snippets_contents) .log_err(); } } @@ -1259,7 +1190,7 @@ 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( + this.proxy.register_language_server( extension.clone(), language_server_id.clone(), language.clone(), @@ -1268,7 +1199,7 @@ impl ExtensionStore { } for (slash_command_name, slash_command) in &manifest.slash_commands { - this.registration_hooks.register_slash_command( + this.proxy.register_slash_command( extension.clone(), extension::SlashCommand { name: slash_command_name.to_string(), @@ -1283,21 +1214,18 @@ impl ExtensionStore { } for (id, _context_server_entry) in &manifest.context_servers { - this.registration_hooks.register_context_server( - extension.clone(), - id.clone(), - cx, - ); + this.proxy + .register_context_server(extension.clone(), id.clone(), cx); } for (provider_id, _provider) in &manifest.indexed_docs_providers { - this.registration_hooks - .register_docs_provider(extension.clone(), provider_id.clone()); + this.proxy + .register_indexed_docs_provider(extension.clone(), provider_id.clone()); } } this.wasm_extensions.extend(wasm_extensions); - this.registration_hooks.reload_current_theme(cx); + this.proxy.reload_current_theme(cx); }) .ok(); }) @@ -1308,7 +1236,7 @@ impl ExtensionStore { let work_dir = self.wasm_host.work_dir.clone(); let extensions_dir = self.installed_dir.clone(); let index_path = self.index_path.clone(); - let extension_api = self.registration_hooks.clone(); + let proxy = self.proxy.clone(); cx.background_executor().spawn(async move { let start_time = Instant::now(); let mut index = ExtensionIndex::default(); @@ -1334,7 +1262,7 @@ impl ExtensionStore { fs.clone(), extension_dir, &mut index, - extension_api.clone(), + proxy.clone(), ) .await .log_err(); @@ -1357,7 +1285,7 @@ impl ExtensionStore { fs: Arc, extension_dir: PathBuf, index: &mut ExtensionIndex, - extension_api: Arc, + proxy: Arc, ) -> Result<()> { let mut extension_manifest = ExtensionManifest::load(fs.clone(), &extension_dir).await?; let extension_id = extension_manifest.id.clone(); @@ -1409,7 +1337,7 @@ impl ExtensionStore { continue; }; - let Some(theme_families) = extension_api + let Some(theme_families) = proxy .list_theme_names(theme_path.clone(), fs.clone()) .await .log_err() diff --git a/crates/extension_host/src/extension_store_test.rs b/crates/extension_host/src/extension_store_test.rs index 5d78539617..1359b5b202 100644 --- a/crates/extension_host/src/extension_store_test.rs +++ b/crates/extension_host/src/extension_store_test.rs @@ -1,20 +1,16 @@ -use crate::extension_lsp_adapter::ExtensionLspAdapter; use crate::{ Event, ExtensionIndex, ExtensionIndexEntry, ExtensionIndexLanguageEntry, ExtensionIndexThemeEntry, ExtensionManifest, ExtensionSettings, ExtensionStore, GrammarManifestEntry, SchemaVersion, RELOAD_DEBOUNCE_DURATION, }; -use anyhow::Result; use async_compression::futures::bufread::GzipEncoder; use collections::BTreeMap; -use extension::Extension; +use extension::ExtensionHostProxy; use fs::{FakeFs, Fs, RealFs}; use futures::{io::BufReader, AsyncReadExt, StreamExt}; -use gpui::{BackgroundExecutor, Context, SemanticVersion, SharedString, Task, TestAppContext}; +use gpui::{Context, SemanticVersion, TestAppContext}; use http_client::{FakeHttpClient, Response}; -use language::{ - LanguageMatcher, LanguageName, LanguageRegistry, LanguageServerBinaryStatus, LoadedLanguage, -}; +use language::{LanguageMatcher, LanguageRegistry, LanguageServerBinaryStatus}; use lsp::LanguageServerName; use node_runtime::NodeRuntime; use parking_lot::Mutex; @@ -31,91 +27,6 @@ use std::{ use theme::ThemeRegistry; use util::test::temp_tree; -use crate::ExtensionRegistrationHooks; - -struct TestExtensionRegistrationHooks { - executor: BackgroundExecutor, - language_registry: Arc, - theme_registry: Arc, -} - -impl ExtensionRegistrationHooks for TestExtensionRegistrationHooks { - fn list_theme_names(&self, path: PathBuf, fs: Arc) -> Task>> { - self.executor.spawn(async move { - let themes = theme::read_user_theme(&path, fs).await?; - Ok(themes.themes.into_iter().map(|theme| theme.name).collect()) - }) - } - - fn load_user_theme(&self, theme_path: PathBuf, fs: Arc) -> Task> { - let theme_registry = self.theme_registry.clone(); - self.executor - .spawn(async move { theme_registry.load_user_theme(&theme_path, fs).await }) - } - - fn remove_user_themes(&self, themes: Vec) { - self.theme_registry.remove_user_themes(&themes); - } - - fn register_language( - &self, - language: language::LanguageName, - grammar: Option>, - matcher: language::LanguageMatcher, - load: Arc Result + 'static + Send + Sync>, - ) { - self.language_registry - .register_language(language, grammar, matcher, load) - } - - fn remove_languages( - &self, - languages_to_remove: &[language::LanguageName], - grammars_to_remove: &[Arc], - ) { - self.language_registry - .remove_languages(&languages_to_remove, &grammars_to_remove); - } - - fn register_wasm_grammars(&self, grammars: Vec<(Arc, PathBuf)>) { - self.language_registry.register_wasm_grammars(grammars) - } - - fn register_lsp_adapter( - &self, - extension: Arc, - language_server_id: LanguageServerName, - language: LanguageName, - ) { - self.language_registry.register_lsp_adapter( - language.clone(), - Arc::new(ExtensionLspAdapter::new( - extension, - language_server_id, - language, - )), - ); - } - - fn update_lsp_status( - &self, - server_name: lsp::LanguageServerName, - status: LanguageServerBinaryStatus, - ) { - self.language_registry - .update_lsp_status(server_name, status); - } - - fn remove_lsp_adapter( - &self, - language_name: &language::LanguageName, - server_name: &lsp::LanguageServerName, - ) { - self.language_registry - .remove_lsp_adapter(language_name, server_name); - } -} - #[cfg(test)] #[ctor::ctor] fn init_logger() { @@ -347,20 +258,18 @@ async fn test_extension_store(cx: &mut TestAppContext) { .collect(), }; - let language_registry = Arc::new(LanguageRegistry::test(cx.executor())); + let proxy = Arc::new(ExtensionHostProxy::new()); let theme_registry = Arc::new(ThemeRegistry::new(Box::new(()))); - let registration_hooks = Arc::new(TestExtensionRegistrationHooks { - executor: cx.executor(), - language_registry: language_registry.clone(), - theme_registry: theme_registry.clone(), - }); + theme_extension::init(proxy.clone(), theme_registry.clone(), cx.executor()); + let language_registry = Arc::new(LanguageRegistry::test(cx.executor())); + language_extension::init(proxy.clone(), language_registry.clone()); let node_runtime = NodeRuntime::unavailable(); let store = cx.new_model(|cx| { ExtensionStore::new( PathBuf::from("/the-extension-dir"), None, - registration_hooks.clone(), + proxy.clone(), fs.clone(), http_client.clone(), http_client.clone(), @@ -485,7 +394,7 @@ async fn test_extension_store(cx: &mut TestAppContext) { ExtensionStore::new( PathBuf::from("/the-extension-dir"), None, - registration_hooks, + proxy, fs.clone(), http_client.clone(), http_client.clone(), @@ -568,13 +477,11 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) { let project = Project::test(fs.clone(), [project_dir.as_path()], cx).await; - let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); + let proxy = Arc::new(ExtensionHostProxy::new()); let theme_registry = Arc::new(ThemeRegistry::new(Box::new(()))); - let registration_hooks = Arc::new(TestExtensionRegistrationHooks { - executor: cx.executor(), - language_registry: language_registry.clone(), - theme_registry: theme_registry.clone(), - }); + theme_extension::init(proxy.clone(), theme_registry.clone(), cx.executor()); + let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); + language_extension::init(proxy.clone(), language_registry.clone()); let node_runtime = NodeRuntime::unavailable(); let mut status_updates = language_registry.language_server_binary_statuses(); @@ -668,7 +575,7 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) { ExtensionStore::new( extensions_dir.clone(), Some(cache_dir), - registration_hooks, + proxy, fs.clone(), extension_client.clone(), builder_client, diff --git a/crates/extension_host/src/headless_host.rs b/crates/extension_host/src/headless_host.rs index 6ad8b71aa3..19a574b9d4 100644 --- a/crates/extension_host/src/headless_host.rs +++ b/crates/extension_host/src/headless_host.rs @@ -3,29 +3,18 @@ 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 extension::{ + Extension, ExtensionHostProxy, ExtensionLanguageProxy, ExtensionLanguageServerProxy, + 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 language::{LanguageConfig, LanguageName, LanguageQueries, 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)>>, -} +use crate::wasm_host::{WasmExtension, WasmHost}; #[derive(Clone, Debug)] pub struct ExtensionVersion { @@ -34,28 +23,37 @@ pub struct ExtensionVersion { pub dev: bool, } +pub struct HeadlessExtensionStore { + pub fs: Arc, + pub extension_dir: PathBuf, + pub proxy: Arc, + pub wasm_host: Arc, + pub loaded_extensions: HashMap, Arc>, + pub loaded_languages: HashMap, Vec>, + pub loaded_language_servers: HashMap, Vec<(LanguageServerName, LanguageName)>>, +} + impl HeadlessExtensionStore { pub fn new( fs: Arc, http_client: Arc, - languages: Arc, extension_dir: PathBuf, + extension_host_proxy: Arc, 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_host_proxy.clone(), extension_dir.join("work"), cx, ), extension_dir, + proxy: extension_host_proxy, loaded_extensions: Default::default(), loaded_languages: Default::default(), loaded_language_servers: Default::default(), @@ -154,7 +152,7 @@ impl HeadlessExtensionStore { config.grammar = None; - this.registration_hooks.register_language( + this.proxy.register_language( config.name.clone(), None, config.matcher.clone(), @@ -184,7 +182,7 @@ impl HeadlessExtensionStore { .entry(manifest.id.clone()) .or_default() .push((language_server_id.clone(), language.clone())); - this.registration_hooks.register_lsp_adapter( + this.proxy.register_language_server( wasm_extension.clone(), language_server_id.clone(), language.clone(), @@ -202,19 +200,20 @@ impl HeadlessExtensionStore { 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, &[]); + self.proxy.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); + self.proxy + .remove_language_server(&language, &language_server_name); } let path = self.extension_dir.join(&extension_id.to_string()); @@ -318,71 +317,3 @@ impl HeadlessExtensionStore { 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, - extension: Arc, - language_server_id: LanguageServerName, - language: LanguageName, - ) { - log::info!("registering lsp adapter {:?}", language); - 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)>) { - 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/extension_host/src/wasm_host.rs b/crates/extension_host/src/wasm_host.rs index 01c57599a8..766ca8c0bb 100644 --- a/crates/extension_host/src/wasm_host.rs +++ b/crates/extension_host/src/wasm_host.rs @@ -1,11 +1,11 @@ pub mod wit; -use crate::{ExtensionManifest, ExtensionRegistrationHooks}; +use crate::ExtensionManifest; use anyhow::{anyhow, bail, Context as _, Result}; use async_trait::async_trait; use extension::{ - CodeLabel, Command, Completion, KeyValueStoreDelegate, ProjectDelegate, SlashCommand, - SlashCommandArgumentCompletion, SlashCommandOutput, Symbol, WorktreeDelegate, + CodeLabel, Command, Completion, ExtensionHostProxy, KeyValueStoreDelegate, ProjectDelegate, + SlashCommand, SlashCommandArgumentCompletion, SlashCommandOutput, Symbol, WorktreeDelegate, }; use fs::{normalize_path, Fs}; use futures::future::LocalBoxFuture; @@ -40,7 +40,7 @@ pub struct WasmHost { release_channel: ReleaseChannel, http_client: Arc, node_runtime: NodeRuntime, - pub registration_hooks: Arc, + pub(crate) proxy: Arc, fs: Arc, pub work_dir: PathBuf, _main_thread_message_task: Task<()>, @@ -330,7 +330,7 @@ impl WasmHost { fs: Arc, http_client: Arc, node_runtime: NodeRuntime, - registration_hooks: Arc, + proxy: Arc, work_dir: PathBuf, cx: &mut AppContext, ) -> Arc { @@ -346,7 +346,7 @@ impl WasmHost { work_dir, http_client, node_runtime, - registration_hooks, + proxy, release_channel: ReleaseChannel::global(cx), _main_thread_message_task: task, main_thread_message_tx: tx, diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_0_1.rs b/crates/extension_host/src/wasm_host/wit/since_v0_0_1.rs index bd1770de38..1f0891b410 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_0_1.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_0_1.rs @@ -3,7 +3,7 @@ use crate::wasm_host::wit::since_v0_0_4; use crate::wasm_host::WasmState; use anyhow::Result; use async_trait::async_trait; -use extension::WorktreeDelegate; +use extension::{ExtensionLanguageServerProxy, WorktreeDelegate}; use language::LanguageServerBinaryStatus; use semantic_version::SemanticVersion; use std::sync::{Arc, OnceLock}; @@ -149,8 +149,9 @@ impl ExtensionImports for WasmState { }; self.host - .registration_hooks - .update_lsp_status(lsp::LanguageServerName(server_name.into()), status); + .proxy + .update_language_server_status(lsp::LanguageServerName(server_name.into()), status); + Ok(()) } diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs index 18f4bc0234..c1c07a2b09 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs @@ -5,7 +5,7 @@ use anyhow::{anyhow, bail, Context, Result}; use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; use async_trait::async_trait; -use extension::{KeyValueStoreDelegate, WorktreeDelegate}; +use extension::{ExtensionLanguageServerProxy, KeyValueStoreDelegate, WorktreeDelegate}; use futures::{io::BufReader, FutureExt as _}; use futures::{lock::Mutex, AsyncReadExt}; use language::LanguageName; @@ -495,8 +495,9 @@ impl ExtensionImports for WasmState { }; self.host - .registration_hooks - .update_lsp_status(::lsp::LanguageServerName(server_name.into()), status); + .proxy + .update_language_server_status(::lsp::LanguageServerName(server_name.into()), status); + Ok(()) } diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs index 234eec26ec..f7e11e1032 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs @@ -8,7 +8,9 @@ use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; use async_trait::async_trait; use context_servers::manager::ContextServerSettings; -use extension::{KeyValueStoreDelegate, ProjectDelegate, WorktreeDelegate}; +use extension::{ + ExtensionLanguageServerProxy, KeyValueStoreDelegate, ProjectDelegate, WorktreeDelegate, +}; use futures::{io::BufReader, FutureExt as _}; use futures::{lock::Mutex, AsyncReadExt}; use language::{language_settings::AllLanguageSettings, LanguageName, LanguageServerBinaryStatus}; @@ -682,8 +684,9 @@ impl ExtensionImports for WasmState { }; self.host - .registration_hooks - .update_lsp_status(::lsp::LanguageServerName(server_name.into()), status); + .proxy + .update_language_server_status(::lsp::LanguageServerName(server_name.into()), status); + Ok(()) } diff --git a/crates/extensions_ui/Cargo.toml b/crates/extensions_ui/Cargo.toml index a219fe4bd4..cc6e78d6f3 100644 --- a/crates/extensions_ui/Cargo.toml +++ b/crates/extensions_ui/Cargo.toml @@ -13,21 +13,15 @@ path = "src/extensions_ui.rs" [dependencies] anyhow.workspace = true -assistant_slash_command.workspace = true client.workspace = true collections.workspace = true -context_servers.workspace = true db.workspace = true editor.workspace = true -extension.workspace = true extension_host.workspace = true fs.workspace = true fuzzy.workspace = true gpui.workspace = true -indexed_docs.workspace = true language.workspace = true -log.workspace = true -lsp.workspace = true num-format.workspace = true picker.workspace = true project.workspace = true @@ -36,7 +30,6 @@ semantic_version.workspace = true serde.workspace = true settings.workspace = true smallvec.workspace = true -snippet_provider.workspace = true theme.workspace = true ui.workspace = true util.workspace = true diff --git a/crates/extensions_ui/src/extension_registration_hooks.rs b/crates/extensions_ui/src/extension_registration_hooks.rs deleted file mode 100644 index 1b427cd187..0000000000 --- a/crates/extensions_ui/src/extension_registration_hooks.rs +++ /dev/null @@ -1,209 +0,0 @@ -use std::{path::PathBuf, sync::Arc}; - -use anyhow::Result; -use assistant_slash_command::{ExtensionSlashCommand, SlashCommandRegistry}; -use context_servers::manager::ServerCommand; -use context_servers::ContextServerFactoryRegistry; -use extension::{Extension, ProjectDelegate}; -use extension_host::extension_lsp_adapter::ExtensionLspAdapter; -use fs::Fs; -use gpui::{AppContext, BackgroundExecutor, Model, Task}; -use indexed_docs::{ExtensionIndexedDocsProvider, IndexedDocsRegistry, ProviderId}; -use language::{LanguageName, LanguageRegistry, LanguageServerBinaryStatus, LoadedLanguage}; -use lsp::LanguageServerName; -use snippet_provider::SnippetRegistry; -use theme::{ThemeRegistry, ThemeSettings}; -use ui::SharedString; - -struct ExtensionProject { - worktree_ids: Vec, -} - -impl ProjectDelegate for ExtensionProject { - fn worktree_ids(&self) -> Vec { - self.worktree_ids.clone() - } -} - -pub struct ConcreteExtensionRegistrationHooks { - slash_command_registry: Arc, - theme_registry: Arc, - indexed_docs_registry: Arc, - snippet_registry: Arc, - language_registry: Arc, - context_server_factory_registry: Model, - executor: BackgroundExecutor, -} - -impl ConcreteExtensionRegistrationHooks { - pub fn new( - theme_registry: Arc, - slash_command_registry: Arc, - indexed_docs_registry: Arc, - snippet_registry: Arc, - language_registry: Arc, - context_server_factory_registry: Model, - cx: &AppContext, - ) -> Arc { - Arc::new(Self { - theme_registry, - slash_command_registry, - indexed_docs_registry, - snippet_registry, - language_registry, - context_server_factory_registry, - executor: cx.background_executor().clone(), - }) - } -} - -impl extension_host::ExtensionRegistrationHooks for ConcreteExtensionRegistrationHooks { - fn remove_user_themes(&self, themes: Vec) { - self.theme_registry.remove_user_themes(&themes); - } - - fn load_user_theme(&self, theme_path: PathBuf, fs: Arc) -> Task> { - let theme_registry = self.theme_registry.clone(); - self.executor - .spawn(async move { theme_registry.load_user_theme(&theme_path, fs).await }) - } - - fn register_slash_command( - &self, - extension: Arc, - command: extension::SlashCommand, - ) { - self.slash_command_registry - .register_command(ExtensionSlashCommand::new(extension, command), false) - } - - fn register_context_server( - &self, - extension: Arc, - id: Arc, - cx: &mut AppContext, - ) { - self.context_server_factory_registry - .update(cx, |registry, _| { - registry.register_server_factory( - id.clone(), - Arc::new({ - move |project, cx| { - log::info!( - "loading command for context server {id} from extension {}", - extension.manifest().id - ); - - let id = id.clone(); - let extension = extension.clone(); - cx.spawn(|mut cx| async move { - let extension_project = - project.update(&mut cx, |project, cx| { - Arc::new(ExtensionProject { - worktree_ids: project - .visible_worktrees(cx) - .map(|worktree| worktree.read(cx).id().to_proto()) - .collect(), - }) - })?; - - let command = extension - .context_server_command(id.clone(), extension_project) - .await?; - - log::info!("loaded command for context server {id}: {command:?}"); - - Ok(ServerCommand { - path: command.command, - args: command.args, - env: Some(command.env.into_iter().collect()), - }) - }) - } - }), - ) - }); - } - - fn register_docs_provider(&self, extension: Arc, provider_id: Arc) { - self.indexed_docs_registry - .register_provider(Box::new(ExtensionIndexedDocsProvider::new( - extension, - ProviderId(provider_id), - ))); - } - - fn register_snippets(&self, path: &PathBuf, snippet_contents: &str) -> Result<()> { - self.snippet_registry - .register_snippets(path, snippet_contents) - } - - fn update_lsp_status( - &self, - server_name: lsp::LanguageServerName, - status: LanguageServerBinaryStatus, - ) { - self.language_registry - .update_lsp_status(server_name, status); - } - - fn register_lsp_adapter( - &self, - extension: Arc, - language_server_id: LanguageServerName, - language: LanguageName, - ) { - self.language_registry.register_lsp_adapter( - language.clone(), - Arc::new(ExtensionLspAdapter::new( - extension, - language_server_id, - language, - )), - ); - } - - fn remove_lsp_adapter( - &self, - language_name: &language::LanguageName, - server_name: &lsp::LanguageServerName, - ) { - self.language_registry - .remove_lsp_adapter(language_name, server_name); - } - - fn remove_languages( - &self, - languages_to_remove: &[language::LanguageName], - grammars_to_remove: &[Arc], - ) { - self.language_registry - .remove_languages(&languages_to_remove, &grammars_to_remove); - } - - fn register_wasm_grammars(&self, grammars: Vec<(Arc, PathBuf)>) { - self.language_registry.register_wasm_grammars(grammars) - } - - fn register_language( - &self, - language: language::LanguageName, - grammar: Option>, - matcher: language::LanguageMatcher, - load: Arc Result + 'static + Send + Sync>, - ) { - self.language_registry - .register_language(language, grammar, matcher, load) - } - - fn reload_current_theme(&self, cx: &mut AppContext) { - ThemeSettings::reload_current_theme(cx) - } - - fn list_theme_names(&self, path: PathBuf, fs: Arc) -> Task>> { - self.executor.spawn(async move { - let themes = theme::read_user_theme(&path, fs).await?; - Ok(themes.themes.into_iter().map(|theme| theme.name).collect()) - }) - } -} diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs index 077d80b9b8..eaffdafa41 100644 --- a/crates/extensions_ui/src/extensions_ui.rs +++ b/crates/extensions_ui/src/extensions_ui.rs @@ -1,10 +1,7 @@ mod components; -mod extension_registration_hooks; mod extension_suggest; mod extension_version_selector; -pub use extension_registration_hooks::ConcreteExtensionRegistrationHooks; - use std::ops::DerefMut; use std::sync::OnceLock; use std::time::Duration; diff --git a/crates/indexed_docs/src/extension_indexed_docs_provider.rs b/crates/indexed_docs/src/extension_indexed_docs_provider.rs index ed006546fe..25b0f16357 100644 --- a/crates/indexed_docs/src/extension_indexed_docs_provider.rs +++ b/crates/indexed_docs/src/extension_indexed_docs_provider.rs @@ -3,9 +3,33 @@ use std::sync::Arc; use anyhow::Result; use async_trait::async_trait; -use extension::Extension; +use extension::{Extension, ExtensionHostProxy, ExtensionIndexedDocsProviderProxy}; +use gpui::AppContext; -use crate::{IndexedDocsDatabase, IndexedDocsProvider, PackageName, ProviderId}; +use crate::{ + IndexedDocsDatabase, IndexedDocsProvider, IndexedDocsRegistry, PackageName, ProviderId, +}; + +pub fn init(cx: &mut AppContext) { + let proxy = ExtensionHostProxy::default_global(cx); + proxy.register_indexed_docs_provider_proxy(IndexedDocsRegistryProxy { + indexed_docs_registry: IndexedDocsRegistry::global(cx), + }); +} + +struct IndexedDocsRegistryProxy { + indexed_docs_registry: Arc, +} + +impl ExtensionIndexedDocsProviderProxy for IndexedDocsRegistryProxy { + fn register_indexed_docs_provider(&self, extension: Arc, provider_id: Arc) { + self.indexed_docs_registry + .register_provider(Box::new(ExtensionIndexedDocsProvider::new( + extension, + ProviderId(provider_id), + ))); + } +} pub struct ExtensionIndexedDocsProvider { extension: Arc, diff --git a/crates/indexed_docs/src/indexed_docs.rs b/crates/indexed_docs/src/indexed_docs.rs index 95e5c62335..42672cd220 100644 --- a/crates/indexed_docs/src/indexed_docs.rs +++ b/crates/indexed_docs/src/indexed_docs.rs @@ -3,7 +3,14 @@ mod providers; mod registry; mod store; +use gpui::AppContext; + pub use crate::extension_indexed_docs_provider::ExtensionIndexedDocsProvider; pub use crate::providers::rustdoc::*; pub use crate::registry::*; pub use crate::store::*; + +pub fn init(cx: &mut AppContext) { + IndexedDocsRegistry::init_global(cx); + extension_indexed_docs_provider::init(cx); +} diff --git a/crates/indexed_docs/src/registry.rs b/crates/indexed_docs/src/registry.rs index fa3425466c..6332e6c4b0 100644 --- a/crates/indexed_docs/src/registry.rs +++ b/crates/indexed_docs/src/registry.rs @@ -20,7 +20,7 @@ impl IndexedDocsRegistry { GlobalIndexedDocsRegistry::global(cx).0.clone() } - pub fn init_global(cx: &mut AppContext) { + pub(crate) fn init_global(cx: &mut AppContext) { GlobalIndexedDocsRegistry::set_global( cx, GlobalIndexedDocsRegistry(Arc::new(Self::new(cx.background_executor().clone()))), diff --git a/crates/language_extension/Cargo.toml b/crates/language_extension/Cargo.toml new file mode 100644 index 0000000000..3d1e4d0a64 --- /dev/null +++ b/crates/language_extension/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "language_extension" +version = "0.1.0" +edition = "2021" +publish = false +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/language_extension.rs" + +[dependencies] +anyhow.workspace = true +async-trait.workspace = true +collections.workspace = true +extension.workspace = true +futures.workspace = true +gpui.workspace = true +language.workspace = true +lsp.workspace = true +serde.workspace = true +serde_json.workspace = true +util.workspace = true diff --git a/crates/language_extension/LICENSE-GPL b/crates/language_extension/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/language_extension/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/extension_host/src/extension_lsp_adapter.rs b/crates/language_extension/src/extension_lsp_adapter.rs similarity index 93% rename from crates/extension_host/src/extension_lsp_adapter.rs rename to crates/language_extension/src/extension_lsp_adapter.rs index 069eddba57..eab9529fe0 100644 --- a/crates/extension_host/src/extension_lsp_adapter.rs +++ b/crates/language_extension/src/extension_lsp_adapter.rs @@ -1,22 +1,28 @@ +use std::any::Any; +use std::ops::Range; +use std::path::PathBuf; +use std::pin::Pin; +use std::sync::Arc; + use anyhow::{Context, Result}; use async_trait::async_trait; use collections::HashMap; -use extension::{Extension, WorktreeDelegate}; +use extension::{Extension, ExtensionLanguageServerProxy, WorktreeDelegate}; use futures::{Future, FutureExt}; use gpui::AsyncAppContext; use language::{ - CodeLabel, HighlightId, Language, LanguageName, LanguageToolchainStore, LspAdapter, - LspAdapterDelegate, + CodeLabel, HighlightId, Language, LanguageName, LanguageServerBinaryStatus, + LanguageToolchainStore, LspAdapter, LspAdapterDelegate, }; use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerName}; use serde::Serialize; use serde_json::Value; -use std::ops::Range; -use std::{any::Any, path::PathBuf, pin::Pin, sync::Arc}; use util::{maybe, ResultExt}; +use crate::LanguageServerRegistryProxy; + /// An adapter that allows an [`LspAdapterDelegate`] to be used as a [`WorktreeDelegate`]. -pub struct WorktreeDelegateAdapter(pub Arc); +struct WorktreeDelegateAdapter(pub Arc); #[async_trait] impl WorktreeDelegate for WorktreeDelegateAdapter { @@ -44,14 +50,50 @@ impl WorktreeDelegate for WorktreeDelegateAdapter { } } -pub struct ExtensionLspAdapter { +impl ExtensionLanguageServerProxy for LanguageServerRegistryProxy { + fn register_language_server( + &self, + extension: Arc, + language_server_id: LanguageServerName, + language: LanguageName, + ) { + self.language_registry.register_lsp_adapter( + language.clone(), + Arc::new(ExtensionLspAdapter::new( + extension, + language_server_id, + language, + )), + ); + } + + fn remove_language_server( + &self, + language: &LanguageName, + language_server_id: &LanguageServerName, + ) { + self.language_registry + .remove_lsp_adapter(language, language_server_id); + } + + fn update_language_server_status( + &self, + language_server_id: LanguageServerName, + status: LanguageServerBinaryStatus, + ) { + self.language_registry + .update_lsp_status(language_server_id, status); + } +} + +struct ExtensionLspAdapter { extension: Arc, language_server_id: LanguageServerName, language_name: LanguageName, } impl ExtensionLspAdapter { - pub fn new( + fn new( extension: Arc, language_server_id: LanguageServerName, language_name: LanguageName, diff --git a/crates/language_extension/src/language_extension.rs b/crates/language_extension/src/language_extension.rs new file mode 100644 index 0000000000..d8ffc71d7c --- /dev/null +++ b/crates/language_extension/src/language_extension.rs @@ -0,0 +1,51 @@ +mod extension_lsp_adapter; + +use std::path::PathBuf; +use std::sync::Arc; + +use anyhow::Result; +use extension::{ExtensionGrammarProxy, ExtensionHostProxy, ExtensionLanguageProxy}; +use language::{LanguageMatcher, LanguageName, LanguageRegistry, LoadedLanguage}; + +pub fn init( + extension_host_proxy: Arc, + language_registry: Arc, +) { + let language_server_registry_proxy = LanguageServerRegistryProxy { language_registry }; + extension_host_proxy.register_grammar_proxy(language_server_registry_proxy.clone()); + extension_host_proxy.register_language_proxy(language_server_registry_proxy.clone()); + extension_host_proxy.register_language_server_proxy(language_server_registry_proxy); +} + +#[derive(Clone)] +struct LanguageServerRegistryProxy { + language_registry: Arc, +} + +impl ExtensionGrammarProxy for LanguageServerRegistryProxy { + fn register_grammars(&self, grammars: Vec<(Arc, PathBuf)>) { + self.language_registry.register_wasm_grammars(grammars) + } +} + +impl ExtensionLanguageProxy for LanguageServerRegistryProxy { + fn register_language( + &self, + language: LanguageName, + grammar: Option>, + matcher: LanguageMatcher, + load: Arc Result + Send + Sync + 'static>, + ) { + self.language_registry + .register_language(language, grammar, matcher, load); + } + + fn remove_languages( + &self, + languages_to_remove: &[LanguageName], + grammars_to_remove: &[Arc], + ) { + self.language_registry + .remove_languages(&languages_to_remove, &grammars_to_remove); + } +} diff --git a/crates/remote_server/Cargo.toml b/crates/remote_server/Cargo.toml index d46fb8df56..82853217dc 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.workspace = true extension_host.workspace = true fs.workspace = true futures.workspace = true @@ -37,6 +38,7 @@ git_hosting_providers.workspace = true gpui.workspace = true http_client.workspace = true language.workspace = true +language_extension.workspace = true languages.workspace = true log.workspace = true lsp.workspace = true diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 28cd6e115c..2fb8330603 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::ExtensionHostProxy; use extension_host::headless_host::HeadlessExtensionStore; use fs::Fs; use gpui::{AppContext, AsyncAppContext, Context as _, Model, ModelContext, PromptLevel}; @@ -47,6 +48,7 @@ pub struct HeadlessAppState { pub http_client: Arc, pub node_runtime: NodeRuntime, pub languages: Arc, + pub extension_host_proxy: Arc, } impl HeadlessProject { @@ -63,9 +65,11 @@ impl HeadlessProject { http_client, node_runtime, languages, + extension_host_proxy: proxy, }: HeadlessAppState, cx: &mut ModelContext, ) -> Self { + language_extension::init(proxy.clone(), languages.clone()); languages::init(languages.clone(), node_runtime.clone(), cx); let worktree_store = cx.new_model(|cx| { @@ -152,8 +156,8 @@ impl HeadlessProject { let extensions = HeadlessExtensionStore::new( fs.clone(), http_client.clone(), - languages.clone(), paths::remote_extensions_dir().to_path_buf(), + proxy, node_runtime, cx, ); diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index 3a9803287a..bdb862c5af 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -1,6 +1,7 @@ use crate::headless_project::HeadlessProject; use client::{Client, UserStore}; use clock::FakeSystemClock; +use extension::ExtensionHostProxy; use fs::{FakeFs, Fs}; use gpui::{Context, Model, SemanticVersion, TestAppContext}; use http_client::{BlockedHttpClient, FakeHttpClient}; @@ -1234,6 +1235,7 @@ pub async fn init_test( let http_client = Arc::new(BlockedHttpClient); let node_runtime = NodeRuntime::unavailable(); let languages = Arc::new(LanguageRegistry::new(cx.executor())); + let proxy = Arc::new(ExtensionHostProxy::new()); server_cx.update(HeadlessProject::init); let headless = server_cx.new_model(|cx| { client::init_settings(cx); @@ -1245,6 +1247,7 @@ pub async fn init_test( http_client, node_runtime, languages, + extension_host_proxy: proxy, }, cx, ) diff --git a/crates/remote_server/src/unix.rs b/crates/remote_server/src/unix.rs index 467fd452f8..18378ec8e9 100644 --- a/crates/remote_server/src/unix.rs +++ b/crates/remote_server/src/unix.rs @@ -3,6 +3,7 @@ use crate::HeadlessProject; use anyhow::{anyhow, Context, Result}; use chrono::Utc; use client::{telemetry, ProxySettings}; +use extension::ExtensionHostProxy; use fs::{Fs, RealFs}; use futures::channel::mpsc; use futures::{select, select_biased, AsyncRead, AsyncWrite, AsyncWriteExt, FutureExt, SinkExt}; @@ -434,6 +435,9 @@ pub fn execute_run( GitHostingProviderRegistry::set_global(git_hosting_provider_registry, cx); git_hosting_providers::init(cx); + extension::init(cx); + let extension_host_proxy = ExtensionHostProxy::global(cx); + let project = cx.new_model(|cx| { let fs = Arc::new(RealFs::new(Default::default(), None)); let node_settings_rx = initialize_settings(session.clone(), fs.clone(), cx); @@ -466,6 +470,7 @@ pub fn execute_run( http_client, node_runtime, languages, + extension_host_proxy, }, cx, ) diff --git a/crates/snippet_provider/Cargo.toml b/crates/snippet_provider/Cargo.toml index 95ab19ebb6..aa4e1a5f84 100644 --- a/crates/snippet_provider/Cargo.toml +++ b/crates/snippet_provider/Cargo.toml @@ -11,6 +11,7 @@ workspace = true [dependencies] anyhow.workspace = true collections.workspace = true +extension.workspace = true fs.workspace = true futures.workspace = true gpui.workspace = true diff --git a/crates/snippet_provider/src/extension_snippet.rs b/crates/snippet_provider/src/extension_snippet.rs new file mode 100644 index 0000000000..41a7c886e1 --- /dev/null +++ b/crates/snippet_provider/src/extension_snippet.rs @@ -0,0 +1,26 @@ +use std::path::PathBuf; +use std::sync::Arc; + +use anyhow::Result; +use extension::{ExtensionHostProxy, ExtensionSnippetProxy}; +use gpui::AppContext; + +use crate::SnippetRegistry; + +pub fn init(cx: &mut AppContext) { + let proxy = ExtensionHostProxy::default_global(cx); + proxy.register_snippet_proxy(SnippetRegistryProxy { + snippet_registry: SnippetRegistry::global(cx), + }); +} + +struct SnippetRegistryProxy { + snippet_registry: Arc, +} + +impl ExtensionSnippetProxy for SnippetRegistryProxy { + fn register_snippet(&self, path: &PathBuf, snippet_contents: &str) -> Result<()> { + self.snippet_registry + .register_snippets(path, snippet_contents) + } +} diff --git a/crates/snippet_provider/src/lib.rs b/crates/snippet_provider/src/lib.rs index 17d60d25a0..34aa1ebefc 100644 --- a/crates/snippet_provider/src/lib.rs +++ b/crates/snippet_provider/src/lib.rs @@ -1,3 +1,4 @@ +mod extension_snippet; mod format; mod registry; @@ -18,6 +19,7 @@ use util::ResultExt; pub fn init(cx: &mut AppContext) { SnippetRegistry::init_global(cx); + extension_snippet::init(cx); } // Is `None` if the snippet file is global. diff --git a/crates/theme_extension/Cargo.toml b/crates/theme_extension/Cargo.toml new file mode 100644 index 0000000000..1e12f037b9 --- /dev/null +++ b/crates/theme_extension/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "theme_extension" +version = "0.1.0" +edition = "2021" +publish = false +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/theme_extension.rs" + +[dependencies] +anyhow.workspace = true +extension.workspace = true +fs.workspace = true +gpui.workspace = true +theme.workspace = true diff --git a/crates/theme_extension/LICENSE-GPL b/crates/theme_extension/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/theme_extension/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/theme_extension/src/theme_extension.rs b/crates/theme_extension/src/theme_extension.rs new file mode 100644 index 0000000000..0266db324b --- /dev/null +++ b/crates/theme_extension/src/theme_extension.rs @@ -0,0 +1,47 @@ +use std::path::PathBuf; +use std::sync::Arc; + +use anyhow::Result; +use extension::{ExtensionHostProxy, ExtensionThemeProxy}; +use fs::Fs; +use gpui::{AppContext, BackgroundExecutor, SharedString, Task}; +use theme::{ThemeRegistry, ThemeSettings}; + +pub fn init( + extension_host_proxy: Arc, + theme_registry: Arc, + executor: BackgroundExecutor, +) { + extension_host_proxy.register_theme_proxy(ThemeRegistryProxy { + theme_registry, + executor, + }); +} + +struct ThemeRegistryProxy { + theme_registry: Arc, + executor: BackgroundExecutor, +} + +impl ExtensionThemeProxy for ThemeRegistryProxy { + fn list_theme_names(&self, theme_path: PathBuf, fs: Arc) -> Task>> { + self.executor.spawn(async move { + let themes = theme::read_user_theme(&theme_path, fs).await?; + Ok(themes.themes.into_iter().map(|theme| theme.name).collect()) + }) + } + + fn remove_user_themes(&self, themes: Vec) { + self.theme_registry.remove_user_themes(&themes); + } + + fn load_user_theme(&self, theme_path: PathBuf, fs: Arc) -> Task> { + let theme_registry = self.theme_registry.clone(); + self.executor + .spawn(async move { theme_registry.load_user_theme(&theme_path, fs).await }) + } + + fn reload_current_theme(&self, cx: &mut AppContext) { + ThemeSettings::reload_current_theme(cx) + } +} diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 52ec265480..755076e360 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -19,7 +19,6 @@ 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 @@ -36,12 +35,12 @@ collab_ui.workspace = true collections.workspace = true command_palette.workspace = true command_palette_hooks.workspace = true -context_servers.workspace = true copilot.workspace = true db.workspace = true diagnostics.workspace = true editor.workspace = true env_logger.workspace = true +extension.workspace = true extension_host.workspace = true extensions_ui.workspace = true feature_flags.workspace = true @@ -56,11 +55,11 @@ go_to_line.workspace = true gpui = { workspace = true, features = ["wayland", "x11", "font-kit"] } http_client.workspace = true image_viewer.workspace = true -indexed_docs.workspace = true inline_completion_button.workspace = true install_cli.workspace = true journal.workspace = true language.workspace = true +language_extension.workspace = true language_model.workspace = true language_models.workspace = true language_selector.workspace = true @@ -109,6 +108,7 @@ tasks_ui.workspace = true telemetry_events.workspace = true terminal_view.workspace = true theme.workspace = true +theme_extension.workspace = true theme_selector.workspace = true time.workspace = true toolchain_selector.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index e96a70f91d..73b0e0f199 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -5,16 +5,15 @@ mod reliability; mod zed; use anyhow::{anyhow, Context as _, Result}; -use assistant_slash_command::SlashCommandRegistry; use chrono::Offset; use clap::{command, Parser}; use cli::FORCE_CLI_MODE_ENV_VAR_NAME; use client::{parse_zed_link, Client, ProxySettings, UserStore}; use collab_ui::channel_view::ChannelView; -use context_servers::ContextServerFactoryRegistry; use db::kvp::{GLOBAL_KEY_VALUE_STORE, KEY_VALUE_STORE}; use editor::Editor; use env_logger::Builder; +use extension::ExtensionHostProxy; use fs::{Fs, RealFs}; use futures::{future, StreamExt}; use git::GitHostingProviderRegistry; @@ -23,7 +22,6 @@ use gpui::{ VisualContext, }; use http_client::{read_proxy_from_env, Uri}; -use indexed_docs::IndexedDocsRegistry; use language::LanguageRegistry; use log::LevelFilter; use reqwest_client::ReqwestClient; @@ -40,7 +38,6 @@ use settings::{ }; use simplelog::ConfigBuilder; use smol::process::Command; -use snippet_provider::SnippetRegistry; use std::{ env, fs::OpenOptions, @@ -284,6 +281,9 @@ fn main() { OpenListener::set_global(cx, open_listener.clone()); + extension::init(cx); + let extension_host_proxy = ExtensionHostProxy::global(cx); + let client = Client::production(cx); cx.set_http_client(client.http_client().clone()); let mut languages = LanguageRegistry::new(cx.background_executor().clone()); @@ -317,6 +317,7 @@ fn main() { let node_runtime = NodeRuntime::new(client.http_client(), rx); language::init(cx); + language_extension::init(extension_host_proxy.clone(), languages.clone()); languages::init(languages.clone(), node_runtime.clone(), cx); let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx)); let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx)); @@ -326,7 +327,6 @@ fn main() { zed::init(cx); project::Project::init(&client, cx); client::init(&client, cx); - language::init(cx); let telemetry = client.telemetry(); telemetry.start( system_id.as_ref().map(|id| id.to_string()), @@ -376,6 +376,11 @@ fn main() { SystemAppearance::init(cx); theme::init(theme::LoadThemes::All(Box::new(Assets)), cx); + theme_extension::init( + extension_host_proxy.clone(), + ThemeRegistry::global(cx), + cx.background_executor().clone(), + ); command_palette::init(cx); let copilot_language_server_id = app_state.languages.next_language_server_id(); copilot::init( @@ -407,17 +412,8 @@ fn main() { app_state.client.telemetry().clone(), cx, ); - let api = extensions_ui::ConcreteExtensionRegistrationHooks::new( - ThemeRegistry::global(cx), - SlashCommandRegistry::global(cx), - IndexedDocsRegistry::global(cx), - SnippetRegistry::global(cx), - app_state.languages.clone(), - ContextServerFactoryRegistry::global(cx), - cx, - ); extension_host::init( - api, + extension_host_proxy, app_state.fs.clone(), app_state.client.clone(), app_state.node_runtime.clone(), From 9833756224180f9e94704c9a07906e7850ff0351 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 23 Nov 2024 02:21:19 +0200 Subject: [PATCH 118/886] Fix file finder menu actions (#21087) Closes https://github.com/zed-industries/zed/issues/21077 * BREAKING: rename `file_finder::OpenMenu` into `file_finder::ToggleMenu` * Display the keybinding for menu toggling when the menu is open * Fix `enter` not working in the menu Release Notes: - Fixed enter not working and menu toggle binding not shown in the file finder menu --- assets/keymaps/default-linux.json | 7 +++++- assets/keymaps/default-macos.json | 7 +++++- crates/file_finder/src/file_finder.rs | 31 ++++++++++++++------------ crates/ui/src/components/keybinding.rs | 2 +- 4 files changed, 30 insertions(+), 17 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 313ee024b5..2eedc1c839 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -649,11 +649,16 @@ "tab": "channel_modal::ToggleMode" } }, + { + "context": "FileFinder", + "bindings": { + "ctrl": "file_finder::ToggleMenu" + } + }, { "context": "FileFinder && !menu_open", "bindings": { "ctrl-shift-p": "file_finder::SelectPrev", - "ctrl": "file_finder::OpenMenu", "ctrl-j": "pane::SplitDown", "ctrl-k": "pane::SplitUp", "ctrl-h": "pane::SplitLeft", diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 025ba4d69d..963d48ba5e 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -650,11 +650,16 @@ "tab": "channel_modal::ToggleMode" } }, + { + "context": "FileFinder", + "bindings": { + "cmd": "file_finder::ToggleMenu" + } + }, { "context": "FileFinder && !menu_open", "bindings": { "cmd-shift-p": "file_finder::SelectPrev", - "cmd": "file_finder::OpenMenu", "cmd-j": "pane::SplitDown", "cmd-k": "pane::SplitUp", "cmd-h": "pane::SplitLeft", diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 138a02d1f6..6a758211f8 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -42,7 +42,7 @@ use workspace::{ Workspace, }; -actions!(file_finder, [SelectPrev, OpenMenu]); +actions!(file_finder, [SelectPrev, ToggleMenu]); impl ModalView for FileFinder { fn on_before_dismiss(&mut self, cx: &mut ViewContext) -> workspace::DismissDecision { @@ -189,10 +189,12 @@ impl FileFinder { cx.dispatch_action(Box::new(menu::SelectPrev)); } - fn handle_open_menu(&mut self, _: &OpenMenu, cx: &mut ViewContext) { + fn handle_toggle_menu(&mut self, _: &ToggleMenu, cx: &mut ViewContext) { self.picker.update(cx, |picker, cx| { let menu_handle = &picker.delegate.popover_menu_handle; - if !menu_handle.is_deployed() { + if menu_handle.is_deployed() { + menu_handle.hide(cx); + } else { menu_handle.show(cx); } }); @@ -282,7 +284,7 @@ impl Render for FileFinder { .w(modal_max_width) .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed)) .on_action(cx.listener(Self::handle_select_prev)) - .on_action(cx.listener(Self::handle_open_menu)) + .on_action(cx.listener(Self::handle_toggle_menu)) .on_action(cx.listener(Self::go_to_file_split_left)) .on_action(cx.listener(Self::go_to_file_split_right)) .on_action(cx.listener(Self::go_to_file_split_up)) @@ -1242,6 +1244,7 @@ impl PickerDelegate for FileFinderDelegate { } fn render_footer(&self, cx: &mut ViewContext>) -> Option { + let context = self.focus_handle.clone(); Some( h_flex() .w_full() @@ -1263,19 +1266,19 @@ impl PickerDelegate for FileFinderDelegate { .trigger( Button::new("actions-trigger", "Split Options") .selected_label_color(Color::Accent) - .key_binding(KeyBinding::for_action_in( - &OpenMenu, - &self.focus_handle, - cx, - )), + .key_binding(KeyBinding::for_action_in(&ToggleMenu, &context, cx)), ) .menu({ move |cx| { - Some(ContextMenu::build(cx, move |menu, _| { - menu.action("Split Left", pane::SplitLeft.boxed_clone()) - .action("Split Right", pane::SplitRight.boxed_clone()) - .action("Split Up", pane::SplitUp.boxed_clone()) - .action("Split Down", pane::SplitDown.boxed_clone()) + Some(ContextMenu::build(cx, { + let context = context.clone(); + move |menu, _| { + menu.context(context) + .action("Split Left", pane::SplitLeft.boxed_clone()) + .action("Split Right", pane::SplitRight.boxed_clone()) + .action("Split Up", pane::SplitUp.boxed_clone()) + .action("Split Down", pane::SplitDown.boxed_clone()) + } })) } }), diff --git a/crates/ui/src/components/keybinding.rs b/crates/ui/src/components/keybinding.rs index 770e46eafd..328481de6e 100644 --- a/crates/ui/src/components/keybinding.rs +++ b/crates/ui/src/components/keybinding.rs @@ -3,7 +3,7 @@ use crate::PlatformStyle; use crate::{h_flex, prelude::*, Icon, IconName, IconSize}; use gpui::{relative, Action, FocusHandle, IntoElement, Keystroke, WindowContext}; -#[derive(IntoElement, Clone)] +#[derive(Debug, IntoElement, Clone)] pub struct KeyBinding { /// A keybinding consists of a key and a set of modifier keys. /// More then one keybinding produces a chord. From 5766afe7102b8f9af3fe1b025d9db5c6f7c86ca7 Mon Sep 17 00:00:00 2001 From: Kyle Kelley Date: Fri, 22 Nov 2024 16:31:11 -0800 Subject: [PATCH 119/886] Pass through remote kernel's language on legacy selection (#21088) When selecting an active kernel based on legacy usage, have remote kernels defer to language within kernelspec. Release Notes: - N/A --- crates/repl/src/repl_store.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/repl/src/repl_store.rs b/crates/repl/src/repl_store.rs index 27854c0eee..49c24bce68 100644 --- a/crates/repl/src/repl_store.rs +++ b/crates/repl/src/repl_store.rs @@ -258,8 +258,9 @@ impl ReplStore { runtime_specification.kernelspec.language.to_lowercase() == language_at_cursor.code_fence_block_name().to_lowercase() } - KernelSpecification::Remote(_) => { - unimplemented!() + KernelSpecification::Remote(remote_spec) => { + remote_spec.kernelspec.language.to_lowercase() + == language_at_cursor.code_fence_block_name().to_lowercase() } }) .cloned() From 984bb192baa0d1f5cc27269c4929d8899cb07879 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 22 Nov 2024 20:40:39 -0700 Subject: [PATCH 120/886] Send llm events to snowflake too (#21091) Closes #ISSUE Release Notes: - N/A --- crates/client/src/client.rs | 8 ++ crates/client/src/telemetry.rs | 4 + crates/collab/src/api.rs | 33 ++++++++ crates/collab/src/api/events.rs | 43 +++++++++- crates/collab/src/cents.rs | 3 + crates/collab/src/llm.rs | 95 +++++++++++++++++----- crates/collab/src/llm/db/queries/usages.rs | 4 +- crates/collab/src/llm/token.rs | 8 ++ crates/collab/src/rpc.rs | 9 +- crates/collab/src/tests/test_server.rs | 1 + 10 files changed, 183 insertions(+), 25 deletions(-) diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 041973e884..a20584fabd 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -1067,6 +1067,8 @@ impl Client { let proxy = http.proxy().cloned(); let credentials = credentials.clone(); let rpc_url = self.rpc_url(http, release_channel); + let system_id = self.telemetry.system_id(); + let metrics_id = self.telemetry.metrics_id(); cx.background_executor().spawn(async move { use HttpOrHttps::*; @@ -1118,6 +1120,12 @@ impl Client { "x-zed-release-channel", HeaderValue::from_str(release_channel.map(|r| r.dev_name()).unwrap_or("unknown"))?, ); + if let Some(system_id) = system_id { + request_headers.insert("x-zed-system-id", HeaderValue::from_str(&system_id)?); + } + if let Some(metrics_id) = metrics_id { + request_headers.insert("x-zed-metrics-id", HeaderValue::from_str(&metrics_id)?); + } match url_scheme { Https => { diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 583f9757c4..eef2a8215f 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -533,6 +533,10 @@ impl Telemetry { self.state.lock().metrics_id.clone() } + pub fn system_id(self: &Arc) -> Option> { + self.state.lock().system_id.clone() + } + pub fn installation_id(self: &Arc) -> Option> { self.state.lock().installation_id.clone() } diff --git a/crates/collab/src/api.rs b/crates/collab/src/api.rs index 46ca5906c5..7adf17ac06 100644 --- a/crates/collab/src/api.rs +++ b/crates/collab/src/api.rs @@ -61,6 +61,39 @@ impl std::fmt::Display for CloudflareIpCountryHeader { } } +pub struct SystemIdHeader(String); + +impl Header for SystemIdHeader { + fn name() -> &'static HeaderName { + static SYSTEM_ID_HEADER: OnceLock = OnceLock::new(); + SYSTEM_ID_HEADER.get_or_init(|| HeaderName::from_static("x-zed-system-id")) + } + + fn decode<'i, I>(values: &mut I) -> Result + where + Self: Sized, + I: Iterator, + { + let system_id = values + .next() + .ok_or_else(axum::headers::Error::invalid)? + .to_str() + .map_err(|_| axum::headers::Error::invalid())?; + + Ok(Self(system_id.to_string())) + } + + fn encode>(&self, _values: &mut E) { + unimplemented!() + } +} + +impl std::fmt::Display for SystemIdHeader { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + pub fn routes(rpc_server: Arc) -> Router<(), Body> { Router::new() .route("/user", get(get_authenticated_user)) diff --git a/crates/collab/src/api/events.rs b/crates/collab/src/api/events.rs index 3cda6a397a..11137cb4e9 100644 --- a/crates/collab/src/api/events.rs +++ b/crates/collab/src/api/events.rs @@ -1578,8 +1578,8 @@ fn for_snowflake( }) } -#[derive(Serialize, Deserialize)] -struct SnowflakeRow { +#[derive(Serialize, Deserialize, Debug)] +pub struct SnowflakeRow { pub time: chrono::DateTime, pub user_id: Option, pub device_id: Option, @@ -1588,3 +1588,42 @@ struct SnowflakeRow { pub user_properties: Option, pub insert_id: Option, } + +impl SnowflakeRow { + pub fn new( + event_type: impl Into, + metrics_id: Option, + is_staff: bool, + system_id: Option, + event_properties: serde_json::Value, + ) -> Self { + Self { + time: chrono::Utc::now(), + event_type: event_type.into(), + device_id: system_id, + user_id: metrics_id.map(|id| id.to_string()), + insert_id: Some(uuid::Uuid::new_v4().to_string()), + event_properties, + user_properties: Some(json!({"is_staff": is_staff})), + } + } + + pub async fn write( + self, + client: &Option, + stream: &Option, + ) -> anyhow::Result<()> { + let Some((client, stream)) = client.as_ref().zip(stream.as_ref()) else { + return Ok(()); + }; + let row = serde_json::to_vec(&self)?; + client + .put_record() + .stream_name(stream) + .partition_key(&self.user_id.unwrap_or_default()) + .data(row.into()) + .send() + .await?; + Ok(()) + } +} diff --git a/crates/collab/src/cents.rs b/crates/collab/src/cents.rs index defbcea4e2..a05971f141 100644 --- a/crates/collab/src/cents.rs +++ b/crates/collab/src/cents.rs @@ -1,3 +1,5 @@ +use serde::Serialize; + /// A number of cents. #[derive( Debug, @@ -12,6 +14,7 @@ derive_more::AddAssign, derive_more::Sub, derive_more::SubAssign, + Serialize, )] pub struct Cents(pub u32); diff --git a/crates/collab/src/llm.rs b/crates/collab/src/llm.rs index fa48ec95ea..603b76db73 100644 --- a/crates/collab/src/llm.rs +++ b/crates/collab/src/llm.rs @@ -3,9 +3,11 @@ pub mod db; mod telemetry; mod token; +use crate::api::events::SnowflakeRow; +use crate::api::CloudflareIpCountryHeader; +use crate::build_kinesis_client; use crate::{ - api::CloudflareIpCountryHeader, build_clickhouse_client, db::UserId, executor::Executor, Cents, - Config, Error, Result, + build_clickhouse_client, db::UserId, executor::Executor, Cents, Config, Error, Result, }; use anyhow::{anyhow, Context as _}; use authorization::authorize_access_to_language_model; @@ -28,6 +30,7 @@ use rpc::{ proto::Plan, LanguageModelProvider, PerformCompletionParams, EXPIRED_LLM_TOKEN_HEADER_NAME, }; use rpc::{ListModelsResponse, MAX_LLM_MONTHLY_SPEND_REACHED_HEADER_NAME}; +use serde_json::json; use std::{ pin::Pin, sync::Arc, @@ -45,6 +48,7 @@ pub struct LlmState { pub executor: Executor, pub db: Arc, pub http_client: ReqwestClient, + pub kinesis_client: Option, pub clickhouse_client: Option, active_user_count_by_model: RwLock, ActiveUserCount)>>, @@ -77,6 +81,11 @@ impl LlmState { executor, db, http_client, + kinesis_client: if config.kinesis_access_key.is_some() { + build_kinesis_client(&config).await.log_err() + } else { + None + }, clickhouse_client: config .clickhouse_url .as_ref() @@ -521,25 +530,50 @@ async fn check_usage_limit( UsageMeasure::TokensPerDay => "tokens_per_day", }; - if let Some(client) = state.clickhouse_client.as_ref() { - tracing::info!( - target: "user rate limit", - user_id = claims.user_id, - login = claims.github_user_login, - authn.jti = claims.jti, - is_staff = claims.is_staff, - provider = provider.to_string(), - model = model.name, - requests_this_minute = usage.requests_this_minute, - tokens_this_minute = usage.tokens_this_minute, - tokens_this_day = usage.tokens_this_day, - users_in_recent_minutes = users_in_recent_minutes, - users_in_recent_days = users_in_recent_days, - max_requests_per_minute = per_user_max_requests_per_minute, - max_tokens_per_minute = per_user_max_tokens_per_minute, - max_tokens_per_day = per_user_max_tokens_per_day, - ); + tracing::info!( + target: "user rate limit", + user_id = claims.user_id, + login = claims.github_user_login, + authn.jti = claims.jti, + is_staff = claims.is_staff, + provider = provider.to_string(), + model = model.name, + requests_this_minute = usage.requests_this_minute, + tokens_this_minute = usage.tokens_this_minute, + tokens_this_day = usage.tokens_this_day, + users_in_recent_minutes = users_in_recent_minutes, + users_in_recent_days = users_in_recent_days, + max_requests_per_minute = per_user_max_requests_per_minute, + max_tokens_per_minute = per_user_max_tokens_per_minute, + max_tokens_per_day = per_user_max_tokens_per_day, + ); + SnowflakeRow::new( + "Language Model Rate Limited", + claims.metrics_id, + claims.is_staff, + claims.system_id.clone(), + json!({ + "usage": usage, + "users_in_recent_minutes": users_in_recent_minutes, + "users_in_recent_days": users_in_recent_days, + "max_requests_per_minute": per_user_max_requests_per_minute, + "max_tokens_per_minute": per_user_max_tokens_per_minute, + "max_tokens_per_day": per_user_max_tokens_per_day, + "plan": match claims.plan { + Plan::Free => "free".to_string(), + Plan::ZedPro => "zed_pro".to_string(), + }, + "model": model.name.clone(), + "provider": provider.to_string(), + "usage_measure": resource.to_string(), + }), + ) + .write(&state.kinesis_client, &state.config.kinesis_stream) + .await + .log_err(); + + if let Some(client) = state.clickhouse_client.as_ref() { report_llm_rate_limit( client, LlmRateLimitEventRow { @@ -652,6 +686,27 @@ impl Drop for TokenCountingStream { tokens_this_minute = usage.tokens_this_minute, ); + let properties = json!({ + "plan": match claims.plan { + Plan::Free => "free".to_string(), + Plan::ZedPro => "zed_pro".to_string(), + }, + "model": model, + "provider": provider, + "usage": usage, + "tokens": tokens + }); + SnowflakeRow::new( + "Language Model Used", + claims.metrics_id, + claims.is_staff, + claims.system_id.clone(), + properties, + ) + .write(&state.kinesis_client, &state.config.kinesis_stream) + .await + .log_err(); + if let Some(clickhouse_client) = state.clickhouse_client.as_ref() { report_llm_usage( clickhouse_client, diff --git a/crates/collab/src/llm/db/queries/usages.rs b/crates/collab/src/llm/db/queries/usages.rs index f262821743..27e8039f54 100644 --- a/crates/collab/src/llm/db/queries/usages.rs +++ b/crates/collab/src/llm/db/queries/usages.rs @@ -9,7 +9,7 @@ use strum::IntoEnumIterator as _; use super::*; -#[derive(Debug, PartialEq, Clone, Copy, Default)] +#[derive(Debug, PartialEq, Clone, Copy, Default, serde::Serialize)] pub struct TokenUsage { pub input: usize, pub input_cache_creation: usize, @@ -23,7 +23,7 @@ impl TokenUsage { } } -#[derive(Debug, PartialEq, Clone, Copy)] +#[derive(Debug, PartialEq, Clone, Copy, serde::Serialize)] pub struct Usage { pub requests_this_minute: usize, pub tokens_this_minute: usize, diff --git a/crates/collab/src/llm/token.rs b/crates/collab/src/llm/token.rs index 35f7cf26e7..7e0706e2d5 100644 --- a/crates/collab/src/llm/token.rs +++ b/crates/collab/src/llm/token.rs @@ -8,6 +8,7 @@ use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation}; use serde::{Deserialize, Serialize}; use std::time::Duration; use thiserror::Error; +use uuid::Uuid; #[derive(Clone, Debug, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -16,6 +17,10 @@ pub struct LlmTokenClaims { pub exp: u64, pub jti: String, pub user_id: u64, + #[serde(default)] + pub system_id: Option, + #[serde(default)] + pub metrics_id: Option, pub github_user_login: String, pub is_staff: bool, pub has_llm_closed_beta_feature_flag: bool, @@ -36,6 +41,7 @@ impl LlmTokenClaims { has_llm_closed_beta_feature_flag: bool, has_llm_subscription: bool, plan: rpc::proto::Plan, + system_id: Option, config: &Config, ) -> Result { let secret = config @@ -49,6 +55,8 @@ impl LlmTokenClaims { exp: (now + LLM_TOKEN_LIFETIME).timestamp() as u64, jti: uuid::Uuid::new_v4().to_string(), user_id: user.id.to_proto(), + system_id, + metrics_id: Some(user.metrics_id), github_user_login: user.github_login.clone(), is_staff, has_llm_closed_beta_feature_flag, diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 1184c48618..a17d4924b7 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -1,6 +1,6 @@ mod connection_pool; -use crate::api::CloudflareIpCountryHeader; +use crate::api::{CloudflareIpCountryHeader, SystemIdHeader}; use crate::llm::LlmTokenClaims; use crate::{ auth, @@ -137,6 +137,7 @@ struct Session { /// The GeoIP country code for the user. #[allow(unused)] geoip_country_code: Option, + system_id: Option, _executor: Executor, } @@ -682,6 +683,7 @@ impl Server { principal: Principal, zed_version: ZedVersion, geoip_country_code: Option, + system_id: Option, send_connection_id: Option>, executor: Executor, ) -> impl Future { @@ -737,6 +739,7 @@ impl Server { app_state: this.app_state.clone(), http_client, geoip_country_code, + system_id, _executor: executor.clone(), supermaven_client, }; @@ -1056,6 +1059,7 @@ pub fn routes(server: Arc) -> Router<(), Body> { .layer(Extension(server)) } +#[allow(clippy::too_many_arguments)] pub async fn handle_websocket_request( TypedHeader(ProtocolVersion(protocol_version)): TypedHeader, app_version_header: Option>, @@ -1063,6 +1067,7 @@ pub async fn handle_websocket_request( Extension(server): Extension>, Extension(principal): Extension, country_code_header: Option>, + system_id_header: Option>, ws: WebSocketUpgrade, ) -> axum::response::Response { if protocol_version != rpc::PROTOCOL_VERSION { @@ -1104,6 +1109,7 @@ pub async fn handle_websocket_request( principal, version, country_code_header.map(|header| header.to_string()), + system_id_header.map(|header| header.to_string()), None, Executor::Production, ) @@ -4053,6 +4059,7 @@ async fn get_llm_api_token( has_llm_closed_beta_feature_flag, has_llm_subscription, session.current_plan(&db).await?, + session.system_id.clone(), &session.app_state.config, )?; response.send(proto::GetLlmTokenResponse { token })?; diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index 8a09f06092..c93cce9770 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -244,6 +244,7 @@ impl TestServer { Principal::User(user), ZedVersion(SemanticVersion::new(1, 0, 0)), None, + None, Some(connection_id_tx), Executor::Deterministic(cx.background_executor().clone()), )) From 43f0ea759ba69e24e9f7df3c8325352aaa226a58 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Fri, 22 Nov 2024 23:49:53 -0500 Subject: [PATCH 121/886] Remove non-existent call event types (#21093) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These are not real (from Clickhouse): ``` ┌─operation────────────┬──────c─┐ 1. │ join channel │ 136221 │ 2. │ open channel notes │ 95529 │ 3. │ hang up │ 66264 │ 4. │ disable microphone │ 34116 │ 5. │ enable microphone │ 25090 │ 6. │ enable screen share │ 20751 │ 7. │ invite │ 15827 │ 8. │ share project │ 14580 │ 9. │ accept incoming │ 13708 │ 10. │ disable screen share │ 10440 │ 11. │ unshare project │ 9556 │ 12. │ decline incoming │ 455 │ 13. │ enable camera │ 6 │ 14. │ disable camera │ 4 │ └──────────────────────┴────────┘ ``` Release Notes: - N/A --- crates/collab/src/api/events.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/collab/src/api/events.rs b/crates/collab/src/api/events.rs index 11137cb4e9..95bd2a89b2 100644 --- a/crates/collab/src/api/events.rs +++ b/crates/collab/src/api/events.rs @@ -1420,8 +1420,6 @@ fn for_snowflake( "enable screen share" => "Screen Share Enabled".to_string(), "disable screen share" => "Screen Share Disabled".to_string(), "decline incoming" => "Incoming Call Declined".to_string(), - "enable camera" => "Camera Enabled".to_string(), - "disable camera" => "Camera Disabled".to_string(), _ => format!("Unknown Call Event: {}", e.operation), }; From 8a9c53524aeb0e012e158491493a70c316a0f3ab Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Sat, 23 Nov 2024 05:38:00 +0000 Subject: [PATCH 122/886] docs: Add JSON Schema settings for json-language-server (#21084) Add json-language server docs Recognize `.vscode/*` files as JSONC by default --- assets/settings/default.json | 2 +- docs/src/languages/json.md | 43 ++++++++++++++++++++++++++++++++++-- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 02527e8e67..45a211789f 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -885,7 +885,7 @@ // "file_types": { "Plain Text": ["txt"], - "JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json", "**/Zed/**/*.json"], + "JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json", "**/Zed/**/*.json", "**/.vscode/**/*.json"], "Shell Script": [".env.*"] }, /// By default use a recent system version of node, or install our own. diff --git a/docs/src/languages/json.md b/docs/src/languages/json.md index 31fc8c0689..3783dcae2c 100644 --- a/docs/src/languages/json.md +++ b/docs/src/languages/json.md @@ -30,8 +30,47 @@ To workaround this behavior you can add the following to your `.prettierrc` } ``` +## JSON Language Server + +Zed automatically out of the box supports JSON Schema validation of `package.json` and `tsconfig.json` files, but `json-language-server` can use JSON Schema definitions in project files, from the [JSON Schema Store](https://www.schemastore.org/json/) or other publicly available URLs for JSON validation. + +### Inline Schema Specification + +To specify a schema inline with your JSON files, add a `$schema` top level key linking to your json schema file. + +For example to for a `.luarc.json` for use with [lua-language-server](https://github.com/LuaLS/lua-language-server/): + +```json +{ + "$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json", + "runtime.version": "Lua 5.4" +} +``` + +### Schema Specification via Settings + +You can alternatively associate JSON Schemas with file paths by via Zed LSP settings. + +To + +```json +"lsp": { +"json-language-server": { + "settings": { + "json": { + "schemas": [ + { + "fileMatch": ["*/*.luarc.json"], + "url": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json" + } + ] + } + } +} +``` + +You can also pass any of the [supported settings](https://github.com/Microsoft/vscode/blob/main/extensions/json-language-features/server/README.md#settings) to json-language-server by specifying them in your Zed settings.json: + From 2177e833d8caadda2e3c2f66cbb871497522cb95 Mon Sep 17 00:00:00 2001 From: Kyle Kelley Date: Fri, 22 Nov 2024 22:11:20 -0800 Subject: [PATCH 123/886] Upgrade jupyter websocket client (#21095) Upgrade to changes from https://github.com/runtimed/runtimed/pull/158 Release Notes: - N/A --- Cargo.lock | 114 ++++++++++++++++------ Cargo.toml | 8 +- crates/repl/src/kernels/mod.rs | 3 +- crates/repl/src/kernels/native_kernel.rs | 4 +- crates/repl/src/kernels/remote_kernels.rs | 24 ++--- 5 files changed, 98 insertions(+), 55 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9881c23e84..52841152f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -885,6 +885,20 @@ version = "4.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" +[[package]] +name = "async-tls" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfeefd0ca297cbbb3bd34fd6b228401c2a5177038257afd751bc29f0a2da4795" +dependencies = [ + "futures-core", + "futures-io", + "rustls 0.20.9", + "rustls-pemfile 1.0.4", + "webpki", + "webpki-roots 0.22.6", +] + [[package]] name = "async-tls" version = "0.13.0" @@ -916,6 +930,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce01ac37fdc85f10a43c43bc582cbd566720357011578a935761075f898baf58" dependencies = [ "async-std", + "async-tls 0.12.0", "futures-io", "futures-util", "log", @@ -930,7 +945,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90e661b6cb0a6eb34d02c520b052daa3aa9ac0cc02495c9d066bbce13ead132b" dependencies = [ "async-std", - "async-tls", + "async-tls 0.13.0", "futures-io", "futures-util", "log", @@ -1100,7 +1115,7 @@ dependencies = [ "fastrand 2.2.0", "hex", "http 0.2.12", - "ring", + "ring 0.17.8", "time", "tokio", "tracing", @@ -1290,7 +1305,7 @@ dependencies = [ "once_cell", "p256", "percent-encoding", - "ring", + "ring 0.17.8", "sha2", "subtle", "time", @@ -4457,7 +4472,7 @@ dependencies = [ "futures-core", "futures-sink", "nanorand", - "spin", + "spin 0.9.8", ] [[package]] @@ -6370,7 +6385,7 @@ dependencies = [ "base64 0.21.7", "js-sys", "pem", - "ring", + "ring 0.17.8", "serde", "serde_json", "simple_asn1", @@ -6378,9 +6393,9 @@ dependencies = [ [[package]] name = "jupyter-protocol" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f3e9d36f282f7e0400de20921d283121a97c5a5a6db2c1bb0c0853defff9934" +checksum = "3d4d496ac890e14efc12c5289818b3c39e3026a7bb02d5576b011e1a062d4bcc" dependencies = [ "anyhow", "async-trait", @@ -6396,9 +6411,9 @@ dependencies = [ [[package]] name = "jupyter-serde" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11adb69edaf2eb03d5e84249f68f870dd03d4c8f955314b5a32b2db5798e9b9a" +checksum = "32aa595c3912167b7eafcaa822b767ad1fa9605a18127fc9ac741241b796410e" dependencies = [ "anyhow", "serde", @@ -6409,9 +6424,9 @@ dependencies = [ [[package]] name = "jupyter-websocket-client" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d315d037789a652084877b0919615e937d2f2e877b01aa4ba8fcc1ab07cb58b" +checksum = "5850894210a3f033ff730d6f956b0335db38573ce7bb61c6abbf69dcbe284ba7" dependencies = [ "anyhow", "async-trait", @@ -6717,7 +6732,7 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" dependencies = [ - "spin", + "spin 0.9.8", ] [[package]] @@ -7439,9 +7454,9 @@ dependencies = [ [[package]] name = "nbformat" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "187de1b1f1430353ef9b5208096d84f7bf089ee1593f14213d122b7fbb1f3dee" +checksum = "aa6827a3881aa100bb2241cd2633b3c79474dbc93704f1f2cf5cc85064cda4be" dependencies = [ "anyhow", "chrono", @@ -9470,7 +9485,7 @@ dependencies = [ "bytes 1.8.0", "getrandom 0.2.15", "rand 0.8.5", - "ring", + "ring 0.17.8", "rustc-hash 2.0.0", "rustls 0.23.16", "rustls-pki-types", @@ -10113,6 +10128,21 @@ dependencies = [ "util", ] +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin 0.5.2", + "untrusted 0.7.1", + "web-sys", + "winapi", +] + [[package]] name = "ring" version = "0.17.8" @@ -10123,8 +10153,8 @@ dependencies = [ "cfg-if", "getrandom 0.2.15", "libc", - "spin", - "untrusted", + "spin 0.9.8", + "untrusted 0.9.0", "windows-sys 0.52.0", ] @@ -10260,9 +10290,9 @@ dependencies = [ [[package]] name = "runtimelib" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2db079f82c110e25c3202d20c7cd29dcbfa93d96de7c5bb8bb6f294f477567cf" +checksum = "b3a8ab675beb5cf25c28f9c6ddb8f47bcf73b43872797e6ab6157865f44d1e19" dependencies = [ "anyhow", "async-dispatcher", @@ -10276,7 +10306,7 @@ dependencies = [ "glob", "jupyter-protocol", "jupyter-serde", - "ring", + "ring 0.17.8", "serde", "serde_json", "shellexpand 3.1.0", @@ -10403,6 +10433,18 @@ dependencies = [ "rustix 0.38.40", ] +[[package]] +name = "rustls" +version = "0.20.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99" +dependencies = [ + "log", + "ring 0.16.20", + "sct", + "webpki", +] + [[package]] name = "rustls" version = "0.21.12" @@ -10410,7 +10452,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ "log", - "ring", + "ring 0.17.8", "rustls-webpki 0.101.7", "sct", ] @@ -10422,7 +10464,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eee87ff5d9b36712a58574e12e9f0ea80f915a5b0ac518d322b24a465617925e" dependencies = [ "once_cell", - "ring", + "ring 0.17.8", "rustls-pki-types", "rustls-webpki 0.102.8", "subtle", @@ -10487,8 +10529,8 @@ version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ - "ring", - "untrusted", + "ring 0.17.8", + "untrusted 0.9.0", ] [[package]] @@ -10497,9 +10539,9 @@ version = "0.102.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" dependencies = [ - "ring", + "ring 0.17.8", "rustls-pki-types", - "untrusted", + "untrusted 0.9.0", ] [[package]] @@ -10613,8 +10655,8 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ - "ring", - "untrusted", + "ring 0.17.8", + "untrusted 0.9.0", ] [[package]] @@ -11376,6 +11418,12 @@ dependencies = [ "smallvec", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "spin" version = "0.9.8" @@ -13485,6 +13533,12 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1766d682d402817b5ac4490b3c3002d91dfa0d22812f341609f97b08757359c" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "untrusted" version = "0.9.0" @@ -14393,8 +14447,8 @@ version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" dependencies = [ - "ring", - "untrusted", + "ring 0.17.8", + "untrusted 0.9.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index b071ca19d1..c066b942e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -378,14 +378,14 @@ indexmap = { version = "1.6.2", features = ["serde"] } indoc = "2" itertools = "0.13.0" jsonwebtoken = "9.3" -jupyter-protocol = { version = "0.2.0" } -jupyter-websocket-client = { version = "0.4.1" } +jupyter-protocol = { version = "0.3.0" } +jupyter-websocket-client = { version = "0.5.0" } libc = "0.2" linkify = "0.10.0" log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] } markup5ever_rcdom = "0.3.0" nanoid = "0.4" -nbformat = "0.6.0" +nbformat = { version = "0.7.0" } nix = "0.29" num-format = "0.4.4" once_cell = "1.19.0" @@ -419,7 +419,7 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "fd110f "stream", ] } rsa = "0.9.6" -runtimelib = { version = "0.21.0", default-features = false, features = [ +runtimelib = { version = "0.22.0", default-features = false, features = [ "async-dispatcher-runtime", ] } rustc-demangle = "0.1.23" diff --git a/crates/repl/src/kernels/mod.rs b/crates/repl/src/kernels/mod.rs index 47fde97154..e829b1946c 100644 --- a/crates/repl/src/kernels/mod.rs +++ b/crates/repl/src/kernels/mod.rs @@ -15,7 +15,8 @@ use project::{Project, WorktreeId}; pub use remote_kernels::*; use anyhow::Result; -use runtimelib::{ExecutionState, JupyterKernelspec, JupyterMessage, KernelInfoReply}; +use jupyter_protocol::JupyterKernelspec; +use runtimelib::{ExecutionState, JupyterMessage, KernelInfoReply}; use ui::{Icon, IconName, SharedString}; pub type JupyterMessageChannel = stream::SelectAll>; diff --git a/crates/repl/src/kernels/native_kernel.rs b/crates/repl/src/kernels/native_kernel.rs index 6f7c5d92ee..974a721ac5 100644 --- a/crates/repl/src/kernels/native_kernel.rs +++ b/crates/repl/src/kernels/native_kernel.rs @@ -6,9 +6,9 @@ use futures::{ AsyncBufReadExt as _, SinkExt as _, }; use gpui::{EntityId, Task, View, WindowContext}; -use jupyter_protocol::{JupyterMessage, JupyterMessageContent, KernelInfoReply}; +use jupyter_protocol::{JupyterKernelspec, JupyterMessage, JupyterMessageContent, KernelInfoReply}; use project::Fs; -use runtimelib::{dirs, ConnectionInfo, ExecutionState, JupyterKernelspec}; +use runtimelib::{dirs, ConnectionInfo, ExecutionState}; use smol::{net::TcpListener, process::Command}; use std::{ env, diff --git a/crates/repl/src/kernels/remote_kernels.rs b/crates/repl/src/kernels/remote_kernels.rs index 808a7dbf02..e1b41276fa 100644 --- a/crates/repl/src/kernels/remote_kernels.rs +++ b/crates/repl/src/kernels/remote_kernels.rs @@ -1,8 +1,7 @@ use futures::{channel::mpsc, SinkExt as _}; use gpui::{Task, View, WindowContext}; use http_client::{AsyncBody, HttpClient, Request}; -use jupyter_protocol::{ExecutionState, JupyterMessage, KernelInfoReply}; -use runtimelib::JupyterKernelspec; +use jupyter_protocol::{ExecutionState, JupyterKernelspec, JupyterMessage, KernelInfoReply}; use futures::StreamExt; use smol::io::AsyncReadExt as _; @@ -34,8 +33,9 @@ pub async fn launch_remote_kernel( // let kernel_launch_request = KernelLaunchRequest { name: kernel_name.to_string(), - // todo: add path to runtimelib - // path, + // Note: since the path we have locally may not be the same as the one on the remote server, + // we don't send it. We'll have to evaluate this decisiion along the way. + path: None, }; let kernel_launch_request = serde_json::to_string(&kernel_launch_request)?; @@ -91,19 +91,7 @@ pub async fn list_remote_kernelspecs( 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, - }, + kernelspec: spec.spec, }) .collect::>(); @@ -163,7 +151,7 @@ impl RemoteRunningKernel { ) .await?; - let kernel_socket = remote_server.connect_to_kernel(&kernel_id).await?; + let (kernel_socket, _response) = remote_server.connect_to_kernel(&kernel_id).await?; let (mut w, mut r): (JupyterWebSocketWriter, JupyterWebSocketReader) = kernel_socket.split(); From f30de4852a5c672b4cb43ab528156c953f1c6eb0 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Sat, 23 Nov 2024 06:17:39 +0000 Subject: [PATCH 124/886] docs: Proto Language is by extension not native (#21096) Fixes docs to reflect that Protobuf support is via extension. Comment out references ProtoLS formatter. Need to test both protols and protobuf-language-server to ensure both work. --- docs/src/languages/proto.md | 40 +++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/docs/src/languages/proto.md b/docs/src/languages/proto.md index 934080a1d7..777fd81b8a 100644 --- a/docs/src/languages/proto.md +++ b/docs/src/languages/proto.md @@ -1,9 +1,44 @@ # Proto -Proto/proto3 (Protocol Buffers definition language) support is available natively in Zed. +Proto/proto3 (Protocol Buffers definition language) support is available through the [Proto extension](https://github.com/zed-industries/zed/tree/main/extensions/proto). - Tree Sitter: [coder3101/tree-sitter-proto](https://github.com/coder3101/tree-sitter-proto) -- Language Server: [protols](https://github.com/coder3101/protols) +- Language Servers: [protobuf-language-server](https://github.com/lasorda/protobuf-language-server) + + From 9adc3b4e82f1c3e825c597bedeb0b184876dcb78 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Sat, 23 Nov 2024 11:24:52 -0500 Subject: [PATCH 125/886] Break ground on `assistant2` (#21109) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR breaks ground on a new `assistant2` crate. In order to see this new version of the assistant, both of the following must be true: 1. The `assistant2` feature flag is enabled for your user - It is **not** currently enabled for all staff. 2. You are running a development build of Zed The intent here is to enable the folks working on `assistant2` to incrementally land work onto `main` without breaking use of the current Assistant for anyone. Screenshot 2024-11-23 at 10 46 08 AM Release Notes: - N/A --- Cargo.lock | 14 +++ Cargo.toml | 2 + crates/assistant2/Cargo.toml | 22 ++++ crates/assistant2/LICENSE-GPL | 1 + crates/assistant2/src/assistant.rs | 40 +++++++ crates/assistant2/src/assistant_panel.rs | 123 ++++++++++++++++++++++ crates/feature_flags/src/feature_flags.rs | 10 ++ crates/zed/Cargo.toml | 1 + crates/zed/src/main.rs | 1 + crates/zed/src/zed.rs | 34 +++++- 10 files changed, 243 insertions(+), 5 deletions(-) create mode 100644 crates/assistant2/Cargo.toml create mode 120000 crates/assistant2/LICENSE-GPL create mode 100644 crates/assistant2/src/assistant.rs create mode 100644 crates/assistant2/src/assistant_panel.rs diff --git a/Cargo.lock b/Cargo.lock index 52841152f0..8138744707 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -449,6 +449,19 @@ dependencies = [ "zed_actions", ] +[[package]] +name = "assistant2" +version = "0.1.0" +dependencies = [ + "anyhow", + "command_palette_hooks", + "feature_flags", + "gpui", + "proto", + "ui", + "workspace", +] + [[package]] name = "assistant_slash_command" version = "0.1.0" @@ -15549,6 +15562,7 @@ dependencies = [ "ashpd", "assets", "assistant", + "assistant2", "async-watch", "audio", "auto_update", diff --git a/Cargo.toml b/Cargo.toml index c066b942e7..a8537611fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "crates/anthropic", "crates/assets", "crates/assistant", + "crates/assistant2", "crates/assistant_slash_command", "crates/assistant_tool", "crates/audio", @@ -186,6 +187,7 @@ ai = { path = "crates/ai" } anthropic = { path = "crates/anthropic" } assets = { path = "crates/assets" } assistant = { path = "crates/assistant" } +assistant2 = { path = "crates/assistant2" } assistant_slash_command = { path = "crates/assistant_slash_command" } assistant_tool = { path = "crates/assistant_tool" } audio = { path = "crates/audio" } diff --git a/crates/assistant2/Cargo.toml b/crates/assistant2/Cargo.toml new file mode 100644 index 0000000000..320cd015e2 --- /dev/null +++ b/crates/assistant2/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "assistant2" +version = "0.1.0" +edition = "2021" +publish = false +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/assistant.rs" +doctest = false + +[dependencies] +anyhow.workspace = true +command_palette_hooks.workspace = true +feature_flags.workspace = true +gpui.workspace = true +proto.workspace = true +ui.workspace = true +workspace.workspace = true diff --git a/crates/assistant2/LICENSE-GPL b/crates/assistant2/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/assistant2/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/assistant2/src/assistant.rs b/crates/assistant2/src/assistant.rs new file mode 100644 index 0000000000..31676198ba --- /dev/null +++ b/crates/assistant2/src/assistant.rs @@ -0,0 +1,40 @@ +mod assistant_panel; + +use command_palette_hooks::CommandPaletteFilter; +use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt}; +use gpui::{actions, AppContext}; + +pub use crate::assistant_panel::AssistantPanel; + +actions!(assistant2, [ToggleFocus, NewChat]); + +const NAMESPACE: &str = "assistant2"; + +/// Initializes the `assistant2` crate. +pub fn init(cx: &mut AppContext) { + assistant_panel::init(cx); + feature_gate_assistant2_actions(cx); +} + +fn feature_gate_assistant2_actions(cx: &mut AppContext) { + const ASSISTANT1_NAMESPACE: &str = "assistant"; + + CommandPaletteFilter::update_global(cx, |filter, _cx| { + filter.hide_namespace(NAMESPACE); + }); + + cx.observe_flag::(move |is_enabled, cx| { + if is_enabled { + CommandPaletteFilter::update_global(cx, |filter, _cx| { + filter.show_namespace(NAMESPACE); + filter.hide_namespace(ASSISTANT1_NAMESPACE); + }); + } else { + CommandPaletteFilter::update_global(cx, |filter, _cx| { + filter.hide_namespace(NAMESPACE); + filter.show_namespace(ASSISTANT1_NAMESPACE); + }); + } + }) + .detach(); +} diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs new file mode 100644 index 0000000000..7c586dd35e --- /dev/null +++ b/crates/assistant2/src/assistant_panel.rs @@ -0,0 +1,123 @@ +use anyhow::Result; +use gpui::{ + prelude::*, px, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, + FocusableView, Pixels, Task, View, ViewContext, WeakView, WindowContext, +}; +use ui::prelude::*; +use workspace::dock::{DockPosition, Panel, PanelEvent}; +use workspace::{Pane, Workspace}; + +use crate::{NewChat, ToggleFocus}; + +pub fn init(cx: &mut AppContext) { + cx.observe_new_views( + |workspace: &mut Workspace, _cx: &mut ViewContext| { + workspace.register_action(|workspace, _: &ToggleFocus, cx| { + workspace.toggle_panel_focus::(cx); + }); + }, + ) + .detach(); +} + +pub struct AssistantPanel { + pane: View, +} + +impl AssistantPanel { + pub fn load( + workspace: WeakView, + cx: AsyncWindowContext, + ) -> Task>> { + cx.spawn(|mut cx| async move { + workspace.update(&mut cx, |workspace, cx| { + cx.new_view(|cx| Self::new(workspace, cx)) + }) + }) + } + + fn new(workspace: &Workspace, cx: &mut ViewContext) -> Self { + let pane = cx.new_view(|cx| { + let mut pane = Pane::new( + workspace.weak_handle(), + workspace.project().clone(), + Default::default(), + None, + NewChat.boxed_clone(), + cx, + ); + pane.set_can_split(false, cx); + pane.set_can_navigate(true, cx); + + pane + }); + + Self { pane } + } +} + +impl FocusableView for AssistantPanel { + fn focus_handle(&self, cx: &AppContext) -> FocusHandle { + self.pane.focus_handle(cx) + } +} + +impl EventEmitter for AssistantPanel {} + +impl Panel for AssistantPanel { + fn persistent_name() -> &'static str { + "AssistantPanel2" + } + + fn position(&self, _cx: &WindowContext) -> DockPosition { + DockPosition::Right + } + + fn position_is_valid(&self, _: DockPosition) -> bool { + true + } + + fn set_position(&mut self, _position: DockPosition, _cx: &mut ViewContext) {} + + fn size(&self, _cx: &WindowContext) -> Pixels { + px(640.) + } + + fn set_size(&mut self, _size: Option, _cx: &mut ViewContext) {} + + fn is_zoomed(&self, cx: &WindowContext) -> bool { + self.pane.read(cx).is_zoomed() + } + + fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext) { + self.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx)); + } + + fn set_active(&mut self, _active: bool, _cx: &mut ViewContext) {} + + fn pane(&self) -> Option> { + Some(self.pane.clone()) + } + + fn remote_id() -> Option { + Some(proto::PanelId::AssistantPanel) + } + + fn icon(&self, _cx: &WindowContext) -> Option { + Some(IconName::ZedAssistant) + } + + fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> { + Some("Assistant Panel") + } + + fn toggle_action(&self) -> Box { + Box::new(ToggleFocus) + } +} + +impl Render for AssistantPanel { + fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { + div().child(Label::new("Assistant II")) + } +} diff --git a/crates/feature_flags/src/feature_flags.rs b/crates/feature_flags/src/feature_flags.rs index 286acdfc98..416971b36e 100644 --- a/crates/feature_flags/src/feature_flags.rs +++ b/crates/feature_flags/src/feature_flags.rs @@ -39,6 +39,16 @@ pub trait FeatureFlag { } } +pub struct Assistant2FeatureFlag; + +impl FeatureFlag for Assistant2FeatureFlag { + const NAME: &'static str = "assistant2"; + + fn enabled_for_staff() -> bool { + false + } +} + pub struct Remoting {} impl FeatureFlag for Remoting { const NAME: &'static str = "remoting"; diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 755076e360..1959fb0e00 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -19,6 +19,7 @@ activity_indicator.workspace = true anyhow.workspace = true assets.workspace = true assistant.workspace = true +assistant2.workspace = true async-watch.workspace = true audio.workspace = true auto_update.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 73b0e0f199..6febe05d10 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -406,6 +406,7 @@ fn main() { stdout_is_a_pty(), cx, ); + assistant2::init(cx); assistant_hints::init(cx); repl::init( app_state.fs.clone(), diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 322ea3610b..086935542c 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -236,10 +236,29 @@ pub fn initialize_workspace( .unwrap_or(true) }); + let release_channel = ReleaseChannel::global(cx); + let assistant2_feature_flag = cx.wait_for_flag::(); + let prompt_builder = prompt_builder.clone(); cx.spawn(|workspace_handle, mut cx| async move { - let assistant_panel = - assistant::AssistantPanel::load(workspace_handle.clone(), prompt_builder, cx.clone()); + let is_assistant2_enabled = if cfg!(test) { + false + } else { + let is_assistant2_feature_flag_enabled = assistant2_feature_flag.await; + release_channel == ReleaseChannel::Dev && is_assistant2_feature_flag_enabled + }; + + let (assistant_panel, assistant2_panel) = if is_assistant2_enabled { + let assistant2_panel = + assistant2::AssistantPanel::load(workspace_handle.clone(), cx.clone()).await?; + + (None, Some(assistant2_panel)) + } else { + let assistant_panel = + assistant::AssistantPanel::load(workspace_handle.clone(), prompt_builder, cx.clone()).await?; + + (Some(assistant_panel), None) + }; let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone()); let outline_panel = OutlinePanel::load(workspace_handle.clone(), cx.clone()); @@ -257,7 +276,6 @@ pub fn initialize_workspace( project_panel, outline_panel, terminal_panel, - assistant_panel, channels_panel, chat_panel, notification_panel, @@ -265,14 +283,20 @@ pub fn initialize_workspace( project_panel, outline_panel, terminal_panel, - assistant_panel, channels_panel, chat_panel, notification_panel, )?; workspace_handle.update(&mut cx, |workspace, cx| { - workspace.add_panel(assistant_panel, cx); + if let Some(assistant_panel) = assistant_panel { + workspace.add_panel(assistant_panel, cx); + } + + if let Some(assistant2_panel) = assistant2_panel { + workspace.add_panel(assistant2_panel, cx); + } + workspace.add_panel(project_panel, cx); workspace.add_panel(outline_panel, cx); workspace.add_panel(terminal_panel, cx); From 3a0408953d25f3cbb16902a021e1c699d6883b18 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Sat, 23 Nov 2024 12:11:31 -0500 Subject: [PATCH 126/886] Factor out language model selector into its own crate (#21113) This PR factors the language model selector out into its own `language_model_selector` crate so that it can be reused in `assistant2`. Also renamed it from `ModelSelector` to `LanguageModelSelector` to be a bit more specific. Release Notes: - N/A --- Cargo.lock | 15 +++++ Cargo.toml | 2 + crates/assistant/Cargo.toml | 1 + crates/assistant/src/assistant.rs | 2 - crates/assistant/src/assistant_panel.rs | 26 ++++++--- crates/assistant/src/inline_assistant.rs | 20 +++++-- .../src/terminal_inline_assistant.rs | 21 +++++-- crates/language_model_selector/Cargo.toml | 22 +++++++ crates/language_model_selector/LICENSE-GPL | 1 + .../src/language_model_selector.rs} | 57 ++++++++++--------- 10 files changed, 119 insertions(+), 48 deletions(-) create mode 100644 crates/language_model_selector/Cargo.toml create mode 120000 crates/language_model_selector/LICENSE-GPL rename crates/{assistant/src/model_selector.rs => language_model_selector/src/language_model_selector.rs} (90%) diff --git a/Cargo.lock b/Cargo.lock index 8138744707..cb2e662311 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -402,6 +402,7 @@ dependencies = [ "indoc", "language", "language_model", + "language_model_selector", "language_models", "languages", "log", @@ -6603,6 +6604,20 @@ dependencies = [ "util", ] +[[package]] +name = "language_model_selector" +version = "0.1.0" +dependencies = [ + "feature_flags", + "gpui", + "language_model", + "picker", + "proto", + "ui", + "workspace", + "zed_actions", +] + [[package]] name = "language_models" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index a8537611fe..2e5111e2ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,6 +58,7 @@ members = [ "crates/language", "crates/language_extension", "crates/language_model", + "crates/language_model_selector", "crates/language_models", "crates/language_selector", "crates/language_tools", @@ -236,6 +237,7 @@ journal = { path = "crates/journal" } language = { path = "crates/language" } language_extension = { path = "crates/language_extension" } language_model = { path = "crates/language_model" } +language_model_selector = { path = "crates/language_model_selector" } language_models = { path = "crates/language_models" } language_selector = { path = "crates/language_selector" } language_tools = { path = "crates/language_tools" } diff --git a/crates/assistant/Cargo.toml b/crates/assistant/Cargo.toml index 7f5aef3f46..0799d4bbdb 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_model_selector.workspace = true language_models.workspace = true log.workspace = true lsp.workspace = true diff --git a/crates/assistant/src/assistant.rs b/crates/assistant/src/assistant.rs index b891c3da2a..f6e435bfb8 100644 --- a/crates/assistant/src/assistant.rs +++ b/crates/assistant/src/assistant.rs @@ -5,7 +5,6 @@ pub mod assistant_settings; mod context; pub mod context_store; mod inline_assistant; -mod model_selector; mod patch; mod prompt_library; mod prompts; @@ -37,7 +36,6 @@ pub(crate) use inline_assistant::*; use language_model::{ LanguageModelId, LanguageModelProviderId, LanguageModelRegistry, LanguageModelResponseMessage, }; -pub(crate) use model_selector::*; pub use patch::*; pub use prompts::PromptBuilder; use prompts::PromptLoadingParams; diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index ff60f2b918..9a7beb96d2 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -17,9 +17,9 @@ use crate::{ ContextEvent, ContextId, ContextStore, ContextStoreEvent, CopyCode, CycleMessageRole, DeployHistory, DeployPromptLibrary, Edit, InlineAssistant, InsertDraggedFiles, InsertIntoEditor, InvokedSlashCommandId, InvokedSlashCommandStatus, Message, MessageId, - MessageMetadata, MessageStatus, ModelPickerDelegate, ModelSelector, NewContext, - ParsedSlashCommand, PendingSlashCommandStatus, QuoteSelection, RemoteContextMetadata, - RequestType, SavedContextMetadata, Split, ToggleFocus, ToggleModelSelector, + MessageMetadata, MessageStatus, NewContext, ParsedSlashCommand, PendingSlashCommandStatus, + QuoteSelection, RemoteContextMetadata, RequestType, SavedContextMetadata, Split, ToggleFocus, + ToggleModelSelector, }; use anyhow::Result; use assistant_slash_command::{SlashCommand, SlashCommandOutputSection}; @@ -55,6 +55,7 @@ use language_model::{ LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, Role, ZED_CLOUD_PROVIDER_ID, }; +use language_model_selector::{LanguageModelPickerDelegate, LanguageModelSelector}; use multi_buffer::MultiBufferRow; use picker::{Picker, PickerDelegate}; use project::lsp_store::LocalLspAdapterDelegate; @@ -142,7 +143,7 @@ pub struct AssistantPanel { languages: Arc, fs: Arc, subscriptions: Vec, - model_selector_menu_handle: PopoverMenuHandle>, + model_selector_menu_handle: PopoverMenuHandle>, model_summary_editor: View, authenticate_provider_task: Option<(LanguageModelProviderId, Task<()>)>, configuration_subscription: Option, @@ -4457,13 +4458,13 @@ pub struct ContextEditorToolbarItem { fs: Arc, active_context_editor: Option>, model_summary_editor: View, - model_selector_menu_handle: PopoverMenuHandle>, + model_selector_menu_handle: PopoverMenuHandle>, } impl ContextEditorToolbarItem { pub fn new( workspace: &Workspace, - model_selector_menu_handle: PopoverMenuHandle>, + model_selector_menu_handle: PopoverMenuHandle>, model_summary_editor: View, ) -> Self { Self { @@ -4559,8 +4560,17 @@ impl Render for ContextEditorToolbarItem { // .map(|remaining_items| format!("Files to scan: {}", remaining_items)) // }) .child( - ModelSelector::new( - self.fs.clone(), + LanguageModelSelector::new( + { + let fs = self.fs.clone(); + move |model, cx| { + update_settings_file::( + fs.clone(), + cx, + move |settings, _| settings.set_model(model.clone()), + ); + } + }, ButtonLike::new("active-model") .style(ButtonStyle::Subtle) .child( diff --git a/crates/assistant/src/inline_assistant.rs b/crates/assistant/src/inline_assistant.rs index 855972c267..b1cb1d81b4 100644 --- a/crates/assistant/src/inline_assistant.rs +++ b/crates/assistant/src/inline_assistant.rs @@ -1,7 +1,7 @@ use crate::{ assistant_settings::AssistantSettings, humanize_token_count, prompts::PromptBuilder, AssistantPanel, AssistantPanelEvent, CharOperation, CycleNextInlineAssist, - CyclePreviousInlineAssist, LineDiff, LineOperation, ModelSelector, RequestType, StreamingDiff, + CyclePreviousInlineAssist, LineDiff, LineOperation, RequestType, StreamingDiff, }; use anyhow::{anyhow, Context as _, Result}; use client::{telemetry::Telemetry, ErrorExt}; @@ -33,12 +33,13 @@ use language_model::{ LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, LanguageModelTextStream, Role, }; +use language_model_selector::LanguageModelSelector; use language_models::report_assistant_event; use multi_buffer::MultiBufferRow; use parking_lot::Mutex; use project::{CodeAction, ProjectTransaction}; use rope::Rope; -use settings::{Settings, SettingsStore}; +use settings::{update_settings_file, Settings, SettingsStore}; use smol::future::FutureExt; use std::{ cmp, @@ -1500,8 +1501,17 @@ impl Render for PromptEditor { .justify_center() .gap_2() .child( - ModelSelector::new( - self.fs.clone(), + LanguageModelSelector::new( + { + let fs = self.fs.clone(); + move |model, cx| { + update_settings_file::( + fs.clone(), + cx, + move |settings, _| settings.set_model(model.clone()), + ); + } + }, IconButton::new("context", IconName::SettingsAlt) .shape(IconButtonShape::Square) .icon_size(IconSize::Small) @@ -1521,7 +1531,7 @@ impl Render for PromptEditor { ) }), ) - .with_info_text( + .info_text( "Inline edits use context\n\ from the currently selected\n\ assistant panel tab.", diff --git a/crates/assistant/src/terminal_inline_assistant.rs b/crates/assistant/src/terminal_inline_assistant.rs index 51738b90e4..a5424a8d7e 100644 --- a/crates/assistant/src/terminal_inline_assistant.rs +++ b/crates/assistant/src/terminal_inline_assistant.rs @@ -1,6 +1,7 @@ +use crate::assistant_settings::AssistantSettings; use crate::{ - humanize_token_count, prompts::PromptBuilder, AssistantPanel, AssistantPanelEvent, - ModelSelector, RequestType, DEFAULT_CONTEXT_LINES, + humanize_token_count, prompts::PromptBuilder, AssistantPanel, AssistantPanelEvent, RequestType, + DEFAULT_CONTEXT_LINES, }; use anyhow::{Context as _, Result}; use client::telemetry::Telemetry; @@ -19,8 +20,9 @@ use language::Buffer; use language_model::{ LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role, }; +use language_model_selector::LanguageModelSelector; use language_models::report_assistant_event; -use settings::Settings; +use settings::{update_settings_file, Settings}; use std::{ cmp, sync::Arc, @@ -612,8 +614,17 @@ impl Render for PromptEditor { .w_12() .justify_center() .gap_2() - .child(ModelSelector::new( - self.fs.clone(), + .child(LanguageModelSelector::new( + { + let fs = self.fs.clone(); + move |model, cx| { + update_settings_file::( + fs.clone(), + cx, + move |settings, _| settings.set_model(model.clone()), + ); + } + }, IconButton::new("context", IconName::SettingsAlt) .shape(IconButtonShape::Square) .icon_size(IconSize::Small) diff --git a/crates/language_model_selector/Cargo.toml b/crates/language_model_selector/Cargo.toml new file mode 100644 index 0000000000..cd00af50c0 --- /dev/null +++ b/crates/language_model_selector/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "language_model_selector" +version = "0.1.0" +edition = "2021" +publish = false +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/language_model_selector.rs" + +[dependencies] +feature_flags.workspace = true +gpui.workspace = true +language_model.workspace = true +picker.workspace = true +proto.workspace = true +ui.workspace = true +workspace.workspace = true +zed_actions.workspace = true diff --git a/crates/language_model_selector/LICENSE-GPL b/crates/language_model_selector/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/language_model_selector/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/assistant/src/model_selector.rs b/crates/language_model_selector/src/language_model_selector.rs similarity index 90% rename from crates/assistant/src/model_selector.rs rename to crates/language_model_selector/src/language_model_selector.rs index 1b26b8b5ad..562bccbd88 100644 --- a/crates/assistant/src/model_selector.rs +++ b/crates/language_model_selector/src/language_model_selector.rs @@ -1,30 +1,27 @@ -use feature_flags::ZedPro; - -use language_model::{LanguageModel, LanguageModelAvailability, LanguageModelRegistry}; -use proto::Plan; -use workspace::ShowConfiguration; - use std::sync::Arc; -use crate::assistant_settings::AssistantSettings; -use fs::Fs; -use gpui::{Action, AnyElement, DismissEvent, SharedString, Task}; +use feature_flags::ZedPro; +use gpui::{Action, AnyElement, AppContext, DismissEvent, SharedString, Task}; +use language_model::{LanguageModel, LanguageModelAvailability, LanguageModelRegistry}; use picker::{Picker, PickerDelegate}; -use settings::update_settings_file; +use proto::Plan; use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverMenuHandle, PopoverTrigger}; +use workspace::ShowConfiguration; const TRY_ZED_PRO_URL: &str = "https://zed.dev/pro"; +type OnModelChanged = Arc, &AppContext) + 'static>; + #[derive(IntoElement)] -pub struct ModelSelector { - handle: Option>>, - fs: Arc, +pub struct LanguageModelSelector { + handle: Option>>, + on_model_changed: OnModelChanged, trigger: T, info_text: Option, } -pub struct ModelPickerDelegate { - fs: Arc, +pub struct LanguageModelPickerDelegate { + on_model_changed: OnModelChanged, all_models: Vec, filtered_models: Vec, selected_index: usize, @@ -38,28 +35,34 @@ struct ModelInfo { is_selected: bool, } -impl ModelSelector { - pub fn new(fs: Arc, trigger: T) -> Self { - ModelSelector { +impl LanguageModelSelector { + pub fn new( + on_model_changed: impl Fn(Arc, &AppContext) + 'static, + trigger: T, + ) -> Self { + LanguageModelSelector { handle: None, - fs, + on_model_changed: Arc::new(on_model_changed), trigger, info_text: None, } } - pub fn with_handle(mut self, handle: PopoverMenuHandle>) -> Self { + pub fn with_handle( + mut self, + handle: PopoverMenuHandle>, + ) -> Self { self.handle = Some(handle); self } - pub fn with_info_text(mut self, text: impl Into) -> Self { + pub fn info_text(mut self, text: impl Into) -> Self { self.info_text = Some(text.into()); self } } -impl PickerDelegate for ModelPickerDelegate { +impl PickerDelegate for LanguageModelPickerDelegate { type ListItem = ListItem; fn match_count(&self) -> usize { @@ -137,9 +140,7 @@ impl PickerDelegate for ModelPickerDelegate { fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext>) { if let Some(model_info) = self.filtered_models.get(self.selected_index) { let model = model_info.model.clone(); - update_settings_file::(self.fs.clone(), cx, move |settings, _| { - settings.set_model(model.clone()) - }); + (self.on_model_changed)(model.clone(), cx); // Update the selection status let selected_model_id = model_info.model.id(); @@ -296,7 +297,7 @@ impl PickerDelegate for ModelPickerDelegate { } } -impl RenderOnce for ModelSelector { +impl RenderOnce for LanguageModelSelector { fn render(self, cx: &mut WindowContext) -> impl IntoElement { let selected_provider = LanguageModelRegistry::read_global(cx) .active_provider() @@ -331,8 +332,8 @@ impl RenderOnce for ModelSelector { }) .collect::>(); - let delegate = ModelPickerDelegate { - fs: self.fs.clone(), + let delegate = LanguageModelPickerDelegate { + on_model_changed: self.on_model_changed.clone(), all_models: all_models.clone(), filtered_models: all_models, selected_index: 0, From 2a23db6e052e1262b65f57d3a88ec732beda1337 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Sat, 23 Nov 2024 12:46:11 -0500 Subject: [PATCH 127/886] assistant2: Sketch in toolbar (#21114) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR sketches in the toolbar for `assistant2`. Screenshot 2024-11-23 at 12 39 49 PM Release Notes: - N/A --- Cargo.lock | 2 + crates/assistant2/Cargo.toml | 2 + crates/assistant2/src/assistant.rs | 2 +- crates/assistant2/src/assistant_panel.rs | 127 ++++++++++++++++++++++- 4 files changed, 127 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cb2e662311..2a5c4d94bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -458,6 +458,8 @@ dependencies = [ "command_palette_hooks", "feature_flags", "gpui", + "language_model", + "language_model_selector", "proto", "ui", "workspace", diff --git a/crates/assistant2/Cargo.toml b/crates/assistant2/Cargo.toml index 320cd015e2..8e3405c340 100644 --- a/crates/assistant2/Cargo.toml +++ b/crates/assistant2/Cargo.toml @@ -17,6 +17,8 @@ anyhow.workspace = true command_palette_hooks.workspace = true feature_flags.workspace = true gpui.workspace = true +language_model.workspace = true +language_model_selector.workspace = true proto.workspace = true ui.workspace = true workspace.workspace = true diff --git a/crates/assistant2/src/assistant.rs b/crates/assistant2/src/assistant.rs index 31676198ba..da1038f2ca 100644 --- a/crates/assistant2/src/assistant.rs +++ b/crates/assistant2/src/assistant.rs @@ -6,7 +6,7 @@ use gpui::{actions, AppContext}; pub use crate::assistant_panel::AssistantPanel; -actions!(assistant2, [ToggleFocus, NewChat]); +actions!(assistant2, [ToggleFocus, NewChat, ToggleModelSelector]); const NAMESPACE: &str = "assistant2"; diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index 7c586dd35e..51bfb8c4ff 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -3,11 +3,13 @@ use gpui::{ prelude::*, px, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusableView, Pixels, Task, View, ViewContext, WeakView, WindowContext, }; -use ui::prelude::*; +use language_model::LanguageModelRegistry; +use language_model_selector::LanguageModelSelector; +use ui::{prelude::*, ButtonLike, Divider, IconButtonShape, Tab, Tooltip}; use workspace::dock::{DockPosition, Panel, PanelEvent}; use workspace::{Pane, Workspace}; -use crate::{NewChat, ToggleFocus}; +use crate::{NewChat, ToggleFocus, ToggleModelSelector}; pub fn init(cx: &mut AppContext) { cx.observe_new_views( @@ -116,8 +118,123 @@ impl Panel for AssistantPanel { } } -impl Render for AssistantPanel { - fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { - div().child(Label::new("Assistant II")) +impl AssistantPanel { + fn render_toolbar(&self, cx: &mut ViewContext) -> impl IntoElement { + let focus_handle = self.focus_handle(cx); + + h_flex() + .id("assistant-toolbar") + .justify_between() + .gap(DynamicSpacing::Base08.rems(cx)) + .h(Tab::container_height(cx)) + .px(DynamicSpacing::Base08.rems(cx)) + .bg(cx.theme().colors().tab_bar_background) + .border_b_1() + .border_color(cx.theme().colors().border_variant) + .child(h_flex().child(Label::new("Chat Title Goes Here"))) + .child( + h_flex() + .gap(DynamicSpacing::Base08.rems(cx)) + .child(self.render_language_model_selector(cx)) + .child(Divider::vertical()) + .child( + IconButton::new("new-chat", IconName::Plus) + .shape(IconButtonShape::Square) + .icon_size(IconSize::Small) + .style(ButtonStyle::Subtle) + .tooltip({ + let focus_handle = focus_handle.clone(); + move |cx| { + Tooltip::for_action_in("New Chat", &NewChat, &focus_handle, cx) + } + }) + .on_click(move |_event, _cx| { + println!("New Chat"); + }), + ) + .child( + IconButton::new("open-history", IconName::HistoryRerun) + .shape(IconButtonShape::Square) + .icon_size(IconSize::Small) + .style(ButtonStyle::Subtle) + .tooltip(move |cx| Tooltip::text("Open History", cx)) + .on_click(move |_event, _cx| { + println!("Open History"); + }), + ) + .child( + IconButton::new("configure-assistant", IconName::Settings) + .shape(IconButtonShape::Square) + .icon_size(IconSize::Small) + .style(ButtonStyle::Subtle) + .tooltip(move |cx| Tooltip::text("Configure Assistant", cx)) + .on_click(move |_event, _cx| { + println!("Configure Assistant"); + }), + ), + ) + } + + fn render_language_model_selector(&self, cx: &mut ViewContext) -> impl IntoElement { + let active_provider = LanguageModelRegistry::read_global(cx).active_provider(); + let active_model = LanguageModelRegistry::read_global(cx).active_model(); + + LanguageModelSelector::new( + |model, _cx| { + println!("Selected {:?}", model.name()); + }, + ButtonLike::new("active-model") + .style(ButtonStyle::Subtle) + .child( + h_flex() + .w_full() + .gap_0p5() + .child( + div() + .overflow_x_hidden() + .flex_grow() + .whitespace_nowrap() + .child(match (active_provider, active_model) { + (Some(provider), Some(model)) => h_flex() + .gap_1() + .child( + Icon::new( + model.icon().unwrap_or_else(|| provider.icon()), + ) + .color(Color::Muted) + .size(IconSize::XSmall), + ) + .child( + Label::new(model.name().0) + .size(LabelSize::Small) + .color(Color::Muted), + ) + .into_any_element(), + _ => Label::new("No model selected") + .size(LabelSize::Small) + .color(Color::Muted) + .into_any_element(), + }), + ) + .child( + Icon::new(IconName::ChevronDown) + .color(Color::Muted) + .size(IconSize::XSmall), + ), + ) + .tooltip(move |cx| Tooltip::for_action("Change Model", &ToggleModelSelector, cx)), + ) + } +} + +impl Render for AssistantPanel { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + v_flex() + .key_context("AssistantPanel2") + .size_full() + .on_action(cx.listener(|_this, _: &NewChat, _cx| { + println!("Action: New Chat"); + })) + .child(self.render_toolbar(cx)) } } From 628b96f2972aaeb0ba8b10ecb3b8ee76473db619 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Sat, 23 Nov 2024 14:09:15 -0500 Subject: [PATCH 128/886] assistant2: Sketch in chat editor (#21116) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR sketches in the chat editor for `assistant2`. Screenshot 2024-11-23 at 1 56 23 PM Release Notes: - N/A --- Cargo.lock | 3 + crates/assistant2/Cargo.toml | 3 + crates/assistant2/src/assistant.rs | 1 + crates/assistant2/src/assistant_panel.rs | 15 ++++- crates/assistant2/src/chat_editor.rs | 76 ++++++++++++++++++++++++ 5 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 crates/assistant2/src/chat_editor.rs diff --git a/Cargo.lock b/Cargo.lock index 2a5c4d94bb..91205f214f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -456,11 +456,14 @@ version = "0.1.0" dependencies = [ "anyhow", "command_palette_hooks", + "editor", "feature_flags", "gpui", "language_model", "language_model_selector", "proto", + "settings", + "theme", "ui", "workspace", ] diff --git a/crates/assistant2/Cargo.toml b/crates/assistant2/Cargo.toml index 8e3405c340..9dd605d559 100644 --- a/crates/assistant2/Cargo.toml +++ b/crates/assistant2/Cargo.toml @@ -15,10 +15,13 @@ doctest = false [dependencies] anyhow.workspace = true command_palette_hooks.workspace = true +editor.workspace = true feature_flags.workspace = true gpui.workspace = true language_model.workspace = true language_model_selector.workspace = true proto.workspace = true +settings.workspace = true +theme.workspace = true ui.workspace = true workspace.workspace = true diff --git a/crates/assistant2/src/assistant.rs b/crates/assistant2/src/assistant.rs index da1038f2ca..f8284d9ff5 100644 --- a/crates/assistant2/src/assistant.rs +++ b/crates/assistant2/src/assistant.rs @@ -1,4 +1,5 @@ mod assistant_panel; +mod chat_editor; use command_palette_hooks::CommandPaletteFilter; use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt}; diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index 51bfb8c4ff..2fa08d7f5e 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -9,6 +9,7 @@ use ui::{prelude::*, ButtonLike, Divider, IconButtonShape, Tab, Tooltip}; use workspace::dock::{DockPosition, Panel, PanelEvent}; use workspace::{Pane, Workspace}; +use crate::chat_editor::ChatEditor; use crate::{NewChat, ToggleFocus, ToggleModelSelector}; pub fn init(cx: &mut AppContext) { @@ -24,6 +25,7 @@ pub fn init(cx: &mut AppContext) { pub struct AssistantPanel { pane: View, + chat_editor: View, } impl AssistantPanel { @@ -54,7 +56,10 @@ impl AssistantPanel { pane }); - Self { pane } + Self { + pane, + chat_editor: cx.new_view(ChatEditor::new), + } } } @@ -231,10 +236,18 @@ impl Render for AssistantPanel { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { v_flex() .key_context("AssistantPanel2") + .justify_between() .size_full() .on_action(cx.listener(|_this, _: &NewChat, _cx| { println!("Action: New Chat"); })) .child(self.render_toolbar(cx)) + .child(v_flex().bg(cx.theme().colors().panel_background)) + .child( + h_flex() + .border_t_1() + .border_color(cx.theme().colors().border_variant) + .child(self.chat_editor.clone()), + ) } } diff --git a/crates/assistant2/src/chat_editor.rs b/crates/assistant2/src/chat_editor.rs new file mode 100644 index 0000000000..9111f57eac --- /dev/null +++ b/crates/assistant2/src/chat_editor.rs @@ -0,0 +1,76 @@ +use editor::{Editor, EditorElement, EditorStyle}; +use gpui::{TextStyle, View}; +use settings::Settings; +use theme::ThemeSettings; +use ui::prelude::*; + +pub struct ChatEditor { + editor: View, +} + +impl ChatEditor { + pub fn new(cx: &mut ViewContext) -> Self { + Self { + editor: cx.new_view(|cx| { + let mut editor = Editor::auto_height(80, cx); + editor.set_placeholder_text("Ask anything…", cx); + + editor + }), + } + } +} + +impl Render for ChatEditor { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let font_size = TextSize::Default.rems(cx); + let line_height = font_size.to_pixels(cx.rem_size()) * 1.3; + + v_flex() + .size_full() + .gap_2() + .p_2() + .bg(cx.theme().colors().editor_background) + .child({ + let settings = ThemeSettings::get_global(cx); + let text_style = TextStyle { + color: cx.theme().colors().editor_foreground, + font_family: settings.ui_font.family.clone(), + font_features: settings.ui_font.features.clone(), + font_size: font_size.into(), + font_weight: settings.ui_font.weight, + line_height: line_height.into(), + ..Default::default() + }; + + EditorElement::new( + &self.editor, + EditorStyle { + background: cx.theme().colors().editor_background, + local_player: cx.theme().players().local(), + text: text_style, + ..Default::default() + }, + ) + }) + .child( + h_flex() + .justify_between() + .child( + h_flex().child( + Button::new("add-context", "Add Context") + .style(ButtonStyle::Filled) + .icon(IconName::Plus) + .icon_position(IconPosition::Start), + ), + ) + .child( + h_flex() + .gap_2() + .child(Button::new("codebase", "Codebase").style(ButtonStyle::Filled)) + .child(Label::new("or")) + .child(Button::new("chat", "Chat").style(ButtonStyle::Filled)), + ), + ) + } +} From 0395d1b03756dd305eae93b5fa484a9f7734de20 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Sat, 23 Nov 2024 23:11:45 -0500 Subject: [PATCH 129/886] Clean up app event transformations (#21115) This needs scrutinized. Detailed breakdown of what events I kept and threw out here: https://zed.dev/channel/app-events-17178/notes I also removed a few fake events and tossed out json properties that were being inserted for things we don't have logic to track. See PR review comments below. I think the only bad data we have are that we were identifying all node, pnpm, and yarn projects as 'node' in the `project_type` property, so a few days of lost data there. Release Notes: - N/A --- crates/collab/src/api/events.rs | 93 ++++++++++++++++----------------- 1 file changed, 46 insertions(+), 47 deletions(-) diff --git a/crates/collab/src/api/events.rs b/crates/collab/src/api/events.rs index 95bd2a89b2..68325b17aa 100644 --- a/crates/collab/src/api/events.rs +++ b/crates/collab/src/api/events.rs @@ -1442,65 +1442,64 @@ fn for_snowflake( Event::App(e) => { let mut properties = json!({}); let event_type = match e.operation.trim() { - "extensions: install extension" => "Extension Installed".to_string(), + // App "open" => "App Opened".to_string(), - "project search: open" => "Project Search Opened".to_string(), - "first open" => { - properties["is_first_open"] = json!(true); - "App First Opened".to_string() + "first open" => "App First Opened".to_string(), + "first open for release channel" => { + "App First Opened For Release Channel".to_string() } - "extensions: uninstall extension" => "Extension Uninstalled".to_string(), - "welcome page: close" => "Welcome Page Closed".to_string(), - "open project" => { - properties["is_first_time"] = json!(false); + "close" => "App Closed".to_string(), + + // Project + "open project" => "Project Opened".to_string(), + "open node project" => { + properties["project_type"] = json!("node"); "Project Opened".to_string() } - "welcome page: install cli" => "CLI Installed".to_string(), - "project diagnostics: open" => "Project Diagnostics Opened".to_string(), - "extensions page: open" => "Extensions Page Opened".to_string(), - "welcome page: change theme" => "Welcome Theme Changed".to_string(), - "welcome page: toggle metric telemetry" => { - properties["enabled"] = json!(false); - "Welcome Telemetry Toggled".to_string() + "open pnpm project" => { + properties["project_type"] = json!("pnpm"); + "Project Opened".to_string() } + "open yarn project" => { + properties["project_type"] = json!("yarn"); + "Project Opened".to_string() + } + + // SSH + "create ssh server" => "SSH Server Created".to_string(), + "create ssh project" => "SSH Project Created".to_string(), + "open ssh project" => "SSH Project Opened".to_string(), + + // Welcome Page "welcome page: change keymap" => "Keymap Changed".to_string(), - "welcome page: toggle vim" => { - properties["enabled"] = json!(false); - "Welcome Vim Mode Toggled".to_string() - } + "welcome page: change theme" => "Welcome Theme Changed".to_string(), + "welcome page: close" => "Welcome Page Closed".to_string(), + "welcome page: edit settings" => "Settings Edited".to_string(), + "welcome page: install cli" => "CLI Installed".to_string(), + "welcome page: open" => "Welcome Page Opened".to_string(), + "welcome page: open extensions" => "Extensions Page Opened".to_string(), "welcome page: sign in to copilot" => "Welcome Copilot Signed In".to_string(), "welcome page: toggle diagnostic telemetry" => { "Welcome Telemetry Toggled".to_string() } - "welcome page: open" => "Welcome Page Opened".to_string(), - "close" => "App Closed".to_string(), - "markdown preview: open" => "Markdown Preview Opened".to_string(), - "welcome page: open extensions" => "Extensions Page Opened".to_string(), - "open node project" | "open pnpm project" | "open yarn project" => { - properties["project_type"] = json!("node"); - properties["is_first_time"] = json!(false); - "Project Opened".to_string() - } - "repl sessions: open" => "REPL Session Started".to_string(), - "welcome page: toggle helix" => { - properties["enabled"] = json!(false); - "Helix Mode Toggled".to_string() - } - "welcome page: edit settings" => { - properties["changed_settings"] = json!([]); - "Settings Edited".to_string() + "welcome page: toggle metric telemetry" => { + "Welcome Telemetry Toggled".to_string() } + "welcome page: toggle vim" => "Welcome Vim Mode Toggled".to_string(), "welcome page: view docs" => "Documentation Viewed".to_string(), - "open ssh project" => { - properties["is_first_time"] = json!(false); - "SSH Project Opened".to_string() - } - "create ssh server" => "SSH Server Created".to_string(), - "create ssh project" => "SSH Project Created".to_string(), - "first open for release channel" => { - properties["is_first_for_channel"] = json!(true); - "App First Opened For Release Channel".to_string() - } + + // Extensions + "extensions page: open" => "Extensions Page Opened".to_string(), + "extensions: install extension" => "Extension Installed".to_string(), + "extensions: uninstall extension" => "Extension Uninstalled".to_string(), + + // Misc + "markdown preview: open" => "Markdown Preview Opened".to_string(), + "project diagnostics: open" => "Project Diagnostics Opened".to_string(), + "project search: open" => "Project Search Opened".to_string(), + "repl sessions: open" => "REPL Session Started".to_string(), + + // Feature Upsell "feature upsell: toggle vim" => { properties["source"] = json!("Feature Upsell"); "Vim Mode Toggled".to_string() From 3dcb94c204588c112c5c14a1572295e297c847a7 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Sun, 24 Nov 2024 00:34:02 -0500 Subject: [PATCH 130/886] Correct more app event inconsistencies (#21129) - Unify welcome page event type string structure - Differentiate between metric telemetry event and diagnostic telemetry event Release Notes: - N/A --- crates/collab/src/api/events.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/collab/src/api/events.rs b/crates/collab/src/api/events.rs index 68325b17aa..b5cd920fb3 100644 --- a/crates/collab/src/api/events.rs +++ b/crates/collab/src/api/events.rs @@ -1471,22 +1471,22 @@ fn for_snowflake( "open ssh project" => "SSH Project Opened".to_string(), // Welcome Page - "welcome page: change keymap" => "Keymap Changed".to_string(), + "welcome page: change keymap" => "Welcome Keymap Changed".to_string(), "welcome page: change theme" => "Welcome Theme Changed".to_string(), "welcome page: close" => "Welcome Page Closed".to_string(), - "welcome page: edit settings" => "Settings Edited".to_string(), - "welcome page: install cli" => "CLI Installed".to_string(), + "welcome page: edit settings" => "Welcome Settings Edited".to_string(), + "welcome page: install cli" => "Welcome CLI Installed".to_string(), "welcome page: open" => "Welcome Page Opened".to_string(), - "welcome page: open extensions" => "Extensions Page Opened".to_string(), + "welcome page: open extensions" => "Welcome Extensions Page Opened".to_string(), "welcome page: sign in to copilot" => "Welcome Copilot Signed In".to_string(), "welcome page: toggle diagnostic telemetry" => { - "Welcome Telemetry Toggled".to_string() + "Welcome Diagnostic Telemetry Toggled".to_string() } "welcome page: toggle metric telemetry" => { - "Welcome Telemetry Toggled".to_string() + "Welcome Metric Telemetry Toggled".to_string() } "welcome page: toggle vim" => "Welcome Vim Mode Toggled".to_string(), - "welcome page: view docs" => "Documentation Viewed".to_string(), + "welcome page: view docs" => "Welcome Documentation Viewed".to_string(), // Extensions "extensions page: open" => "Extensions Page Opened".to_string(), From 20bffaf93f3598d129ab654493bb866af33a6152 Mon Sep 17 00:00:00 2001 From: Carroll Wainwright Date: Sun, 24 Nov 2024 15:52:11 -0800 Subject: [PATCH 131/886] python: Highlight docstrings for classes and modules (#20486) Release Notes: - Add `string.doc` python syntax highlighting to class and module-level docstrings. Previously, only docstrings inside python functions were labeled as `string.doc`, but docstrings can exist at the class or module level too. This adds the more specific string type for each of those. *Before*: image *After*: image --- crates/languages/src/python/highlights.scm | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/crates/languages/src/python/highlights.scm b/crates/languages/src/python/highlights.scm index 6c3f027c19..98ed203969 100644 --- a/crates/languages/src/python/highlights.scm +++ b/crates/languages/src/python/highlights.scm @@ -96,7 +96,16 @@ "def" name: (_) (parameters)? - body: (block (expression_statement (string) @string.doc))) + body: (block . (expression_statement (string) @string.doc))) + +(class_definition + body: (block + . (comment) @comment* + . (expression_statement (string) @string.doc))) + +(module + . (comment) @comment* + . (expression_statement (string) @string.doc)) (module (expression_statement (assignment)) From e85848a69508d901fc33f8d8883e1f7c356fb8a2 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 25 Nov 2024 00:54:47 +0100 Subject: [PATCH 132/886] pylsp: Prefer version from user venv (#21069) Closes #ISSUE Release Notes: - pylsp will now use version installed in user venv, if one is available. --- crates/language/src/language.rs | 9 ++++- .../src/extension_lsp_adapter.rs | 1 + crates/languages/src/c.rs | 1 + crates/languages/src/go.rs | 1 + crates/languages/src/python.rs | 37 +++++++++---------- crates/languages/src/rust.rs | 1 + crates/languages/src/vtsls.rs | 1 + crates/project/src/lsp_store.rs | 9 ++++- 8 files changed, 37 insertions(+), 23 deletions(-) diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 580955a98b..58be8a4dc3 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -201,13 +201,14 @@ impl CachedLspAdapter { pub async fn get_language_server_command( self: Arc, delegate: Arc, + toolchains: Arc, binary_options: LanguageServerBinaryOptions, cx: &mut AsyncAppContext, ) -> Result { let cached_binary = self.cached_binary.lock().await; self.adapter .clone() - .get_language_server_command(delegate, binary_options, cached_binary, cx) + .get_language_server_command(delegate, toolchains, binary_options, cached_binary, cx) .await } @@ -281,6 +282,7 @@ pub trait LspAdapter: 'static + Send + Sync { fn get_language_server_command<'a>( self: Arc, delegate: Arc, + toolchains: Arc, binary_options: LanguageServerBinaryOptions, mut cached_binary: futures::lock::MutexGuard<'a, Option>, cx: &'a mut AsyncAppContext, @@ -298,7 +300,7 @@ pub trait LspAdapter: 'static + Send + Sync { // because we don't want to download and overwrite our global one // for each worktree we might have open. if binary_options.allow_path_lookup { - if let Some(binary) = self.check_if_user_installed(delegate.as_ref(), cx).await { + if let Some(binary) = self.check_if_user_installed(delegate.as_ref(), toolchains, cx).await { log::info!( "found user-installed language server for {}. path: {:?}, arguments: {:?}", self.name().0, @@ -357,6 +359,7 @@ pub trait LspAdapter: 'static + Send + Sync { async fn check_if_user_installed( &self, _: &dyn LspAdapterDelegate, + _: Arc, _: &AsyncAppContext, ) -> Option { None @@ -1665,6 +1668,7 @@ impl LspAdapter for FakeLspAdapter { async fn check_if_user_installed( &self, _: &dyn LspAdapterDelegate, + _: Arc, _: &AsyncAppContext, ) -> Option { Some(self.language_server_binary.clone()) @@ -1673,6 +1677,7 @@ impl LspAdapter for FakeLspAdapter { fn get_language_server_command<'a>( self: Arc, _: Arc, + _: Arc, _: LanguageServerBinaryOptions, _: futures::lock::MutexGuard<'a, Option>, _: &'a mut AsyncAppContext, diff --git a/crates/language_extension/src/extension_lsp_adapter.rs b/crates/language_extension/src/extension_lsp_adapter.rs index eab9529fe0..3286e09e2d 100644 --- a/crates/language_extension/src/extension_lsp_adapter.rs +++ b/crates/language_extension/src/extension_lsp_adapter.rs @@ -115,6 +115,7 @@ impl LspAdapter for ExtensionLspAdapter { fn get_language_server_command<'a>( self: Arc, delegate: Arc, + _: Arc, _: LanguageServerBinaryOptions, _: futures::lock::MutexGuard<'a, Option>, _: &'a mut AsyncAppContext, diff --git a/crates/languages/src/c.rs b/crates/languages/src/c.rs index 5bfb7f0bc2..8d0369f0e0 100644 --- a/crates/languages/src/c.rs +++ b/crates/languages/src/c.rs @@ -24,6 +24,7 @@ impl super::LspAdapter for CLspAdapter { async fn check_if_user_installed( &self, delegate: &dyn LspAdapterDelegate, + _: Arc, _: &AsyncAppContext, ) -> Option { let path = delegate.which(Self::SERVER_NAME.as_ref()).await?; diff --git a/crates/languages/src/go.rs b/crates/languages/src/go.rs index b3073d7eaa..6e2b5d464e 100644 --- a/crates/languages/src/go.rs +++ b/crates/languages/src/go.rs @@ -67,6 +67,7 @@ impl super::LspAdapter for GoLspAdapter { async fn check_if_user_installed( &self, delegate: &dyn LspAdapterDelegate, + _: Arc, _: &AsyncAppContext, ) -> Option { let path = delegate.which(Self::SERVER_NAME.as_ref()).await?; diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 2cedd704cf..8736a12942 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -79,6 +79,7 @@ impl LspAdapter for PythonLspAdapter { async fn check_if_user_installed( &self, delegate: &dyn LspAdapterDelegate, + _: Arc, _: &AsyncAppContext, ) -> Option { let node = delegate.which("node".as_ref()).await?; @@ -753,33 +754,29 @@ impl LspAdapter for PyLspAdapter { async fn check_if_user_installed( &self, - _: &dyn LspAdapterDelegate, - _: &AsyncAppContext, + delegate: &dyn LspAdapterDelegate, + toolchains: Arc, + cx: &AsyncAppContext, ) -> Option { - // We don't support user-provided pylsp, as global packages are discouraged in Python ecosystem. - None + let venv = toolchains + .active_toolchain( + delegate.worktree_id(), + LanguageName::new("Python"), + &mut cx.clone(), + ) + .await?; + let pylsp_path = Path::new(venv.path.as_ref()).parent()?.join("pylsp"); + pylsp_path.exists().then(|| LanguageServerBinary { + path: venv.path.to_string().into(), + arguments: vec![pylsp_path.into()], + env: None, + }) } async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, ) -> Result> { - // let uri = "https://pypi.org/pypi/python-lsp-server/json"; - // let mut root_manifest = delegate - // .http_client() - // .get(&uri, Default::default(), true) - // .await?; - // let mut body = Vec::new(); - // root_manifest.body_mut().read_to_end(&mut body).await?; - // let as_str = String::from_utf8(body)?; - // let json = serde_json::Value::from_str(&as_str)?; - // let latest_version = json - // .get("info") - // .and_then(|info| info.get("version")) - // .and_then(|version| version.as_str().map(ToOwned::to_owned)) - // .ok_or_else(|| { - // anyhow!("PyPI response did not contain version info for python-language-server") - // })?; Ok(Box::new(()) as Box<_>) } diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index 7f5912d73e..25cddae5a6 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -76,6 +76,7 @@ impl LspAdapter for RustLspAdapter { async fn check_if_user_installed( &self, delegate: &dyn LspAdapterDelegate, + _: Arc, _: &AsyncAppContext, ) -> Option { let path = delegate.which("rust-analyzer".as_ref()).await?; diff --git a/crates/languages/src/vtsls.rs b/crates/languages/src/vtsls.rs index 0ad9158003..e44e4e295f 100644 --- a/crates/languages/src/vtsls.rs +++ b/crates/languages/src/vtsls.rs @@ -77,6 +77,7 @@ impl LspAdapter for VtslsLspAdapter { async fn check_if_user_installed( &self, delegate: &dyn LspAdapterDelegate, + _: Arc, _: &AsyncAppContext, ) -> Option { let env = delegate.shell_env().await; diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 29a4c8e71b..cc326285cb 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -5523,10 +5523,16 @@ impl LspStore { .unwrap_or_default(), allow_binary_download, }; + let toolchains = self.toolchain_store(cx); cx.spawn(|_, mut cx| async move { let binary_result = adapter .clone() - .get_language_server_command(delegate.clone(), lsp_binary_options, &mut cx) + .get_language_server_command( + delegate.clone(), + toolchains, + lsp_binary_options, + &mut cx, + ) .await; delegate.update_status(adapter.name.clone(), LanguageServerBinaryStatus::None); @@ -7783,6 +7789,7 @@ impl LspAdapter for SshLspAdapter { async fn check_if_user_installed( &self, _: &dyn LspAdapterDelegate, + _: Arc, _: &AsyncAppContext, ) -> Option { Some(self.binary.clone()) From 5b0fa6e585dd0b3bb4d81813051b1b8931d24371 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 25 Nov 2024 01:48:43 -0700 Subject: [PATCH 133/886] Hide AI hints on line ends so we can discuss more (#21128) @bennetbo @as-cii @mrnugget I'm really not liking the hints about AI on every line. It feels too distracting to me and damaging to the user experience. I'm wondering if we can hide them and work with design for other ideas. Or at least talk it through. Release Notes: - N/A --- assets/settings/default.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 45a211789f..f1071f9676 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -492,7 +492,7 @@ "enabled": true, // Whether to show inline hints showing the keybindings to use the inline assistant and the // assistant panel. - "show_hints": true, + "show_hints": false, // Whether to show the assistant panel button in the status bar. "button": true, // Where to dock the assistant panel. Can be 'left', 'right' or 'bottom'. From aa58cab766a64f79ca8c25ae01b9b9c17ad2d4f0 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 25 Nov 2024 12:21:32 +0100 Subject: [PATCH 134/886] Fix offline workspace deserialization with assistant2 (#21159) Closes #21156 /cc @maxdeviant Release Notes: - N/A --- crates/zed/src/zed.rs | 53 ++++++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 086935542c..be6df49a2d 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -241,25 +241,6 @@ pub fn initialize_workspace( let prompt_builder = prompt_builder.clone(); cx.spawn(|workspace_handle, mut cx| async move { - let is_assistant2_enabled = if cfg!(test) { - false - } else { - let is_assistant2_feature_flag_enabled = assistant2_feature_flag.await; - release_channel == ReleaseChannel::Dev && is_assistant2_feature_flag_enabled - }; - - let (assistant_panel, assistant2_panel) = if is_assistant2_enabled { - let assistant2_panel = - assistant2::AssistantPanel::load(workspace_handle.clone(), cx.clone()).await?; - - (None, Some(assistant2_panel)) - } else { - let assistant_panel = - assistant::AssistantPanel::load(workspace_handle.clone(), prompt_builder, cx.clone()).await?; - - (Some(assistant_panel), None) - }; - let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone()); let outline_panel = OutlinePanel::load(workspace_handle.clone(), cx.clone()); let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone()); @@ -288,6 +269,33 @@ pub fn initialize_workspace( notification_panel, )?; + workspace_handle.update(&mut cx, |workspace, cx| { + workspace.add_panel(project_panel, cx); + workspace.add_panel(outline_panel, cx); + workspace.add_panel(terminal_panel, cx); + workspace.add_panel(channels_panel, cx); + workspace.add_panel(chat_panel, cx); + workspace.add_panel(notification_panel, cx); + })?; + let is_assistant2_enabled = + if cfg!(test) || release_channel != ReleaseChannel::Dev { + false + } else { + assistant2_feature_flag.await + } + ; + + let (assistant_panel, assistant2_panel) = if is_assistant2_enabled { + let assistant2_panel = + assistant2::AssistantPanel::load(workspace_handle.clone(), cx.clone()).await?; + + (None, Some(assistant2_panel)) + } else { + let assistant_panel = + assistant::AssistantPanel::load(workspace_handle.clone(), prompt_builder, cx.clone()).await?; + + (Some(assistant_panel), None) + }; workspace_handle.update(&mut cx, |workspace, cx| { if let Some(assistant_panel) = assistant_panel { workspace.add_panel(assistant_panel, cx); @@ -296,13 +304,6 @@ pub fn initialize_workspace( if let Some(assistant2_panel) = assistant2_panel { workspace.add_panel(assistant2_panel, cx); } - - workspace.add_panel(project_panel, cx); - workspace.add_panel(outline_panel, cx); - workspace.add_panel(terminal_panel, cx); - workspace.add_panel(channels_panel, cx); - workspace.add_panel(chat_panel, cx); - workspace.add_panel(notification_panel, cx); }) }) .detach(); From 08b214dfb9b9f59e2e1fdff6d2112dde33ca61aa Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 25 Nov 2024 05:27:35 -0700 Subject: [PATCH 135/886] Rename 'chat' to 'thread' in assistant2 (#21141) Release Notes: - N/A --- crates/assistant2/src/assistant.rs | 4 +-- crates/assistant2/src/assistant_panel.rs | 29 +++++++++++-------- .../src/{chat_editor.rs => message_editor.rs} | 6 ++-- 3 files changed, 22 insertions(+), 17 deletions(-) rename crates/assistant2/src/{chat_editor.rs => message_editor.rs} (97%) diff --git a/crates/assistant2/src/assistant.rs b/crates/assistant2/src/assistant.rs index f8284d9ff5..6a80186525 100644 --- a/crates/assistant2/src/assistant.rs +++ b/crates/assistant2/src/assistant.rs @@ -1,5 +1,5 @@ mod assistant_panel; -mod chat_editor; +mod message_editor; use command_palette_hooks::CommandPaletteFilter; use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt}; @@ -7,7 +7,7 @@ use gpui::{actions, AppContext}; pub use crate::assistant_panel::AssistantPanel; -actions!(assistant2, [ToggleFocus, NewChat, ToggleModelSelector]); +actions!(assistant2, [ToggleFocus, NewThread, ToggleModelSelector]); const NAMESPACE: &str = "assistant2"; diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index 2fa08d7f5e..890020e54a 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -9,8 +9,8 @@ use ui::{prelude::*, ButtonLike, Divider, IconButtonShape, Tab, Tooltip}; use workspace::dock::{DockPosition, Panel, PanelEvent}; use workspace::{Pane, Workspace}; -use crate::chat_editor::ChatEditor; -use crate::{NewChat, ToggleFocus, ToggleModelSelector}; +use crate::message_editor::MessageEditor; +use crate::{NewThread, ToggleFocus, ToggleModelSelector}; pub fn init(cx: &mut AppContext) { cx.observe_new_views( @@ -25,7 +25,7 @@ pub fn init(cx: &mut AppContext) { pub struct AssistantPanel { pane: View, - chat_editor: View, + message_editor: View, } impl AssistantPanel { @@ -47,7 +47,7 @@ impl AssistantPanel { workspace.project().clone(), Default::default(), None, - NewChat.boxed_clone(), + NewThread.boxed_clone(), cx, ); pane.set_can_split(false, cx); @@ -58,7 +58,7 @@ impl AssistantPanel { Self { pane, - chat_editor: cx.new_view(ChatEditor::new), + message_editor: cx.new_view(MessageEditor::new), } } } @@ -136,25 +136,30 @@ impl AssistantPanel { .bg(cx.theme().colors().tab_bar_background) .border_b_1() .border_color(cx.theme().colors().border_variant) - .child(h_flex().child(Label::new("Chat Title Goes Here"))) + .child(h_flex().child(Label::new("Thread Title Goes Here"))) .child( h_flex() .gap(DynamicSpacing::Base08.rems(cx)) .child(self.render_language_model_selector(cx)) .child(Divider::vertical()) .child( - IconButton::new("new-chat", IconName::Plus) + IconButton::new("new-thread", IconName::Plus) .shape(IconButtonShape::Square) .icon_size(IconSize::Small) .style(ButtonStyle::Subtle) .tooltip({ let focus_handle = focus_handle.clone(); move |cx| { - Tooltip::for_action_in("New Chat", &NewChat, &focus_handle, cx) + Tooltip::for_action_in( + "New Thread", + &NewThread, + &focus_handle, + cx, + ) } }) .on_click(move |_event, _cx| { - println!("New Chat"); + println!("New Thread"); }), ) .child( @@ -238,8 +243,8 @@ impl Render for AssistantPanel { .key_context("AssistantPanel2") .justify_between() .size_full() - .on_action(cx.listener(|_this, _: &NewChat, _cx| { - println!("Action: New Chat"); + .on_action(cx.listener(|_this, _: &NewThread, _cx| { + println!("Action: New Thread"); })) .child(self.render_toolbar(cx)) .child(v_flex().bg(cx.theme().colors().panel_background)) @@ -247,7 +252,7 @@ impl Render for AssistantPanel { h_flex() .border_t_1() .border_color(cx.theme().colors().border_variant) - .child(self.chat_editor.clone()), + .child(self.message_editor.clone()), ) } } diff --git a/crates/assistant2/src/chat_editor.rs b/crates/assistant2/src/message_editor.rs similarity index 97% rename from crates/assistant2/src/chat_editor.rs rename to crates/assistant2/src/message_editor.rs index 9111f57eac..ee25ad5da7 100644 --- a/crates/assistant2/src/chat_editor.rs +++ b/crates/assistant2/src/message_editor.rs @@ -4,11 +4,11 @@ use settings::Settings; use theme::ThemeSettings; use ui::prelude::*; -pub struct ChatEditor { +pub struct MessageEditor { editor: View, } -impl ChatEditor { +impl MessageEditor { pub fn new(cx: &mut ViewContext) -> Self { Self { editor: cx.new_view(|cx| { @@ -21,7 +21,7 @@ impl ChatEditor { } } -impl Render for ChatEditor { +impl Render for MessageEditor { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let font_size = TextSize::Default.rems(cx); let line_height = font_size.to_pixels(cx.rem_size()) * 1.3; From b83f104f6eea872e18ea2599497328ed26a5d8b4 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 25 Nov 2024 15:58:45 +0200 Subject: [PATCH 136/886] Do not reuse render cache for nested items whose parents are re-rendered (#21165) Fixes a bug with terminal splits panicking during writing a command in the command input Release Notes: - N/A Co-authored-by: Antonio Scandurra --- crates/gpui/src/view.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/gpui/src/view.rs b/crates/gpui/src/view.rs index 7f10eb25c3..4f35413a27 100644 --- a/crates/gpui/src/view.rs +++ b/crates/gpui/src/view.rs @@ -7,6 +7,7 @@ use crate::{ }; use anyhow::{Context, Result}; use refineable::Refineable; +use std::mem; use std::{ any::{type_name, TypeId}, fmt, @@ -341,11 +342,13 @@ impl Element for AnyView { } } + let refreshing = mem::replace(&mut cx.window.refreshing, true); let prepaint_start = cx.prepaint_index(); let mut element = (self.render)(self, cx); element.layout_as_root(bounds.size.into(), cx); element.prepaint_at(bounds.origin, cx); let prepaint_end = cx.prepaint_index(); + cx.window.refreshing = refreshing; ( Some(element), @@ -382,7 +385,9 @@ impl Element for AnyView { let paint_start = cx.paint_index(); if let Some(element) = element { + let refreshing = mem::replace(&mut cx.window.refreshing, true); element.paint(cx); + cx.window.refreshing = refreshing; } else { cx.reuse_paint(element_state.paint_range.clone()); } From 385c447bbe7c2fb40fef0296aef7a4d1725fbbf4 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 25 Nov 2024 11:05:14 -0500 Subject: [PATCH 137/886] docs: Document context servers (#21170) This PR adds documentation for context servers. Release Notes: - N/A --- .cloudflare/docs-proxy/src/worker.js | 9 ---- docs/src/SUMMARY.md | 5 +- docs/src/assistant/assistant.md | 2 + docs/src/assistant/context-servers.md | 49 ++++++++++++++++++++ docs/src/assistant/model-context-protocol.md | 21 +++++++++ docs/src/extensions/context-servers.md | 39 ++++++++++++++++ docs/src/extensions/developing-extensions.md | 1 + 7 files changed, 116 insertions(+), 10 deletions(-) create mode 100644 docs/src/assistant/context-servers.md create mode 100644 docs/src/assistant/model-context-protocol.md create mode 100644 docs/src/extensions/context-servers.md diff --git a/.cloudflare/docs-proxy/src/worker.js b/.cloudflare/docs-proxy/src/worker.js index b29ddc00f1..f9f441883a 100644 --- a/.cloudflare/docs-proxy/src/worker.js +++ b/.cloudflare/docs-proxy/src/worker.js @@ -3,15 +3,6 @@ export default { const url = new URL(request.url); url.hostname = "docs-anw.pages.dev"; - // These pages were removed, but may still be served due to Cloudflare's - // [asset retention](https://developers.cloudflare.com/pages/configuration/serving-pages/#asset-retention). - if ( - url.pathname === "/docs/assistant/context-servers" || - url.pathname === "/docs/assistant/model-context-protocol" - ) { - return await fetch("https://zed.dev/404"); - } - let res = await fetch(url, request); if (res.status === 404) { diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index bc7ba52869..d807da8193 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -43,6 +43,8 @@ - [Inline Assistant](./assistant/inline-assistant.md) - [Commands](./assistant/commands.md) - [Prompts](./assistant/prompting.md) +- [Context Servers](./assistant/context-servers.md) + - [Model Context Protocol](./assistant/model-context-protocol.md) # Extensions @@ -51,7 +53,8 @@ - [Developing Extensions](./extensions/developing-extensions.md) - [Language Extensions](./extensions/languages.md) - [Theme Extensions](./extensions/themes.md) -- [Slash Commands](./extensions/slash-commands.md) +- [Slash Command Extensions](./extensions/slash-commands.md) +- [Context Server Extensions](./extensions/context-servers.md) # Language Support diff --git a/docs/src/assistant/assistant.md b/docs/src/assistant/assistant.md index ee4796ec02..94144882f0 100644 --- a/docs/src/assistant/assistant.md +++ b/docs/src/assistant/assistant.md @@ -15,3 +15,5 @@ This section covers various aspects of the Assistant: - [Using Commands](./commands.md): Explore slash commands that enhance the Assistant's capabilities and future extensibility. - [Prompting & Prompt Library](./prompting.md): Learn how to write and save prompts, how to use the Prompt Library, and how to edit prompt templates. + +- [Context Servers](./context-servers.md): Learn about context servers that enhance the Assistant's capabilities via the [Model Context Protocol](./model-context-protocol.md). diff --git a/docs/src/assistant/context-servers.md b/docs/src/assistant/context-servers.md new file mode 100644 index 0000000000..398442044c --- /dev/null +++ b/docs/src/assistant/context-servers.md @@ -0,0 +1,49 @@ +# Context Servers + +Context servers are a mechanism for pulling context into the Assistant from an external source. They are powered by the [Model Context Protocol](./model-context-protocol.md). + +Currently Zed supports context servers providing [slash commands](./commands.md) for use in the Assistant. + +## Installation + +Context servers can be installed via [extensions](../extensions/context-servers.md). + +If you don't already have a context server, check out one of these: + +- [Postgres Context Server](https://github.com/zed-extensions/postgres-context-server) + +## Configuration + +Context servers may require some configuration in order to run or to change their behavior. + +You can configure each context server using the `context_servers` setting in your `settings.json`: + +```json +{ + "context_servers": { + "postgres-context-server": { + "settings": { + "database_url": "postgresql://postgres@localhost/my_database" + } + } +} +``` + +If desired, you may also provide a custom command to execute a context server: + +```json +{ + "context_servers": { + "my-context-server": { + "command": { + "path": "/path/to/my-context-server", + "args": ["run"], + "env": {} + }, + "settings": { + "enable_something": true + } + } + } +} +``` diff --git a/docs/src/assistant/model-context-protocol.md b/docs/src/assistant/model-context-protocol.md new file mode 100644 index 0000000000..34c67a8845 --- /dev/null +++ b/docs/src/assistant/model-context-protocol.md @@ -0,0 +1,21 @@ +# Model Context Protocol + +Zed uses the [Model Context Protocol](https://modelcontextprotocol.io/) to interact with [context servers](./context-server.md): + +> The Model Context Protocol (MCP) is an open protocol that enables seamless integration between LLM applications and external data sources and tools. Whether you're building an AI-powered IDE, enhancing a chat interface, or creating custom AI workflows, MCP provides a standardized way to connect LLMs with the context they need. + +Check out the [Anthropic news post](https://www.anthropic.com/news/model-context-protocol) and the [Zed blog post](https://zed.dev/blog/mcp) for an introduction to MCP. + +## Try it out + +Want to try it for yourself? + +The following context servers are available today as Zed extensions: + +- [Postgres Context Server](https://github.com/zed-extensions/postgres-context-server) + +## Bring your own context server + +If there's an existing context server you'd like to bring to Zed, check out the [context server extension docs](../extensions/context-servers.md) for how to make it available as an extension. + +If you are interested in building your own context server, check out the [Model Context Protocol docs](https://modelcontextprotocol.io/introduction#get-started-with-mcp) to get started. diff --git a/docs/src/extensions/context-servers.md b/docs/src/extensions/context-servers.md new file mode 100644 index 0000000000..6e61987384 --- /dev/null +++ b/docs/src/extensions/context-servers.md @@ -0,0 +1,39 @@ +# Context Servers + +Extensions may provide [context servers](../assistant/context-servers.md) for use in the Assistant. + +## Example extension + +To see a working example of an extension that provides context servers, check out the [`postgres-context-server` extension](https://github.com/zed-extensions/postgres-context-server). + +This extension can be [installed as a dev extension](./developing-extensions.html#developing-an-extension-locally) if you want to try it out for yourself. + +## Defining context servers + +A given extension may provide one or more context servers. Each context server must be registered in the `extension.toml`: + +```toml +[context-servers.my-context-server] +``` + +Then, in the Rust code for your extension, implement the `context_server_command` method on your extension: + +```rust +impl zed::Extension for MyExtension { + fn context_server_command( + &mut self, + context_server_id: &ContextServerId, + project: &zed::Project, + ) -> Result { + Ok(zed::Command { + command: get_path_to_context_server_executable()?, + args: get_args_for_context_server()?, + env: get_env_for_context_server()?, + }) + } +} +``` + +This method should return the command to start up a context server, along with any arguments or environment variables necessary for it to function. + +If you need to download the context server from an external source—like GitHub Releases or npm—you can also do this here. diff --git a/docs/src/extensions/developing-extensions.md b/docs/src/extensions/developing-extensions.md index 36939d4f1e..bdfab5fcde 100644 --- a/docs/src/extensions/developing-extensions.md +++ b/docs/src/extensions/developing-extensions.md @@ -7,6 +7,7 @@ Extensions can add the following capabilities to Zed: - [Languages](./languages.md) - [Themes](./themes.md) - [Slash Commands](./slash-commands.md) +- [Context Servers](./context-servers.md) ## Directory Structure of a Zed Extension From 93533ed2359f09cfa41743854c6f3ae3543ed7cf Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Mon, 25 Nov 2024 17:19:33 +0100 Subject: [PATCH 138/886] Remove assistant hints (#21171) This reverts #20824 and #20899. After adding them last week we came to the conclusion that the hints are too distracting in everyday use, see #21128 for more details. Release Notes: - N/A --- assets/settings/default.json | 3 - crates/assistant/src/assistant_settings.rs | 11 -- crates/editor/src/editor.rs | 44 ------ crates/editor/src/element.rs | 134 ++++++++---------- crates/gpui/src/window.rs | 22 +-- crates/outline_panel/src/outline_panel.rs | 6 +- crates/recent_projects/src/recent_projects.rs | 8 +- crates/zed/src/main.rs | 3 +- crates/zed/src/zed.rs | 1 - crates/zed/src/zed/assistant_hints.rs | 115 --------------- docs/src/assistant/configuration.md | 26 ++-- docs/src/configuring-zed.md | 21 ++- 12 files changed, 93 insertions(+), 301 deletions(-) delete mode 100644 crates/zed/src/zed/assistant_hints.rs diff --git a/assets/settings/default.json b/assets/settings/default.json index f1071f9676..efb0cc9479 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -490,9 +490,6 @@ "version": "2", // Whether the assistant is enabled. "enabled": true, - // Whether to show inline hints showing the keybindings to use the inline assistant and the - // assistant panel. - "show_hints": false, // Whether to show the assistant panel button in the status bar. "button": true, // Where to dock the assistant panel. Can be 'left', 'right' or 'bottom'. diff --git a/crates/assistant/src/assistant_settings.rs b/crates/assistant/src/assistant_settings.rs index a782f05d03..87baf041ff 100644 --- a/crates/assistant/src/assistant_settings.rs +++ b/crates/assistant/src/assistant_settings.rs @@ -59,7 +59,6 @@ pub struct AssistantSettings { pub inline_alternatives: Vec, pub using_outdated_settings_version: bool, pub enable_experimental_live_diffs: bool, - pub show_hints: bool, } impl AssistantSettings { @@ -202,7 +201,6 @@ impl AssistantSettingsContent { AssistantSettingsContent::Versioned(settings) => match settings { VersionedAssistantSettingsContent::V1(settings) => AssistantSettingsContentV2 { enabled: settings.enabled, - show_hints: None, button: settings.button, dock: settings.dock, default_width: settings.default_width, @@ -243,7 +241,6 @@ impl AssistantSettingsContent { }, AssistantSettingsContent::Legacy(settings) => AssistantSettingsContentV2 { enabled: None, - show_hints: None, button: settings.button, dock: settings.dock, default_width: settings.default_width, @@ -356,7 +353,6 @@ impl Default for VersionedAssistantSettingsContent { fn default() -> Self { Self::V2(AssistantSettingsContentV2 { enabled: None, - show_hints: None, button: None, dock: None, default_width: None, @@ -374,11 +370,6 @@ pub struct AssistantSettingsContentV2 { /// /// Default: true enabled: Option, - /// Whether to show inline hints that show keybindings for inline assistant - /// and assistant panel. - /// - /// Default: true - show_hints: Option, /// Whether to show the assistant panel button in the status bar. /// /// Default: true @@ -513,7 +504,6 @@ impl Settings for AssistantSettings { let value = value.upgrade(); merge(&mut settings.enabled, value.enabled); - merge(&mut settings.show_hints, value.show_hints); merge(&mut settings.button, value.button); merge(&mut settings.dock, value.dock); merge( @@ -584,7 +574,6 @@ mod tests { }), inline_alternatives: None, enabled: None, - show_hints: None, button: None, dock: None, default_width: None, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 401462795e..78f0aab5a5 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -534,15 +534,6 @@ pub enum IsVimMode { No, } -pub trait ActiveLineTrailerProvider { - fn render_active_line_trailer( - &mut self, - style: &EditorStyle, - focus_handle: &FocusHandle, - cx: &mut WindowContext, - ) -> Option; -} - /// Zed's primary text input `View`, allowing users to edit a [`MultiBuffer`] /// /// See the [module level documentation](self) for more information. @@ -670,7 +661,6 @@ pub struct Editor { next_scroll_position: NextScrollCursorCenterTopBottom, addons: HashMap>, _scroll_cursor_center_top_bottom_task: Task<()>, - active_line_trailer_provider: Option>, } #[derive(Copy, Clone, Debug, PartialEq, Eq, Default)] @@ -2209,7 +2199,6 @@ impl Editor { addons: HashMap::default(), _scroll_cursor_center_top_bottom_task: Task::ready(()), text_style_refinement: None, - active_line_trailer_provider: None, }; this.tasks_update_task = Some(this.refresh_runnables(cx)); this._subscriptions.extend(project_subscriptions); @@ -2498,16 +2487,6 @@ impl Editor { self.refresh_inline_completion(false, false, cx); } - pub fn set_active_line_trailer_provider( - &mut self, - provider: Option, - _cx: &mut ViewContext, - ) where - T: ActiveLineTrailerProvider + 'static, - { - self.active_line_trailer_provider = provider.map(|provider| Box::new(provider) as Box<_>); - } - pub fn placeholder_text(&self, _cx: &WindowContext) -> Option<&str> { self.placeholder_text.as_deref() } @@ -11891,29 +11870,6 @@ impl Editor { && self.has_blame_entries(cx) } - pub fn render_active_line_trailer( - &mut self, - style: &EditorStyle, - cx: &mut WindowContext, - ) -> Option { - 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; - } - - let focus_handle = self.focus_handle.clone(); - self.active_line_trailer_provider - .as_mut()? - .render_active_line_trailer(style, &focus_handle, cx) - } - fn has_blame_entries(&self, cx: &mut WindowContext) -> bool { self.blame() .map_or(false, |blame| blame.read(cx).has_generated_entries()) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 0c403022a3..7f4bc3fb77 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1414,7 +1414,7 @@ impl EditorElement { } #[allow(clippy::too_many_arguments)] - fn layout_active_line_trailer( + fn layout_inline_blame( &self, display_row: DisplayRow, display_snapshot: &DisplaySnapshot, @@ -1426,71 +1426,61 @@ impl EditorElement { line_height: Pixels, cx: &mut WindowContext, ) -> Option { - let render_inline_blame = self + if !self .editor - .update(cx, |editor, cx| editor.render_git_blame_inline(cx)); - if render_inline_blame { - let workspace = self - .editor - .read(cx) - .workspace - .as_ref() - .map(|(w, _)| w.clone()); - - let display_point = DisplayPoint::new(display_row, 0); - let buffer_row = MultiBufferRow(display_point.to_point(display_snapshot).row); - - let blame = self.editor.read(cx).blame.clone()?; - let blame_entry = blame - .update(cx, |blame, cx| { - blame.blame_for_rows([Some(buffer_row)], cx).next() - }) - .flatten()?; - - let mut element = - render_inline_blame_entry(&blame, blame_entry, &self.style, workspace, cx); - - let start_y = content_origin.y - + line_height * (display_row.as_f32() - scroll_pixel_position.y / line_height); - - let start_x = { - const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 6.; - - let line_end = if let Some(crease_trailer) = crease_trailer { - crease_trailer.bounds.right() - } else { - content_origin.x - scroll_pixel_position.x + line_layout.width - }; - let padded_line_end = line_end + em_width * INLINE_BLAME_PADDING_EM_WIDTHS; - - let min_column_in_pixels = ProjectSettings::get_global(cx) - .git - .inline_blame - .and_then(|settings| settings.min_column) - .map(|col| self.column_pixels(col as usize, cx)) - .unwrap_or(px(0.)); - let min_start = content_origin.x - scroll_pixel_position.x + min_column_in_pixels; - - cmp::max(padded_line_end, min_start) - }; - - let absolute_offset = point(start_x, start_y); - element.prepaint_as_root(absolute_offset, AvailableSpace::min_size(), cx); - - Some(element) - } else if let Some(mut element) = self.editor.update(cx, |editor, cx| { - editor.render_active_line_trailer(&self.style, cx) - }) { - let start_y = content_origin.y - + line_height * (display_row.as_f32() - scroll_pixel_position.y / line_height); - let start_x = content_origin.x - scroll_pixel_position.x + em_width; - let absolute_offset = point(start_x, start_y); - element.prepaint_as_root(absolute_offset, AvailableSpace::min_size(), cx); - - Some(element) - } else { - None + .update(cx, |editor, cx| editor.render_git_blame_inline(cx)) + { + return None; } + + let workspace = self + .editor + .read(cx) + .workspace + .as_ref() + .map(|(w, _)| w.clone()); + + let display_point = DisplayPoint::new(display_row, 0); + let buffer_row = MultiBufferRow(display_point.to_point(display_snapshot).row); + + let blame = self.editor.read(cx).blame.clone()?; + let blame_entry = blame + .update(cx, |blame, cx| { + blame.blame_for_rows([Some(buffer_row)], cx).next() + }) + .flatten()?; + + let mut element = + render_inline_blame_entry(&blame, blame_entry, &self.style, workspace, cx); + + let start_y = content_origin.y + + line_height * (display_row.as_f32() - scroll_pixel_position.y / line_height); + + let start_x = { + const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 6.; + + let line_end = if let Some(crease_trailer) = crease_trailer { + crease_trailer.bounds.right() + } else { + content_origin.x - scroll_pixel_position.x + line_layout.width + }; + let padded_line_end = line_end + em_width * INLINE_BLAME_PADDING_EM_WIDTHS; + + let min_column_in_pixels = ProjectSettings::get_global(cx) + .git + .inline_blame + .and_then(|settings| settings.min_column) + .map(|col| self.column_pixels(col as usize, cx)) + .unwrap_or(px(0.)); + let min_start = content_origin.x - scroll_pixel_position.x + min_column_in_pixels; + + cmp::max(padded_line_end, min_start) + }; + + let absolute_offset = point(start_x, start_y); + element.prepaint_as_root(absolute_offset, AvailableSpace::min_size(), cx); + + Some(element) } #[allow(clippy::too_many_arguments)] @@ -3466,7 +3456,7 @@ impl EditorElement { self.paint_lines(&invisible_display_ranges, layout, cx); self.paint_redactions(layout, cx); self.paint_cursors(layout, cx); - self.paint_active_line_trailer(layout, cx); + self.paint_inline_blame(layout, cx); cx.with_element_namespace("crease_trailers", |cx| { for trailer in layout.crease_trailers.iter_mut().flatten() { trailer.element.paint(cx); @@ -3948,10 +3938,10 @@ impl EditorElement { } } - fn paint_active_line_trailer(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) { - if let Some(mut element) = layout.active_line_trailer.take() { + fn paint_inline_blame(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) { + if let Some(mut inline_blame) = layout.inline_blame.take() { cx.paint_layer(layout.text_hitbox.bounds, |cx| { - element.paint(cx); + inline_blame.paint(cx); }) } } @@ -5343,14 +5333,14 @@ impl Element for EditorElement { ) }); - let mut active_line_trailer = None; + let mut inline_blame = None; if let Some(newest_selection_head) = newest_selection_head { let display_row = newest_selection_head.row(); if (start_row..end_row).contains(&display_row) { let line_ix = display_row.minus(start_row) as usize; let line_layout = &line_layouts[line_ix]; let crease_trailer_layout = crease_trailers[line_ix].as_ref(); - active_line_trailer = self.layout_active_line_trailer( + inline_blame = self.layout_inline_blame( display_row, &snapshot.display_snapshot, line_layout, @@ -5669,7 +5659,7 @@ impl Element for EditorElement { line_elements, line_numbers, blamed_display_rows, - active_line_trailer, + inline_blame, blocks, cursors, visible_cursors, @@ -5806,7 +5796,7 @@ pub struct EditorLayout { line_numbers: Vec>, display_hunks: Vec<(DisplayDiffHunk, Option)>, blamed_display_rows: Option>, - active_line_trailer: Option, + inline_blame: Option, blocks: Vec, highlighted_ranges: Vec<(Range, Hsla)>, highlighted_gutter_ranges: Vec<(Range, Hsla)>, diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 2b6f1d4a99..c1c14edba2 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -3064,7 +3064,7 @@ impl<'a> WindowContext<'a> { } /// Represent this action as a key binding string, to display in the UI. - pub fn keystroke_text_for_action(&self, action: &dyn Action) -> String { + pub fn keystroke_text_for(&self, action: &dyn Action) -> String { self.bindings_for_action(action) .into_iter() .next() @@ -3079,26 +3079,6 @@ impl<'a> WindowContext<'a> { .unwrap_or_else(|| action.name().to_string()) } - /// Represent this action as a key binding string, to display in the UI. - pub fn keystroke_text_for_action_in( - &self, - action: &dyn Action, - focus_handle: &FocusHandle, - ) -> String { - self.bindings_for_action_in(action, focus_handle) - .into_iter() - .next() - .map(|binding| { - binding - .keystrokes() - .iter() - .map(ToString::to_string) - .collect::>() - .join(" ") - }) - .unwrap_or_else(|| action.name().to_string()) - } - /// Dispatch a mouse or keyboard event on the window. #[profiling::function] pub fn dispatch_event(&mut self, event: PlatformInput) -> DispatchEventResult { diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index f378348782..f878b582d9 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -3875,13 +3875,13 @@ impl OutlinePanel { .child({ let keystroke = match self.position(cx) { DockPosition::Left => { - cx.keystroke_text_for_action(&workspace::ToggleLeftDock) + cx.keystroke_text_for(&workspace::ToggleLeftDock) } DockPosition::Bottom => { - cx.keystroke_text_for_action(&workspace::ToggleBottomDock) + cx.keystroke_text_for(&workspace::ToggleBottomDock) } DockPosition::Right => { - cx.keystroke_text_for_action(&workspace::ToggleRightDock) + cx.keystroke_text_for(&workspace::ToggleRightDock) } }; Label::new(format!("Toggle this panel with {keystroke}")) diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index c08136cdf5..404bf26b62 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -172,13 +172,13 @@ impl PickerDelegate for RecentProjectsDelegate { fn placeholder_text(&self, cx: &mut WindowContext) -> Arc { let (create_window, reuse_window) = if self.create_new_window { ( - cx.keystroke_text_for_action(&menu::Confirm), - cx.keystroke_text_for_action(&menu::SecondaryConfirm), + cx.keystroke_text_for(&menu::Confirm), + cx.keystroke_text_for(&menu::SecondaryConfirm), ) } else { ( - cx.keystroke_text_for_action(&menu::SecondaryConfirm), - cx.keystroke_text_for_action(&menu::Confirm), + cx.keystroke_text_for(&menu::SecondaryConfirm), + cx.keystroke_text_for(&menu::Confirm), ) }; Arc::from(format!( diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 6febe05d10..cccd50da96 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -61,7 +61,7 @@ use zed::{ OpenRequest, }; -use crate::zed::{assistant_hints, inline_completion_registry}; +use crate::zed::inline_completion_registry; #[cfg(feature = "mimalloc")] #[global_allocator] @@ -407,7 +407,6 @@ fn main() { cx, ); assistant2::init(cx); - assistant_hints::init(cx); repl::init( app_state.fs.clone(), app_state.client.telemetry().clone(), diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index be6df49a2d..5ba63b9c1f 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1,5 +1,4 @@ mod app_menus; -pub mod assistant_hints; pub mod inline_completion_registry; #[cfg(any(target_os = "linux", target_os = "freebsd"))] pub(crate) mod linux_prompts; diff --git a/crates/zed/src/zed/assistant_hints.rs b/crates/zed/src/zed/assistant_hints.rs deleted file mode 100644 index 244b7fab26..0000000000 --- a/crates/zed/src/zed/assistant_hints.rs +++ /dev/null @@ -1,115 +0,0 @@ -use assistant::assistant_settings::AssistantSettings; -use collections::HashMap; -use editor::{ActiveLineTrailerProvider, Editor, EditorMode}; -use gpui::{AnyWindowHandle, AppContext, ViewContext, WeakView, WindowContext}; -use settings::{Settings, SettingsStore}; -use std::{cell::RefCell, rc::Rc}; -use theme::ActiveTheme; -use ui::prelude::*; -use workspace::Workspace; - -pub fn init(cx: &mut AppContext) { - let editors: Rc, AnyWindowHandle>>> = Rc::default(); - - cx.observe_new_views({ - let editors = editors.clone(); - move |_: &mut Workspace, cx: &mut ViewContext| { - let workspace_handle = cx.view().clone(); - cx.subscribe(&workspace_handle, { - let editors = editors.clone(); - move |_, _, event, cx| match event { - workspace::Event::ItemAdded { item } => { - if let Some(editor) = item.act_as::(cx) { - if editor.read(cx).mode() != EditorMode::Full { - return; - } - - cx.on_release({ - let editor_handle = editor.downgrade(); - let editors = editors.clone(); - move |_, _, _| { - editors.borrow_mut().remove(&editor_handle); - } - }) - .detach(); - editors - .borrow_mut() - .insert(editor.downgrade(), cx.window_handle()); - - let show_hints = should_show_hints(cx); - editor.update(cx, |editor, cx| { - assign_active_line_trailer_provider(editor, show_hints, cx) - }) - } - } - _ => {} - } - }) - .detach(); - } - }) - .detach(); - - let mut show_hints = AssistantSettings::get_global(cx).show_hints; - cx.observe_global::(move |cx| { - let new_show_hints = should_show_hints(cx); - if new_show_hints != show_hints { - show_hints = new_show_hints; - for (editor, window) in editors.borrow().iter() { - _ = window.update(cx, |_window, cx| { - _ = editor.update(cx, |editor, cx| { - assign_active_line_trailer_provider(editor, show_hints, cx); - }) - }); - } - } - }) - .detach(); -} - -struct AssistantHintsProvider; - -impl ActiveLineTrailerProvider for AssistantHintsProvider { - fn render_active_line_trailer( - &mut self, - style: &editor::EditorStyle, - focus_handle: &gpui::FocusHandle, - cx: &mut WindowContext, - ) -> Option { - if !focus_handle.is_focused(cx) { - return None; - } - - let chat_keybinding = - cx.keystroke_text_for_action_in(&assistant::ToggleFocus, focus_handle); - let generate_keybinding = - cx.keystroke_text_for_action_in(&zed_actions::InlineAssist::default(), focus_handle); - - Some( - h_flex() - .id("inline-assistant-instructions") - .w_full() - .font_family(style.text.font().family) - .text_color(cx.theme().status().hint) - .line_height(style.text.line_height) - .child(format!( - "{chat_keybinding} to chat, {generate_keybinding} to generate" - )) - .into_any(), - ) - } -} - -fn assign_active_line_trailer_provider( - editor: &mut Editor, - show_hints: bool, - cx: &mut ViewContext, -) { - let provider = show_hints.then_some(AssistantHintsProvider); - editor.set_active_line_trailer_provider(provider, cx); -} - -fn should_show_hints(cx: &AppContext) -> bool { - let assistant_settings = AssistantSettings::get_global(cx); - assistant_settings.enabled && assistant_settings.show_hints -} diff --git a/docs/src/assistant/configuration.md b/docs/src/assistant/configuration.md index 1be96491f4..2145bd9504 100644 --- a/docs/src/assistant/configuration.md +++ b/docs/src/assistant/configuration.md @@ -200,28 +200,18 @@ You must provide the model's Context Window in the `max_tokens` parameter, this { "assistant": { "enabled": true, - "show_hints": true, - "button": true, - "dock": "right" - "default_width": 480, "default_model": { "provider": "zed.dev", "model": "claude-3-5-sonnet" }, "version": "2", + "button": true, + "default_width": 480, + "dock": "right" } } ``` -| key | type | default | description | -| -------------- | ------- | ------- | ------------------------------------------------------------------------------------- | -| enabled | boolean | true | Setting this to `false` will completely disable the assistant | -| show_hints | boolean | true | Whether to to show hints in the editor explaining how to use assistant | -| button | boolean | true | Show the assistant icon in the status bar | -| dock | string | "right" | The default dock position for the assistant panel. Can be ["left", "right", "bottom"] | -| default_height | string | null | The pixel height of the assistant panel when docked to the bottom | -| default_width | string | null | The pixel width of the assistant panel when docked to the left or right | - #### Custom endpoints {#custom-endpoint} You can use a custom API endpoint for different providers, as long as it's compatible with the providers API structure. @@ -281,3 +271,13 @@ will generate two outputs for every assist. One with Claude 3.5 Sonnet, and one } } ``` + +#### Common Panel Settings + +| key | type | default | description | +| -------------- | ------- | ------- | ------------------------------------------------------------------------------------- | +| enabled | boolean | true | Setting this to `false` will completely disable the assistant | +| button | boolean | true | Show the assistant icon in the status bar | +| dock | string | "right" | The default dock position for the assistant panel. Can be ["left", "right", "bottom"] | +| default_height | string | null | The pixel height of the assistant panel when docked to the bottom | +| default_width | string | null | The pixel width of the assistant panel when docked to the left or right | diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index 4991ff1119..5eacf4136d 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -2333,18 +2333,15 @@ Run the `theme selector: toggle` action in the command palette to see a current - Default: ```json -{ - "assistant": { - "enabled": true, - "button": true, - "dock": "right", - "default_width": 640, - "default_height": 320, - "provider": "openai", - "version": "1", - "show_hints": true - } -} +"assistant": { + "enabled": true, + "button": true, + "dock": "right", + "default_width": 640, + "default_height": 320, + "provider": "openai", + "version": "1", +}, ``` ## Outline Panel From 389422cbf3ebd44f833159fbdcacbe99f5320c92 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 25 Nov 2024 11:25:37 -0500 Subject: [PATCH 139/886] docs: Fix broken link to context servers docs (#21172) This PR fixes a broken link to the context server docs. Release Notes: - N/A --- docs/src/assistant/model-context-protocol.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/assistant/model-context-protocol.md b/docs/src/assistant/model-context-protocol.md index 34c67a8845..74e16b59ff 100644 --- a/docs/src/assistant/model-context-protocol.md +++ b/docs/src/assistant/model-context-protocol.md @@ -1,6 +1,6 @@ # Model Context Protocol -Zed uses the [Model Context Protocol](https://modelcontextprotocol.io/) to interact with [context servers](./context-server.md): +Zed uses the [Model Context Protocol](https://modelcontextprotocol.io/) to interact with [context servers](./context-servers.md): > The Model Context Protocol (MCP) is an open protocol that enables seamless integration between LLM applications and external data sources and tools. Whether you're building an AI-powered IDE, enhancing a chat interface, or creating custom AI workflows, MCP provides a standardized way to connect LLMs with the context they need. From 28142be5e9ff16abbf62d8c28fa634ba12886b14 Mon Sep 17 00:00:00 2001 From: teapo <75266237+4teapo@users.noreply.github.com> Date: Mon, 25 Nov 2024 17:11:23 +0000 Subject: [PATCH 140/886] Update Luau docs (#21174) Formatter arguments & Tree-sitter grammar changed. Release Notes: - N/A --- docs/src/languages/luau.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/languages/luau.md b/docs/src/languages/luau.md index c7abd0cae9..0c5e94dcf8 100644 --- a/docs/src/languages/luau.md +++ b/docs/src/languages/luau.md @@ -5,7 +5,7 @@ Luau language support in Zed is provided by the community-maintained [Luau extension](https://github.com/4teapo/zed-luau). Report issues to: [https://github.com/4teapo/zed-luau/issues](https://github.com/4teapo/zed-luau/issues) -- Tree Sitter: [tree-sitter-grammars/tree-sitter-luau](https://github.com/tree-sitter-grammars/tree-sitter-luau) +- Tree Sitter: [4teapo/tree-sitter-luau](https://github.com/4teapo/tree-sitter-luau) - Language Server: [JohnnyMorganz/luau-lsp](https://github.com/JohnnyMorganz/luau-lsp) ## Configuration @@ -33,7 +33,7 @@ Then add the following to your Zed `settings.json`: "formatter": { "external": { "command": "stylua", - "arguments": ["-"] + "arguments": ["--stdin-filepath", "{buffer_path}", "-"] } } } From bd02b35ba948970d22bdee167abe86bbdd8351c7 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 25 Nov 2024 19:21:30 +0200 Subject: [PATCH 141/886] Avoid excessive diagnostics refreshes (#21173) Attempts to reduce the diagnostics flicker, when editing very fundamental parts of the large code base in Rust. https://github.com/user-attachments/assets/dc3f9c21-8c6e-48db-967b-040649fd00da Release Notes: - N/A --- crates/diagnostics/src/diagnostics.rs | 6 ++++++ crates/diagnostics/src/diagnostics_tests.rs | 17 ++++++++++++++--- crates/gpui/src/app/test_context.rs | 11 ++++++++--- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index bd0af230ab..be8da5c130 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -33,6 +33,7 @@ use std::{ mem, ops::Range, sync::Arc, + time::Duration, }; use theme::ActiveTheme; pub use toolbar_controls::ToolbarControls; @@ -82,6 +83,8 @@ struct DiagnosticGroupState { impl EventEmitter for ProjectDiagnosticsEditor {} +const DIAGNOSTICS_UPDATE_DEBOUNCE: Duration = Duration::from_millis(50); + impl Render for ProjectDiagnosticsEditor { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let child = if self.path_states.is_empty() { @@ -198,6 +201,9 @@ impl ProjectDiagnosticsEditor { } let project_handle = self.project.clone(); self.update_excerpts_task = Some(cx.spawn(|this, mut cx| async move { + cx.background_executor() + .timer(DIAGNOSTICS_UPDATE_DEBOUNCE) + .await; loop { let Some((path, language_server_id)) = this.update(&mut cx, |this, _| { let Some((path, language_server_id)) = this.paths_to_update.pop_first() else { diff --git a/crates/diagnostics/src/diagnostics_tests.rs b/crates/diagnostics/src/diagnostics_tests.rs index c5ae29ff2e..ff305e45a2 100644 --- a/crates/diagnostics/src/diagnostics_tests.rs +++ b/crates/diagnostics/src/diagnostics_tests.rs @@ -155,7 +155,8 @@ async fn test_diagnostics(cx: &mut TestAppContext) { }); let editor = view.update(cx, |view, _| view.editor.clone()); - view.next_notification(cx).await; + view.next_notification(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10), cx) + .await; assert_eq!( editor_blocks(&editor, cx), [ @@ -240,7 +241,8 @@ async fn test_diagnostics(cx: &mut TestAppContext) { lsp_store.disk_based_diagnostics_finished(language_server_id, cx); }); - view.next_notification(cx).await; + view.next_notification(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10), cx) + .await; assert_eq!( editor_blocks(&editor, cx), [ @@ -352,7 +354,8 @@ async fn test_diagnostics(cx: &mut TestAppContext) { lsp_store.disk_based_diagnostics_finished(language_server_id, cx); }); - view.next_notification(cx).await; + view.next_notification(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10), cx) + .await; assert_eq!( editor_blocks(&editor, cx), [ @@ -491,6 +494,8 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) { }); // Only the first language server's diagnostics are shown. + cx.executor() + .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10)); cx.executor().run_until_parked(); assert_eq!( editor_blocks(&editor, cx), @@ -537,6 +542,8 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) { }); // Both language server's diagnostics are shown. + cx.executor() + .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10)); cx.executor().run_until_parked(); assert_eq!( editor_blocks(&editor, cx), @@ -603,6 +610,8 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) { }); // Only the first language server's diagnostics are updated. + cx.executor() + .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10)); cx.executor().run_until_parked(); assert_eq!( editor_blocks(&editor, cx), @@ -659,6 +668,8 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) { }); // Both language servers' diagnostics are updated. + cx.executor() + .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10)); cx.executor().run_until_parked(); assert_eq!( editor_blocks(&editor, cx), diff --git a/crates/gpui/src/app/test_context.rs b/crates/gpui/src/app/test_context.rs index 34449c91ec..2fea804301 100644 --- a/crates/gpui/src/app/test_context.rs +++ b/crates/gpui/src/app/test_context.rs @@ -538,12 +538,15 @@ impl Model { impl View { /// Returns a future that resolves when the view is next updated. - pub fn next_notification(&self, cx: &TestAppContext) -> impl Future { + pub fn next_notification( + &self, + advance_clock_by: Duration, + cx: &TestAppContext, + ) -> impl Future { use postage::prelude::{Sink as _, Stream as _}; let (mut tx, mut rx) = postage::mpsc::channel(1); - let mut cx = cx.app.app.borrow_mut(); - let subscription = cx.observe(self, move |_, _| { + let subscription = cx.app.app.borrow_mut().observe(self, move |_, _| { tx.try_send(()).ok(); }); @@ -553,6 +556,8 @@ impl View { Duration::from_secs(1) }; + cx.executor().advance_clock(advance_clock_by); + async move { let notification = crate::util::timeout(duration, rx.recv()) .await From a02684b2f7f86fc8cc7963c350815a656525c677 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 25 Nov 2024 14:08:40 -0500 Subject: [PATCH 142/886] assistant2: Add rudimentary chat functionality (#21178) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds in rudimentary functionality for sending messages to the LLM in `assistant2`. Screenshot 2024-11-25 at 1 49 11 PM Release Notes: - N/A --- Cargo.lock | 3 + assets/keymaps/default-macos.json | 12 ++ crates/assistant2/Cargo.toml | 3 + crates/assistant2/src/assistant.rs | 6 +- crates/assistant2/src/assistant_panel.rs | 28 ++++- crates/assistant2/src/message_editor.rs | 144 ++++++++++++++++++++++- crates/assistant2/src/thread.rs | 23 ++++ 7 files changed, 211 insertions(+), 8 deletions(-) create mode 100644 crates/assistant2/src/thread.rs diff --git a/Cargo.lock b/Cargo.lock index 91205f214f..b8c24b4594 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -458,13 +458,16 @@ dependencies = [ "command_palette_hooks", "editor", "feature_flags", + "futures 0.3.31", "gpui", "language_model", "language_model_selector", "proto", "settings", + "smol", "theme", "ui", + "util", "workspace", ] diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 963d48ba5e..c8bc80a9c0 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -209,6 +209,18 @@ "alt-enter": "editor::Newline" } }, + { + "context": "AssistantPanel2", + "bindings": { + "cmd-n": "assistant2::NewThread" + } + }, + { + "context": "MessageEditor > Editor", + "bindings": { + "cmd-enter": "assistant2::Chat" + } + }, { "context": "PromptLibrary", "bindings": { diff --git a/crates/assistant2/Cargo.toml b/crates/assistant2/Cargo.toml index 9dd605d559..02cbdadb62 100644 --- a/crates/assistant2/Cargo.toml +++ b/crates/assistant2/Cargo.toml @@ -17,11 +17,14 @@ anyhow.workspace = true command_palette_hooks.workspace = true editor.workspace = true feature_flags.workspace = true +futures.workspace = true gpui.workspace = true language_model.workspace = true language_model_selector.workspace = true proto.workspace = true settings.workspace = true +smol.workspace = true theme.workspace = true ui.workspace = true +util.workspace = true workspace.workspace = true diff --git a/crates/assistant2/src/assistant.rs b/crates/assistant2/src/assistant.rs index 6a80186525..1b33e27928 100644 --- a/crates/assistant2/src/assistant.rs +++ b/crates/assistant2/src/assistant.rs @@ -1,5 +1,6 @@ mod assistant_panel; mod message_editor; +mod thread; use command_palette_hooks::CommandPaletteFilter; use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt}; @@ -7,7 +8,10 @@ use gpui::{actions, AppContext}; pub use crate::assistant_panel::AssistantPanel; -actions!(assistant2, [ToggleFocus, NewThread, ToggleModelSelector]); +actions!( + assistant2, + [ToggleFocus, NewThread, ToggleModelSelector, Chat] +); const NAMESPACE: &str = "assistant2"; diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index 890020e54a..88a3f73176 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -1,7 +1,7 @@ use anyhow::Result; use gpui::{ prelude::*, px, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, - FocusableView, Pixels, Task, View, ViewContext, WeakView, WindowContext, + FocusableView, Model, Pixels, Task, View, ViewContext, WeakView, WindowContext, }; use language_model::LanguageModelRegistry; use language_model_selector::LanguageModelSelector; @@ -10,6 +10,7 @@ use workspace::dock::{DockPosition, Panel, PanelEvent}; use workspace::{Pane, Workspace}; use crate::message_editor::MessageEditor; +use crate::thread::Thread; use crate::{NewThread, ToggleFocus, ToggleModelSelector}; pub fn init(cx: &mut AppContext) { @@ -25,6 +26,7 @@ pub fn init(cx: &mut AppContext) { pub struct AssistantPanel { pane: View, + thread: Model, message_editor: View, } @@ -56,9 +58,12 @@ impl AssistantPanel { pane }); + let thread = cx.new_model(Thread::new); + Self { pane, - message_editor: cx.new_view(MessageEditor::new), + thread: thread.clone(), + message_editor: cx.new_view(|cx| MessageEditor::new(thread, cx)), } } } @@ -247,7 +252,24 @@ impl Render for AssistantPanel { println!("Action: New Thread"); })) .child(self.render_toolbar(cx)) - .child(v_flex().bg(cx.theme().colors().panel_background)) + .child( + v_flex() + .id("message-list") + .gap_2() + .size_full() + .p_2() + .overflow_y_scroll() + .bg(cx.theme().colors().panel_background) + .children(self.thread.read(cx).messages.iter().map(|message| { + v_flex() + .p_2() + .border_1() + .border_color(cx.theme().colors().border_variant) + .rounded_md() + .child(Label::new(message.role.to_string())) + .child(Label::new(message.text.clone())) + })), + ) .child( h_flex() .border_t_1() diff --git a/crates/assistant2/src/message_editor.rs b/crates/assistant2/src/message_editor.rs index ee25ad5da7..63f8c869d4 100644 --- a/crates/assistant2/src/message_editor.rs +++ b/crates/assistant2/src/message_editor.rs @@ -1,16 +1,32 @@ use editor::{Editor, EditorElement, EditorStyle}; -use gpui::{TextStyle, View}; +use futures::StreamExt; +use gpui::{AppContext, Model, TextStyle, View}; +use language_model::{ + LanguageModelCompletionEvent, LanguageModelRegistry, LanguageModelRequest, + LanguageModelRequestMessage, MessageContent, Role, StopReason, +}; use settings::Settings; use theme::ThemeSettings; -use ui::prelude::*; +use ui::{prelude::*, ButtonLike, ElevationIndex, KeyBinding}; +use util::ResultExt; + +use crate::thread::{self, Thread}; +use crate::Chat; + +#[derive(Debug, Clone, Copy)] +pub enum RequestKind { + Chat, +} pub struct MessageEditor { + thread: Model, editor: View, } impl MessageEditor { - pub fn new(cx: &mut ViewContext) -> Self { + pub fn new(thread: Model, cx: &mut ViewContext) -> Self { Self { + thread, editor: cx.new_view(|cx| { let mut editor = Editor::auto_height(80, cx); editor.set_placeholder_text("Ask anything…", cx); @@ -19,14 +35,122 @@ impl MessageEditor { }), } } + + fn chat(&mut self, _: &Chat, cx: &mut ViewContext) { + self.send_to_model(RequestKind::Chat, cx); + } + + fn send_to_model( + &mut self, + request_kind: RequestKind, + cx: &mut ViewContext, + ) -> Option<()> { + let provider = LanguageModelRegistry::read_global(cx).active_provider(); + if provider + .as_ref() + .map_or(false, |provider| provider.must_accept_terms(cx)) + { + cx.notify(); + return None; + } + + let model_registry = LanguageModelRegistry::read_global(cx); + let model = model_registry.active_model()?; + + let request = self.build_completion_request(request_kind, cx); + + let user_message = self.editor.read(cx).text(cx); + self.thread.update(cx, |thread, _cx| { + thread.messages.push(thread::Message { + role: Role::User, + text: user_message, + }); + }); + + self.editor.update(cx, |editor, cx| { + editor.clear(cx); + }); + + let task = cx.spawn(|this, mut cx| async move { + let stream = model.stream_completion(request, &cx); + let stream_completion = async { + let mut events = stream.await?; + let mut stop_reason = StopReason::EndTurn; + + let mut text = String::new(); + + while let Some(event) = events.next().await { + let event = event?; + match event { + LanguageModelCompletionEvent::StartMessage { .. } => {} + LanguageModelCompletionEvent::Stop(reason) => { + stop_reason = reason; + } + LanguageModelCompletionEvent::Text(chunk) => { + text.push_str(&chunk); + } + LanguageModelCompletionEvent::ToolUse(_tool_use) => {} + } + + smol::future::yield_now().await; + } + + anyhow::Ok((stop_reason, text)) + }; + + let result = stream_completion.await; + + this.update(&mut cx, |this, cx| { + if let Some((_stop_reason, text)) = result.log_err() { + this.thread.update(cx, |thread, _cx| { + thread.messages.push(thread::Message { + role: Role::Assistant, + text, + }); + }); + } + }) + .ok(); + }); + + self.thread.update(cx, |thread, _cx| { + thread.pending_completion_tasks.push(task); + }); + + None + } + + fn build_completion_request( + &self, + _request_kind: RequestKind, + cx: &AppContext, + ) -> LanguageModelRequest { + let text = self.editor.read(cx).text(cx); + + let request = LanguageModelRequest { + messages: vec![LanguageModelRequestMessage { + role: Role::User, + content: vec![MessageContent::Text(text)], + cache: false, + }], + tools: Vec::new(), + stop: Vec::new(), + temperature: None, + }; + + request + } } impl Render for MessageEditor { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let font_size = TextSize::Default.rems(cx); let line_height = font_size.to_pixels(cx.rem_size()) * 1.3; + let focus_handle = self.editor.focus_handle(cx); v_flex() + .key_context("MessageEditor") + .on_action(cx.listener(Self::chat)) .size_full() .gap_2() .p_2() @@ -69,7 +193,19 @@ impl Render for MessageEditor { .gap_2() .child(Button::new("codebase", "Codebase").style(ButtonStyle::Filled)) .child(Label::new("or")) - .child(Button::new("chat", "Chat").style(ButtonStyle::Filled)), + .child( + ButtonLike::new("chat") + .style(ButtonStyle::Filled) + .layer(ElevationIndex::ModalSurface) + .child(Label::new("Chat")) + .children( + KeyBinding::for_action_in(&Chat, &focus_handle, cx) + .map(|binding| binding.into_any_element()), + ) + .on_click(move |_event, cx| { + focus_handle.dispatch_action(&Chat, cx); + }), + ), ), ) } diff --git a/crates/assistant2/src/thread.rs b/crates/assistant2/src/thread.rs new file mode 100644 index 0000000000..1553eaabb6 --- /dev/null +++ b/crates/assistant2/src/thread.rs @@ -0,0 +1,23 @@ +use gpui::{ModelContext, Task}; +use language_model::Role; + +/// A message in a [`Thread`]. +pub struct Message { + pub role: Role, + pub text: String, +} + +/// A thread of conversation with the LLM. +pub struct Thread { + pub messages: Vec, + pub pending_completion_tasks: Vec>, +} + +impl Thread { + pub fn new(_cx: &mut ModelContext) -> Self { + Self { + messages: Vec::new(), + pending_completion_tasks: Vec::new(), + } + } +} From 91a565f5faa396cb9f31d4a939c597b0ea11b794 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 25 Nov 2024 12:53:23 -0800 Subject: [PATCH 143/886] Simplify BufferStore's local vs remote state (#21180) This is a pure refactor, motivated by wanting to introduce to the BufferStore new logic for opening staged and committed changes. I found the `BufferStoreImpl` trait a little bit confusing, particularly how the different implementors of the trait held a handle back to the owning buffer store. I was able to reduce the amount of code considerably (-78 lines) by using a two-variant enum instead, similar to what we do for `LspStore`, `WorktreeStore` and `Worktree`. Release Notes: - N/A --- crates/project/src/buffer_store.rs | 738 +++++++++++++---------------- 1 file changed, 330 insertions(+), 408 deletions(-) diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index eb56680fb3..55b0f413a9 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -29,38 +29,23 @@ use text::BufferId; use util::{debug_panic, maybe, ResultExt as _, TryFutureExt}; use worktree::{File, PathChange, ProjectEntryId, UpdatedGitRepositoriesSet, Worktree, WorktreeId}; -trait BufferStoreImpl { - fn open_buffer( - &self, - path: Arc, - worktree: Model, - cx: &mut ModelContext, - ) -> Task>>; +/// A set of open buffers. +pub struct BufferStore { + state: BufferStoreState, + #[allow(clippy::type_complexity)] + loading_buffers_by_path: HashMap< + ProjectPath, + postage::watch::Receiver, Arc>>>, + >, + worktree_store: Model, + opened_buffers: HashMap, + downstream_client: Option<(AnyProtoClient, u64)>, + shared_buffers: HashMap>>, +} - fn save_buffer( - &self, - buffer: Model, - cx: &mut ModelContext, - ) -> Task>; - - fn save_buffer_as( - &self, - buffer: Model, - path: ProjectPath, - cx: &mut ModelContext, - ) -> Task>; - - fn create_buffer(&self, cx: &mut ModelContext) -> Task>>; - - fn reload_buffers( - &self, - buffers: HashSet>, - push_to_history: bool, - cx: &mut ModelContext, - ) -> Task>; - - fn as_remote(&self) -> Option>; - fn as_local(&self) -> Option>; +enum BufferStoreState { + Local(LocalBufferStore), + Remote(RemoteBufferStore), } struct RemoteBufferStore { @@ -71,31 +56,15 @@ struct RemoteBufferStore { remote_buffer_listeners: HashMap, anyhow::Error>>>>, worktree_store: Model, - buffer_store: WeakModel, } struct LocalBufferStore { local_buffer_ids_by_path: HashMap, local_buffer_ids_by_entry_id: HashMap, - buffer_store: WeakModel, worktree_store: Model, _subscription: Subscription, } -/// A set of open buffers. -pub struct BufferStore { - state: Box, - #[allow(clippy::type_complexity)] - loading_buffers_by_path: HashMap< - ProjectPath, - postage::watch::Receiver, Arc>>>, - >, - worktree_store: Model, - opened_buffers: HashMap, - downstream_client: Option<(AnyProtoClient, u64)>, - shared_buffers: HashMap>>, -} - enum OpenBuffer { Buffer(WeakModel), Operations(Vec), @@ -119,14 +88,13 @@ impl RemoteBufferStore { pub fn wait_for_remote_buffer( &mut self, id: BufferId, - cx: &mut AppContext, + cx: &mut ModelContext, ) -> Task>> { - let buffer_store = self.buffer_store.clone(); let (tx, rx) = oneshot::channel(); self.remote_buffer_listeners.entry(id).or_default().push(tx); - cx.spawn(|cx| async move { - if let Some(buffer) = buffer_store + cx.spawn(|this, cx| async move { + if let Some(buffer) = this .read_with(&cx, |buffer_store, _| buffer_store.get(id)) .ok() .flatten() @@ -144,7 +112,7 @@ impl RemoteBufferStore { &self, buffer_handle: Model, new_path: Option, - cx: &ModelContext, + cx: &ModelContext, ) -> Task> { let buffer = buffer_handle.read(cx); let buffer_id = buffer.remote_id().into(); @@ -176,7 +144,7 @@ impl RemoteBufferStore { envelope: TypedEnvelope, replica_id: u16, capability: Capability, - cx: &mut ModelContext, + cx: &mut ModelContext, ) -> Result>> { match envelope .payload @@ -277,7 +245,7 @@ impl RemoteBufferStore { &self, message: proto::ProjectTransaction, push_to_history: bool, - cx: &mut ModelContext, + cx: &mut ModelContext, ) -> Task> { cx.spawn(|this, mut cx| async move { let mut project_transaction = ProjectTransaction::default(); @@ -310,36 +278,6 @@ impl RemoteBufferStore { Ok(project_transaction) }) } -} - -impl BufferStoreImpl for Model { - fn as_remote(&self) -> Option> { - Some(self.clone()) - } - - fn as_local(&self) -> Option> { - None - } - - fn save_buffer( - &self, - buffer: Model, - cx: &mut ModelContext, - ) -> Task> { - self.update(cx, |this, cx| { - this.save_remote_buffer(buffer.clone(), None, cx) - }) - } - fn save_buffer_as( - &self, - buffer: Model, - path: ProjectPath, - cx: &mut ModelContext, - ) -> Task> { - self.update(cx, |this, cx| { - this.save_remote_buffer(buffer, Some(path.to_proto()), cx) - }) - } fn open_buffer( &self, @@ -347,46 +285,42 @@ impl BufferStoreImpl for Model { worktree: Model, cx: &mut ModelContext, ) -> Task>> { - self.update(cx, |this, cx| { - let worktree_id = worktree.read(cx).id().to_proto(); - let project_id = this.project_id; - let client = this.upstream_client.clone(); - let path_string = path.clone().to_string_lossy().to_string(); - cx.spawn(move |this, mut cx| async move { - let response = client - .request(proto::OpenBufferByPath { - project_id, - worktree_id, - path: path_string, - }) - .await?; - let buffer_id = BufferId::new(response.buffer_id)?; + let worktree_id = worktree.read(cx).id().to_proto(); + let project_id = self.project_id; + let client = self.upstream_client.clone(); + let path_string = path.clone().to_string_lossy().to_string(); + cx.spawn(move |this, mut cx| async move { + let response = client + .request(proto::OpenBufferByPath { + project_id, + worktree_id, + path: path_string, + }) + .await?; + let buffer_id = BufferId::new(response.buffer_id)?; - let buffer = this - .update(&mut cx, { - |this, cx| this.wait_for_remote_buffer(buffer_id, cx) - })? - .await?; + let buffer = this + .update(&mut cx, { + |this, cx| this.wait_for_remote_buffer(buffer_id, cx) + })? + .await?; - Ok(buffer) - }) + Ok(buffer) }) } fn create_buffer(&self, cx: &mut ModelContext) -> Task>> { - self.update(cx, |this, cx| { - let create = this.upstream_client.request(proto::OpenNewBuffer { - project_id: this.project_id, - }); - cx.spawn(|this, mut cx| async move { - let response = create.await?; - let buffer_id = BufferId::new(response.buffer_id)?; + let create = self.upstream_client.request(proto::OpenNewBuffer { + project_id: self.project_id, + }); + cx.spawn(|this, mut cx| async move { + let response = create.await?; + let buffer_id = BufferId::new(response.buffer_id)?; - this.update(&mut cx, |this, cx| { - this.wait_for_remote_buffer(buffer_id, cx) - })? - .await - }) + this.update(&mut cx, |this, cx| { + this.wait_for_remote_buffer(buffer_id, cx) + })? + .await }) } @@ -396,25 +330,23 @@ impl BufferStoreImpl for Model { push_to_history: bool, cx: &mut ModelContext, ) -> Task> { - self.update(cx, |this, cx| { - let request = this.upstream_client.request(proto::ReloadBuffers { - project_id: this.project_id, - buffer_ids: buffers - .iter() - .map(|buffer| buffer.read(cx).remote_id().to_proto()) - .collect(), - }); + let request = self.upstream_client.request(proto::ReloadBuffers { + project_id: self.project_id, + buffer_ids: buffers + .iter() + .map(|buffer| buffer.read(cx).remote_id().to_proto()) + .collect(), + }); - cx.spawn(|this, mut cx| async move { - let response = request - .await? - .transaction - .ok_or_else(|| anyhow!("missing transaction"))?; - this.update(&mut cx, |this, cx| { - this.deserialize_project_transaction(response, push_to_history, cx) - })? - .await - }) + cx.spawn(|this, mut cx| async move { + let response = request + .await? + .transaction + .ok_or_else(|| anyhow!("missing transaction"))?; + this.update(&mut cx, |this, cx| { + this.deserialize_project_transaction(response, push_to_history, cx) + })? + .await }) } } @@ -426,7 +358,7 @@ impl LocalBufferStore { worktree: Model, path: Arc, mut has_changed_file: bool, - cx: &mut ModelContext, + cx: &mut ModelContext, ) -> Task> { let buffer = buffer_handle.read(cx); @@ -449,7 +381,7 @@ impl LocalBufferStore { let new_file = save.await?; let mtime = new_file.disk_state().mtime(); this.update(&mut cx, |this, cx| { - if let Some((downstream_client, project_id)) = this.downstream_client(cx) { + if let Some((downstream_client, project_id)) = this.downstream_client.clone() { if has_changed_file { downstream_client .send(proto::UpdateBufferFile { @@ -478,15 +410,24 @@ impl LocalBufferStore { }) } - fn subscribe_to_worktree(&mut self, worktree: &Model, cx: &mut ModelContext) { + fn subscribe_to_worktree( + &mut self, + worktree: &Model, + cx: &mut ModelContext, + ) { cx.subscribe(worktree, |this, worktree, event, cx| { if worktree.read(cx).is_local() { match event { worktree::Event::UpdatedEntries(changes) => { - this.local_worktree_entries_changed(&worktree, changes, cx); + Self::local_worktree_entries_changed(this, &worktree, changes, cx); } worktree::Event::UpdatedGitRepositories(updated_repos) => { - this.local_worktree_git_repos_changed(worktree.clone(), updated_repos, cx) + Self::local_worktree_git_repos_changed( + this, + worktree.clone(), + updated_repos, + cx, + ) } _ => {} } @@ -496,66 +437,67 @@ impl LocalBufferStore { } fn local_worktree_entries_changed( - &mut self, + this: &mut BufferStore, worktree_handle: &Model, changes: &[(Arc, ProjectEntryId, PathChange)], - cx: &mut ModelContext, + cx: &mut ModelContext, ) { let snapshot = worktree_handle.read(cx).snapshot(); for (path, entry_id, _) in changes { - self.local_worktree_entry_changed(*entry_id, path, worktree_handle, &snapshot, cx); + Self::local_worktree_entry_changed( + this, + *entry_id, + path, + worktree_handle, + &snapshot, + cx, + ); } } fn local_worktree_git_repos_changed( - &mut self, + this: &mut BufferStore, worktree_handle: Model, changed_repos: &UpdatedGitRepositoriesSet, - cx: &mut ModelContext, + cx: &mut ModelContext, ) { debug_assert!(worktree_handle.read(cx).is_local()); - let Some(buffer_store) = self.buffer_store.upgrade() else { - return; - }; // Identify the loading buffers whose containing repository that has changed. - let (future_buffers, current_buffers) = buffer_store.update(cx, |buffer_store, cx| { - let future_buffers = buffer_store - .loading_buffers() - .filter_map(|(project_path, receiver)| { - if project_path.worktree_id != worktree_handle.read(cx).id() { - return None; - } - let path = &project_path.path; - changed_repos - .iter() - .find(|(work_dir, _)| path.starts_with(work_dir))?; - let path = path.clone(); - Some(async move { - BufferStore::wait_for_loading_buffer(receiver) - .await - .ok() - .map(|buffer| (buffer, path)) - }) + let future_buffers = this + .loading_buffers() + .filter_map(|(project_path, receiver)| { + if project_path.worktree_id != worktree_handle.read(cx).id() { + return None; + } + let path = &project_path.path; + changed_repos + .iter() + .find(|(work_dir, _)| path.starts_with(work_dir))?; + let path = path.clone(); + Some(async move { + BufferStore::wait_for_loading_buffer(receiver) + .await + .ok() + .map(|buffer| (buffer, path)) }) - .collect::>(); + }) + .collect::>(); - // Identify the current buffers whose containing repository has changed. - let current_buffers = buffer_store - .buffers() - .filter_map(|buffer| { - let file = File::from_dyn(buffer.read(cx).file())?; - if file.worktree != worktree_handle { - return None; - } - changed_repos - .iter() - .find(|(work_dir, _)| file.path.starts_with(work_dir))?; - Some((buffer, file.path.clone())) - }) - .collect::>(); - (future_buffers, current_buffers) - }); + // Identify the current buffers whose containing repository has changed. + let current_buffers = this + .buffers() + .filter_map(|buffer| { + let file = File::from_dyn(buffer.read(cx).file())?; + if file.worktree != worktree_handle { + return None; + } + changed_repos + .iter() + .find(|(work_dir, _)| file.path.starts_with(work_dir))?; + Some((buffer, file.path.clone())) + }) + .collect::>(); if future_buffers.len() + current_buffers.len() == 0 { return; @@ -603,7 +545,7 @@ impl LocalBufferStore { buffer.set_diff_base(diff_base.clone(), cx); buffer.remote_id().to_proto() }); - if let Some((client, project_id)) = &this.downstream_client(cx) { + if let Some((client, project_id)) = &this.downstream_client.clone() { client .send(proto::UpdateDiffBase { project_id: *project_id, @@ -619,42 +561,44 @@ impl LocalBufferStore { } fn local_worktree_entry_changed( - &mut self, + this: &mut BufferStore, entry_id: ProjectEntryId, path: &Arc, worktree: &Model, snapshot: &worktree::Snapshot, - cx: &mut ModelContext, + cx: &mut ModelContext, ) -> Option<()> { let project_path = ProjectPath { worktree_id: snapshot.id(), path: path.clone(), }; - let buffer_id = match self.local_buffer_ids_by_entry_id.get(&entry_id) { - Some(&buffer_id) => buffer_id, - None => self.local_buffer_ids_by_path.get(&project_path).copied()?, + + let buffer_id = { + let local = this.as_local_mut()?; + match local.local_buffer_ids_by_entry_id.get(&entry_id) { + Some(&buffer_id) => buffer_id, + None => local.local_buffer_ids_by_path.get(&project_path).copied()?, + } }; - let buffer = self - .buffer_store - .update(cx, |buffer_store, _| { - if let Some(buffer) = buffer_store.get(buffer_id) { - Some(buffer) - } else { - buffer_store.opened_buffers.remove(&buffer_id); - None - } - }) - .ok() - .flatten(); + + let buffer = if let Some(buffer) = this.get(buffer_id) { + Some(buffer) + } else { + this.opened_buffers.remove(&buffer_id); + None + }; + let buffer = if let Some(buffer) = buffer { buffer } else { - self.local_buffer_ids_by_path.remove(&project_path); - self.local_buffer_ids_by_entry_id.remove(&entry_id); + let this = this.as_local_mut()?; + this.local_buffer_ids_by_path.remove(&project_path); + this.local_buffer_ids_by_entry_id.remove(&entry_id); return None; }; let events = buffer.update(cx, |buffer, cx| { + let local = this.as_local_mut()?; let file = buffer.file()?; let old_file = File::from_dyn(Some(file))?; if old_file.worktree != *worktree { @@ -695,11 +639,11 @@ impl LocalBufferStore { let mut events = Vec::new(); if new_file.path != old_file.path { - self.local_buffer_ids_by_path.remove(&ProjectPath { + local.local_buffer_ids_by_path.remove(&ProjectPath { path: old_file.path.clone(), worktree_id: old_file.worktree_id(cx), }); - self.local_buffer_ids_by_path.insert( + local.local_buffer_ids_by_path.insert( ProjectPath { worktree_id: new_file.worktree_id(cx), path: new_file.path.clone(), @@ -714,15 +658,16 @@ impl LocalBufferStore { if new_file.entry_id != old_file.entry_id { if let Some(entry_id) = old_file.entry_id { - self.local_buffer_ids_by_entry_id.remove(&entry_id); + local.local_buffer_ids_by_entry_id.remove(&entry_id); } if let Some(entry_id) = new_file.entry_id { - self.local_buffer_ids_by_entry_id + local + .local_buffer_ids_by_entry_id .insert(entry_id, buffer_id); } } - if let Some((client, project_id)) = &self.downstream_client(cx) { + if let Some((client, project_id)) = &this.downstream_client { client .send(proto::UpdateBufferFile { project_id: *project_id, @@ -735,25 +680,14 @@ impl LocalBufferStore { buffer.file_updated(Arc::new(new_file), cx); Some(events) })?; - self.buffer_store - .update(cx, |_buffer_store, cx| { - for event in events { - cx.emit(event); - } - }) - .log_err()?; + + for event in events { + cx.emit(event); + } None } - fn downstream_client(&self, cx: &AppContext) -> Option<(AnyProtoClient, u64)> { - self.buffer_store - .upgrade()? - .read(cx) - .downstream_client - .clone() - } - fn buffer_changed_file(&mut self, buffer: Model, cx: &mut AppContext) -> Option<()> { let file = File::from_dyn(buffer.read(cx).file())?; @@ -779,29 +713,17 @@ impl LocalBufferStore { Some(()) } -} - -impl BufferStoreImpl for Model { - fn as_remote(&self) -> Option> { - None - } - - fn as_local(&self) -> Option> { - Some(self.clone()) - } fn save_buffer( &self, buffer: Model, cx: &mut ModelContext, ) -> Task> { - self.update(cx, |this, cx| { - let Some(file) = File::from_dyn(buffer.read(cx).file()) else { - return Task::ready(Err(anyhow!("buffer doesn't have a file"))); - }; - let worktree = file.worktree.clone(); - this.save_local_buffer(buffer, worktree, file.path.clone(), false, cx) - }) + let Some(file) = File::from_dyn(buffer.read(cx).file()) else { + return Task::ready(Err(anyhow!("buffer doesn't have a file"))); + }; + let worktree = file.worktree.clone(); + self.save_local_buffer(buffer, worktree, file.path.clone(), false, cx) } fn save_buffer_as( @@ -810,16 +732,14 @@ impl BufferStoreImpl for Model { path: ProjectPath, cx: &mut ModelContext, ) -> Task> { - self.update(cx, |this, cx| { - let Some(worktree) = this - .worktree_store - .read(cx) - .worktree_for_id(path.worktree_id, cx) - else { - return Task::ready(Err(anyhow!("no such worktree"))); - }; - this.save_local_buffer(buffer, worktree, path.path.clone(), true, cx) - }) + let Some(worktree) = self + .worktree_store + .read(cx) + .worktree_for_id(path.worktree_id, cx) + else { + return Task::ready(Err(anyhow!("no such worktree"))); + }; + self.save_local_buffer(buffer, worktree, path.path.clone(), true, cx) } fn open_buffer( @@ -828,76 +748,72 @@ impl BufferStoreImpl for Model { worktree: Model, cx: &mut ModelContext, ) -> Task>> { - let buffer_store = cx.weak_model(); - self.update(cx, |_, cx| { - let load_buffer = worktree.update(cx, |worktree, cx| { - let load_file = worktree.load_file(path.as_ref(), cx); - let reservation = cx.reserve_model(); - let buffer_id = BufferId::from(reservation.entity_id().as_non_zero_u64()); - cx.spawn(move |_, mut cx| async move { - let loaded = load_file.await?; - let text_buffer = cx - .background_executor() - .spawn(async move { text::Buffer::new(0, buffer_id, loaded.text) }) - .await; - cx.insert_model(reservation, |_| { - Buffer::build( - text_buffer, - loaded.diff_base, - Some(loaded.file), - Capability::ReadWrite, - ) - }) + let load_buffer = worktree.update(cx, |worktree, cx| { + let load_file = worktree.load_file(path.as_ref(), cx); + let reservation = cx.reserve_model(); + let buffer_id = BufferId::from(reservation.entity_id().as_non_zero_u64()); + cx.spawn(move |_, mut cx| async move { + let loaded = load_file.await?; + let text_buffer = cx + .background_executor() + .spawn(async move { text::Buffer::new(0, buffer_id, loaded.text) }) + .await; + cx.insert_model(reservation, |_| { + Buffer::build( + text_buffer, + loaded.diff_base, + Some(loaded.file), + Capability::ReadWrite, + ) }) - }); - - cx.spawn(move |this, mut cx| async move { - let buffer = match load_buffer.await { - Ok(buffer) => Ok(buffer), - Err(error) if is_not_found_error(&error) => cx.new_model(|cx| { - let buffer_id = BufferId::from(cx.entity_id().as_non_zero_u64()); - let text_buffer = text::Buffer::new(0, buffer_id, "".into()); - Buffer::build( - text_buffer, - None, - Some(Arc::new(File { - worktree, - path, - disk_state: DiskState::New, - entry_id: None, - is_local: true, - is_private: false, - })), - Capability::ReadWrite, - ) - }), - Err(e) => Err(e), - }?; - this.update(&mut cx, |this, cx| { - buffer_store.update(cx, |buffer_store, cx| { - buffer_store.add_buffer(buffer.clone(), cx) - })??; - let buffer_id = buffer.read(cx).remote_id(); - if let Some(file) = File::from_dyn(buffer.read(cx).file()) { - this.local_buffer_ids_by_path.insert( - ProjectPath { - worktree_id: file.worktree_id(cx), - path: file.path.clone(), - }, - buffer_id, - ); - - if let Some(entry_id) = file.entry_id { - this.local_buffer_ids_by_entry_id - .insert(entry_id, buffer_id); - } - } - - anyhow::Ok(()) - })??; - - Ok(buffer) }) + }); + + cx.spawn(move |this, mut cx| async move { + let buffer = match load_buffer.await { + Ok(buffer) => Ok(buffer), + Err(error) if is_not_found_error(&error) => cx.new_model(|cx| { + let buffer_id = BufferId::from(cx.entity_id().as_non_zero_u64()); + let text_buffer = text::Buffer::new(0, buffer_id, "".into()); + Buffer::build( + text_buffer, + None, + Some(Arc::new(File { + worktree, + path, + disk_state: DiskState::New, + entry_id: None, + is_local: true, + is_private: false, + })), + Capability::ReadWrite, + ) + }), + Err(e) => Err(e), + }?; + this.update(&mut cx, |this, cx| { + this.add_buffer(buffer.clone(), cx)?; + let buffer_id = buffer.read(cx).remote_id(); + if let Some(file) = File::from_dyn(buffer.read(cx).file()) { + let this = this.as_local_mut().unwrap(); + this.local_buffer_ids_by_path.insert( + ProjectPath { + worktree_id: file.worktree_id(cx), + path: file.path.clone(), + }, + buffer_id, + ); + + if let Some(entry_id) = file.entry_id { + this.local_buffer_ids_by_entry_id + .insert(entry_id, buffer_id); + } + } + + anyhow::Ok(()) + })??; + + Ok(buffer) }) } @@ -954,26 +870,18 @@ impl BufferStore { /// Creates a buffer store, optionally retaining its buffers. pub fn local(worktree_store: Model, cx: &mut ModelContext) -> Self { - let this = cx.weak_model(); Self { - state: Box::new(cx.new_model(|cx| { - let subscription = cx.subscribe( - &worktree_store, - |this: &mut LocalBufferStore, _, event, cx| { - if let WorktreeStoreEvent::WorktreeAdded(worktree) = event { - this.subscribe_to_worktree(worktree, cx); - } - }, - ); - - LocalBufferStore { - local_buffer_ids_by_path: Default::default(), - local_buffer_ids_by_entry_id: Default::default(), - buffer_store: this, - worktree_store: worktree_store.clone(), - _subscription: subscription, - } - })), + state: BufferStoreState::Local(LocalBufferStore { + local_buffer_ids_by_path: Default::default(), + local_buffer_ids_by_entry_id: Default::default(), + worktree_store: worktree_store.clone(), + _subscription: cx.subscribe(&worktree_store, |this, _, event, cx| { + if let WorktreeStoreEvent::WorktreeAdded(worktree) = event { + let this = this.as_local_mut().unwrap(); + this.subscribe_to_worktree(worktree, cx); + } + }), + }), downstream_client: None, opened_buffers: Default::default(), shared_buffers: Default::default(), @@ -986,19 +894,17 @@ impl BufferStore { worktree_store: Model, upstream_client: AnyProtoClient, remote_id: u64, - cx: &mut ModelContext, + _cx: &mut ModelContext, ) -> Self { - let this = cx.weak_model(); Self { - state: Box::new(cx.new_model(|_| RemoteBufferStore { + state: BufferStoreState::Remote(RemoteBufferStore { shared_with_me: Default::default(), loading_remote_buffers_by_id: Default::default(), remote_buffer_listeners: Default::default(), project_id: remote_id, upstream_client, worktree_store: worktree_store.clone(), - buffer_store: this, - })), + }), downstream_client: None, opened_buffers: Default::default(), loading_buffers_by_path: Default::default(), @@ -1007,6 +913,27 @@ impl BufferStore { } } + fn as_local_mut(&mut self) -> Option<&mut LocalBufferStore> { + match &mut self.state { + BufferStoreState::Local(state) => Some(state), + _ => None, + } + } + + fn as_remote_mut(&mut self) -> Option<&mut RemoteBufferStore> { + match &mut self.state { + BufferStoreState::Remote(state) => Some(state), + _ => None, + } + } + + fn as_remote(&self) -> Option<&RemoteBufferStore> { + match &self.state { + BufferStoreState::Remote(state) => Some(state), + _ => None, + } + } + pub fn open_buffer( &mut self, project_path: ProjectPath, @@ -1035,10 +962,11 @@ impl BufferStore { let (mut tx, rx) = postage::watch::channel(); entry.insert(rx.clone()); - let project_path = project_path.clone(); - let load_buffer = self - .state - .open_buffer(project_path.path.clone(), worktree, cx); + let path = project_path.path.clone(); + let load_buffer = match &self.state { + BufferStoreState::Local(this) => this.open_buffer(path, worktree, cx), + BufferStoreState::Remote(this) => this.open_buffer(path, worktree, cx), + }; cx.spawn(move |this, mut cx| async move { let load_result = load_buffer.await; @@ -1063,7 +991,10 @@ impl BufferStore { } pub fn create_buffer(&mut self, cx: &mut ModelContext) -> Task>> { - self.state.create_buffer(cx) + match &self.state { + BufferStoreState::Local(this) => this.create_buffer(cx), + BufferStoreState::Remote(this) => this.create_buffer(cx), + } } pub fn save_buffer( @@ -1071,7 +1002,10 @@ impl BufferStore { buffer: Model, cx: &mut ModelContext, ) -> Task> { - self.state.save_buffer(buffer, cx) + match &mut self.state { + BufferStoreState::Local(this) => this.save_buffer(buffer, cx), + BufferStoreState::Remote(this) => this.save_remote_buffer(buffer.clone(), None, cx), + } } pub fn save_buffer_as( @@ -1081,7 +1015,12 @@ impl BufferStore { cx: &mut ModelContext, ) -> Task> { let old_file = buffer.read(cx).file().cloned(); - let task = self.state.save_buffer_as(buffer.clone(), path, cx); + let task = match &self.state { + BufferStoreState::Local(this) => this.save_buffer_as(buffer.clone(), path, cx), + BufferStoreState::Remote(this) => { + this.save_remote_buffer(buffer.clone(), Some(path.to_proto()), cx) + } + }; cx.spawn(|this, mut cx| async move { task.await?; this.update(&mut cx, |_, cx| { @@ -1306,19 +1245,10 @@ impl BufferStore { .ok_or_else(|| anyhow!("unknown buffer id {}", buffer_id)) } - pub fn get_possibly_incomplete( - &self, - buffer_id: BufferId, - cx: &AppContext, - ) -> Option> { + pub fn get_possibly_incomplete(&self, buffer_id: BufferId) -> Option> { self.get(buffer_id).or_else(|| { - self.state.as_remote().and_then(|remote| { - remote - .read(cx) - .loading_remote_buffers_by_id - .get(&buffer_id) - .cloned() - }) + self.as_remote() + .and_then(|remote| remote.loading_remote_buffers_by_id.get(&buffer_id).cloned()) }) } @@ -1337,9 +1267,8 @@ impl BufferStore { }) .collect(); let incomplete_buffer_ids = self - .state .as_remote() - .map(|remote| remote.read(cx).incomplete_buffer_ids()) + .map(|remote| remote.incomplete_buffer_ids()) .unwrap_or_default(); (buffers, incomplete_buffer_ids) } @@ -1357,12 +1286,10 @@ impl BufferStore { }); } - if let Some(remote) = self.state.as_remote() { - remote.update(cx, |remote, _| { - // Wake up all futures currently waiting on a buffer to get opened, - // to give them a chance to fail now that we've disconnected. - remote.remote_buffer_listeners.clear() - }) + if let Some(remote) = self.as_remote_mut() { + // Wake up all futures currently waiting on a buffer to get opened, + // to give them a chance to fail now that we've disconnected. + remote.remote_buffer_listeners.clear() } } @@ -1447,10 +1374,8 @@ impl BufferStore { ) { match event { BufferEvent::FileHandleChanged => { - if let Some(local) = self.state.as_local() { - local.update(cx, |local, cx| { - local.buffer_changed_file(buffer, cx); - }) + if let Some(local) = self.as_local_mut() { + local.buffer_changed_file(buffer, cx); } } BufferEvent::Reloaded => { @@ -1593,13 +1518,13 @@ impl BufferStore { capability: Capability, cx: &mut ModelContext, ) -> Result<()> { - let Some(remote) = self.state.as_remote() else { + let Some(remote) = self.as_remote_mut() else { return Err(anyhow!("buffer store is not a remote")); }; - if let Some(buffer) = remote.update(cx, |remote, cx| { - remote.handle_create_buffer_for_peer(envelope, replica_id, capability, cx) - })? { + if let Some(buffer) = + remote.handle_create_buffer_for_peer(envelope, replica_id, capability, cx)? + { self.add_buffer(buffer, cx)?; } @@ -1616,7 +1541,7 @@ impl BufferStore { this.update(&mut cx, |this, cx| { let payload = envelope.payload.clone(); - if let Some(buffer) = this.get_possibly_incomplete(buffer_id, cx) { + if let Some(buffer) = this.get_possibly_incomplete(buffer_id) { let file = payload.file.ok_or_else(|| anyhow!("invalid file"))?; let worktree = this .worktree_store @@ -1662,7 +1587,7 @@ impl BufferStore { this.update(&mut cx, |this, cx| { let buffer_id = envelope.payload.buffer_id; let buffer_id = BufferId::new(buffer_id)?; - if let Some(buffer) = this.get_possibly_incomplete(buffer_id, cx) { + if let Some(buffer) = this.get_possibly_incomplete(buffer_id) { buffer.update(cx, |buffer, cx| { buffer.set_diff_base(envelope.payload.diff_base.clone(), cx) }); @@ -1756,7 +1681,7 @@ impl BufferStore { let version = deserialize_version(&envelope.payload.version); let mtime = envelope.payload.mtime.clone().map(|time| time.into()); this.update(&mut cx, move |this, cx| { - if let Some(buffer) = this.get_possibly_incomplete(buffer_id, cx) { + if let Some(buffer) = this.get_possibly_incomplete(buffer_id) { buffer.update(cx, |buffer, cx| { buffer.did_save(version, mtime, cx); }); @@ -1788,7 +1713,7 @@ impl BufferStore { .ok_or_else(|| anyhow!("missing line ending"))?, ); this.update(&mut cx, |this, cx| { - if let Some(buffer) = this.get_possibly_incomplete(buffer_id, cx) { + if let Some(buffer) = this.get_possibly_incomplete(buffer_id) { buffer.update(cx, |buffer, cx| { buffer.did_reload(version, line_ending, mtime, cx); }); @@ -1877,8 +1802,10 @@ impl BufferStore { if buffers.is_empty() { return Task::ready(Ok(ProjectTransaction::default())); } - - self.state.reload_buffers(buffers, push_to_history, cx) + match &self.state { + BufferStoreState::Local(this) => this.reload_buffers(buffers, push_to_history, cx), + BufferStoreState::Remote(this) => this.reload_buffers(buffers, push_to_history, cx), + } } async fn handle_reload_buffers( @@ -2000,26 +1927,23 @@ impl BufferStore { self.add_buffer(buffer.clone(), cx).log_err(); let buffer_id = buffer.read(cx).remote_id(); - let local = self - .state - .as_local() + let this = self + .as_local_mut() .expect("local-only method called in a non-local context"); - local.update(cx, |this, cx| { - if let Some(file) = File::from_dyn(buffer.read(cx).file()) { - this.local_buffer_ids_by_path.insert( - ProjectPath { - worktree_id: file.worktree_id(cx), - path: file.path.clone(), - }, - buffer_id, - ); + if let Some(file) = File::from_dyn(buffer.read(cx).file()) { + this.local_buffer_ids_by_path.insert( + ProjectPath { + worktree_id: file.worktree_id(cx), + path: file.path.clone(), + }, + buffer_id, + ); - if let Some(entry_id) = file.entry_id { - this.local_buffer_ids_by_entry_id - .insert(entry_id, buffer_id); - } + if let Some(entry_id) = file.entry_id { + this.local_buffer_ids_by_entry_id + .insert(entry_id, buffer_id); } - }); + } buffer } @@ -2029,10 +1953,8 @@ impl BufferStore { push_to_history: bool, cx: &mut ModelContext, ) -> Task> { - if let Some(remote) = self.state.as_remote() { - remote.update(cx, |remote, cx| { - remote.deserialize_project_transaction(message, push_to_history, cx) - }) + if let Some(this) = self.as_remote_mut() { + this.deserialize_project_transaction(message, push_to_history, cx) } else { debug_panic!("not a remote buffer store"); Task::ready(Err(anyhow!("not a remote buffer store"))) @@ -2040,12 +1962,12 @@ impl BufferStore { } pub fn wait_for_remote_buffer( - &self, + &mut self, id: BufferId, - cx: &mut AppContext, + cx: &mut ModelContext, ) -> Task>> { - if let Some(remote) = self.state.as_remote() { - remote.update(cx, |remote, cx| remote.wait_for_remote_buffer(id, cx)) + if let Some(this) = self.as_remote_mut() { + this.wait_for_remote_buffer(id, cx) } else { debug_panic!("not a remote buffer store"); Task::ready(Err(anyhow!("not a remote buffer store"))) From 9ee1aba80a31d0bfb8ccb623f60e793d80d8e6e1 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 25 Nov 2024 16:13:27 -0500 Subject: [PATCH 144/886] assistant2: Stream in completion text (#21182) This PR makes it so that the completion text streams into the message list rather than being buffered until the end. Release Notes: - N/A --- crates/assistant2/src/assistant_panel.rs | 5 +- crates/assistant2/src/message_editor.rs | 51 +---------------- crates/assistant2/src/thread.rs | 70 +++++++++++++++++++++++- 3 files changed, 75 insertions(+), 51 deletions(-) diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index 88a3f73176..abbb2f20db 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -1,7 +1,7 @@ use anyhow::Result; use gpui::{ prelude::*, px, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, - FocusableView, Model, Pixels, Task, View, ViewContext, WeakView, WindowContext, + FocusableView, Model, Pixels, Subscription, Task, View, ViewContext, WeakView, WindowContext, }; use language_model::LanguageModelRegistry; use language_model_selector::LanguageModelSelector; @@ -28,6 +28,7 @@ pub struct AssistantPanel { pane: View, thread: Model, message_editor: View, + _subscriptions: Vec, } impl AssistantPanel { @@ -59,11 +60,13 @@ impl AssistantPanel { }); let thread = cx.new_model(Thread::new); + let subscriptions = vec![cx.observe(&thread, |_, _, cx| cx.notify())]; Self { pane, thread: thread.clone(), message_editor: cx.new_view(|cx| MessageEditor::new(thread, cx)), + _subscriptions: subscriptions, } } } diff --git a/crates/assistant2/src/message_editor.rs b/crates/assistant2/src/message_editor.rs index 63f8c869d4..d195682cb3 100644 --- a/crates/assistant2/src/message_editor.rs +++ b/crates/assistant2/src/message_editor.rs @@ -1,14 +1,11 @@ use editor::{Editor, EditorElement, EditorStyle}; -use futures::StreamExt; use gpui::{AppContext, Model, TextStyle, View}; use language_model::{ - LanguageModelCompletionEvent, LanguageModelRegistry, LanguageModelRequest, - LanguageModelRequestMessage, MessageContent, Role, StopReason, + LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, MessageContent, Role, }; use settings::Settings; use theme::ThemeSettings; use ui::{prelude::*, ButtonLike, ElevationIndex, KeyBinding}; -use util::ResultExt; use crate::thread::{self, Thread}; use crate::Chat; @@ -71,50 +68,8 @@ impl MessageEditor { editor.clear(cx); }); - let task = cx.spawn(|this, mut cx| async move { - let stream = model.stream_completion(request, &cx); - let stream_completion = async { - let mut events = stream.await?; - let mut stop_reason = StopReason::EndTurn; - - let mut text = String::new(); - - while let Some(event) = events.next().await { - let event = event?; - match event { - LanguageModelCompletionEvent::StartMessage { .. } => {} - LanguageModelCompletionEvent::Stop(reason) => { - stop_reason = reason; - } - LanguageModelCompletionEvent::Text(chunk) => { - text.push_str(&chunk); - } - LanguageModelCompletionEvent::ToolUse(_tool_use) => {} - } - - smol::future::yield_now().await; - } - - anyhow::Ok((stop_reason, text)) - }; - - let result = stream_completion.await; - - this.update(&mut cx, |this, cx| { - if let Some((_stop_reason, text)) = result.log_err() { - this.thread.update(cx, |thread, _cx| { - thread.messages.push(thread::Message { - role: Role::Assistant, - text, - }); - }); - } - }) - .ok(); - }); - - self.thread.update(cx, |thread, _cx| { - thread.pending_completion_tasks.push(task); + self.thread.update(cx, |thread, cx| { + thread.stream_completion(request, model, cx) }); None diff --git a/crates/assistant2/src/thread.rs b/crates/assistant2/src/thread.rs index 1553eaabb6..a6c870b456 100644 --- a/crates/assistant2/src/thread.rs +++ b/crates/assistant2/src/thread.rs @@ -1,5 +1,11 @@ -use gpui::{ModelContext, Task}; -use language_model::Role; +use std::sync::Arc; + +use futures::StreamExt as _; +use gpui::{EventEmitter, ModelContext, Task}; +use language_model::{ + LanguageModel, LanguageModelCompletionEvent, LanguageModelRequest, Role, StopReason, +}; +use util::ResultExt as _; /// A message in a [`Thread`]. pub struct Message { @@ -20,4 +26,64 @@ impl Thread { pending_completion_tasks: Vec::new(), } } + + pub fn stream_completion( + &mut self, + request: LanguageModelRequest, + model: Arc, + cx: &mut ModelContext, + ) { + let task = cx.spawn(|this, mut cx| async move { + let stream = model.stream_completion(request, &cx); + let stream_completion = async { + let mut events = stream.await?; + let mut stop_reason = StopReason::EndTurn; + + while let Some(event) = events.next().await { + let event = event?; + + this.update(&mut cx, |thread, cx| { + match event { + LanguageModelCompletionEvent::StartMessage { .. } => { + thread.messages.push(Message { + role: Role::Assistant, + text: String::new(), + }); + } + LanguageModelCompletionEvent::Stop(reason) => { + stop_reason = reason; + } + LanguageModelCompletionEvent::Text(chunk) => { + if let Some(last_message) = thread.messages.last_mut() { + if last_message.role == Role::Assistant { + last_message.text.push_str(&chunk); + } + } + } + LanguageModelCompletionEvent::ToolUse(_tool_use) => {} + } + + cx.emit(ThreadEvent::StreamedCompletion); + cx.notify(); + })?; + + smol::future::yield_now().await; + } + + anyhow::Ok(stop_reason) + }; + + let result = stream_completion.await; + let _ = result.log_err(); + }); + + self.pending_completion_tasks.push(task); + } } + +#[derive(Debug, Clone)] +pub enum ThreadEvent { + StreamedCompletion, +} + +impl EventEmitter for Thread {} From e7b004756247b04ecfa031424a6c950c6dfbf0ce Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 25 Nov 2024 16:28:38 -0500 Subject: [PATCH 145/886] assistant2: Remove unnecessary `Pane` (#21183) This PR removes an unnecessary `Pane` that was copied over from `assistant::AssistantPanel` to `assistant2::AssistantPanel`. Release Notes: - N/A --- crates/assistant2/src/assistant_panel.rs | 35 ++---------------------- crates/assistant2/src/message_editor.rs | 8 +++++- 2 files changed, 10 insertions(+), 33 deletions(-) diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index abbb2f20db..20ce26fc59 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -7,7 +7,7 @@ use language_model::LanguageModelRegistry; use language_model_selector::LanguageModelSelector; use ui::{prelude::*, ButtonLike, Divider, IconButtonShape, Tab, Tooltip}; use workspace::dock::{DockPosition, Panel, PanelEvent}; -use workspace::{Pane, Workspace}; +use workspace::Workspace; use crate::message_editor::MessageEditor; use crate::thread::Thread; @@ -25,7 +25,6 @@ pub fn init(cx: &mut AppContext) { } pub struct AssistantPanel { - pane: View, thread: Model, message_editor: View, _subscriptions: Vec, @@ -43,27 +42,11 @@ impl AssistantPanel { }) } - fn new(workspace: &Workspace, cx: &mut ViewContext) -> Self { - let pane = cx.new_view(|cx| { - let mut pane = Pane::new( - workspace.weak_handle(), - workspace.project().clone(), - Default::default(), - None, - NewThread.boxed_clone(), - cx, - ); - pane.set_can_split(false, cx); - pane.set_can_navigate(true, cx); - - pane - }); - + fn new(_workspace: &Workspace, cx: &mut ViewContext) -> Self { let thread = cx.new_model(Thread::new); let subscriptions = vec![cx.observe(&thread, |_, _, cx| cx.notify())]; Self { - pane, thread: thread.clone(), message_editor: cx.new_view(|cx| MessageEditor::new(thread, cx)), _subscriptions: subscriptions, @@ -73,7 +56,7 @@ impl AssistantPanel { impl FocusableView for AssistantPanel { fn focus_handle(&self, cx: &AppContext) -> FocusHandle { - self.pane.focus_handle(cx) + self.message_editor.focus_handle(cx) } } @@ -100,20 +83,8 @@ impl Panel for AssistantPanel { fn set_size(&mut self, _size: Option, _cx: &mut ViewContext) {} - fn is_zoomed(&self, cx: &WindowContext) -> bool { - self.pane.read(cx).is_zoomed() - } - - fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext) { - self.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx)); - } - fn set_active(&mut self, _active: bool, _cx: &mut ViewContext) {} - fn pane(&self) -> Option> { - Some(self.pane.clone()) - } - fn remote_id() -> Option { Some(proto::PanelId::AssistantPanel) } diff --git a/crates/assistant2/src/message_editor.rs b/crates/assistant2/src/message_editor.rs index d195682cb3..e1606ff27a 100644 --- a/crates/assistant2/src/message_editor.rs +++ b/crates/assistant2/src/message_editor.rs @@ -1,5 +1,5 @@ use editor::{Editor, EditorElement, EditorStyle}; -use gpui::{AppContext, Model, TextStyle, View}; +use gpui::{AppContext, FocusableView, Model, TextStyle, View}; use language_model::{ LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, MessageContent, Role, }; @@ -97,6 +97,12 @@ impl MessageEditor { } } +impl FocusableView for MessageEditor { + fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle { + self.editor.focus_handle(cx) + } +} + impl Render for MessageEditor { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let font_size = TextSize::Default.rems(cx); From 2b9250843c110b13644c81b7e3abd17a92edc567 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 25 Nov 2024 16:51:32 -0500 Subject: [PATCH 146/886] assistant2: Include previous messages in the thread in the completion request (#21184) This PR makes it so previous messages in the thread are included when constructing the completion request, instead of only sending up the most recent user message. Release Notes: - N/A --- crates/assistant2/src/assistant_panel.rs | 2 +- crates/assistant2/src/message_editor.rs | 48 +++------------------ crates/assistant2/src/thread.rs | 54 ++++++++++++++++++++++-- 3 files changed, 58 insertions(+), 46 deletions(-) diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index 20ce26fc59..f3dd42e4d6 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -234,7 +234,7 @@ impl Render for AssistantPanel { .p_2() .overflow_y_scroll() .bg(cx.theme().colors().panel_background) - .children(self.thread.read(cx).messages.iter().map(|message| { + .children(self.thread.read(cx).messages().map(|message| { v_flex() .p_2() .border_1() diff --git a/crates/assistant2/src/message_editor.rs b/crates/assistant2/src/message_editor.rs index e1606ff27a..f0a8e260bc 100644 --- a/crates/assistant2/src/message_editor.rs +++ b/crates/assistant2/src/message_editor.rs @@ -1,20 +1,13 @@ use editor::{Editor, EditorElement, EditorStyle}; use gpui::{AppContext, FocusableView, Model, TextStyle, View}; -use language_model::{ - LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, MessageContent, Role, -}; +use language_model::LanguageModelRegistry; use settings::Settings; use theme::ThemeSettings; use ui::{prelude::*, ButtonLike, ElevationIndex, KeyBinding}; -use crate::thread::{self, Thread}; +use crate::thread::{RequestKind, Thread}; use crate::Chat; -#[derive(Debug, Clone, Copy)] -pub enum RequestKind { - Chat, -} - pub struct MessageEditor { thread: Model, editor: View, @@ -54,47 +47,20 @@ impl MessageEditor { let model_registry = LanguageModelRegistry::read_global(cx); let model = model_registry.active_model()?; - let request = self.build_completion_request(request_kind, cx); - - let user_message = self.editor.read(cx).text(cx); - self.thread.update(cx, |thread, _cx| { - thread.messages.push(thread::Message { - role: Role::User, - text: user_message, - }); - }); - - self.editor.update(cx, |editor, cx| { + let user_message = self.editor.update(cx, |editor, cx| { + let text = editor.text(cx); editor.clear(cx); + text }); self.thread.update(cx, |thread, cx| { + thread.insert_user_message(user_message); + let request = thread.to_completion_request(request_kind, cx); thread.stream_completion(request, model, cx) }); None } - - fn build_completion_request( - &self, - _request_kind: RequestKind, - cx: &AppContext, - ) -> LanguageModelRequest { - let text = self.editor.read(cx).text(cx); - - let request = LanguageModelRequest { - messages: vec![LanguageModelRequestMessage { - role: Role::User, - content: vec![MessageContent::Text(text)], - cache: false, - }], - tools: Vec::new(), - stop: Vec::new(), - temperature: None, - }; - - request - } } impl FocusableView for MessageEditor { diff --git a/crates/assistant2/src/thread.rs b/crates/assistant2/src/thread.rs index a6c870b456..a433c10267 100644 --- a/crates/assistant2/src/thread.rs +++ b/crates/assistant2/src/thread.rs @@ -1,12 +1,18 @@ use std::sync::Arc; use futures::StreamExt as _; -use gpui::{EventEmitter, ModelContext, Task}; +use gpui::{AppContext, EventEmitter, ModelContext, Task}; use language_model::{ - LanguageModel, LanguageModelCompletionEvent, LanguageModelRequest, Role, StopReason, + LanguageModel, LanguageModelCompletionEvent, LanguageModelRequest, LanguageModelRequestMessage, + MessageContent, Role, StopReason, }; use util::ResultExt as _; +#[derive(Debug, Clone, Copy)] +pub enum RequestKind { + Chat, +} + /// A message in a [`Thread`]. pub struct Message { pub role: Role, @@ -15,8 +21,8 @@ pub struct Message { /// A thread of conversation with the LLM. pub struct Thread { - pub messages: Vec, - pub pending_completion_tasks: Vec>, + messages: Vec, + pending_completion_tasks: Vec>, } impl Thread { @@ -27,6 +33,46 @@ impl Thread { } } + pub fn messages(&self) -> impl Iterator { + self.messages.iter() + } + + pub fn insert_user_message(&mut self, text: impl Into) { + self.messages.push(Message { + role: Role::User, + text: text.into(), + }); + } + + pub fn to_completion_request( + &self, + _request_kind: RequestKind, + _cx: &AppContext, + ) -> LanguageModelRequest { + let mut request = LanguageModelRequest { + messages: vec![], + tools: Vec::new(), + stop: Vec::new(), + temperature: None, + }; + + for message in &self.messages { + let mut request_message = LanguageModelRequestMessage { + role: message.role, + content: Vec::new(), + cache: false, + }; + + request_message + .content + .push(MessageContent::Text(message.text.clone())); + + request.messages.push(request_message); + } + + request + } + pub fn stream_completion( &mut self, request: LanguageModelRequest, From cc5daa22bdf8e549becd5d33e4eb29b72f149c04 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 25 Nov 2024 17:07:55 -0500 Subject: [PATCH 147/886] assistant2: Improve tracking of pending completions (#21186) This PR improves the tracking of pending completions in `assistant2` such that we actually remove ones that have been completed. Release Notes: - N/A --- crates/assistant2/src/thread.rs | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/crates/assistant2/src/thread.rs b/crates/assistant2/src/thread.rs index a433c10267..c1df6c76d3 100644 --- a/crates/assistant2/src/thread.rs +++ b/crates/assistant2/src/thread.rs @@ -6,7 +6,7 @@ use language_model::{ LanguageModel, LanguageModelCompletionEvent, LanguageModelRequest, LanguageModelRequestMessage, MessageContent, Role, StopReason, }; -use util::ResultExt as _; +use util::{post_inc, ResultExt as _}; #[derive(Debug, Clone, Copy)] pub enum RequestKind { @@ -19,17 +19,24 @@ pub struct Message { pub text: String, } +struct PendingCompletion { + id: usize, + _task: Task<()>, +} + /// A thread of conversation with the LLM. pub struct Thread { messages: Vec, - pending_completion_tasks: Vec>, + completion_count: usize, + pending_completions: Vec, } impl Thread { pub fn new(_cx: &mut ModelContext) -> Self { Self { messages: Vec::new(), - pending_completion_tasks: Vec::new(), + completion_count: 0, + pending_completions: Vec::new(), } } @@ -79,7 +86,9 @@ impl Thread { model: Arc, cx: &mut ModelContext, ) { - let task = cx.spawn(|this, mut cx| async move { + let pending_completion_id = post_inc(&mut self.completion_count); + + let task = cx.spawn(|thread, mut cx| async move { let stream = model.stream_completion(request, &cx); let stream_completion = async { let mut events = stream.await?; @@ -88,7 +97,7 @@ impl Thread { while let Some(event) = events.next().await { let event = event?; - this.update(&mut cx, |thread, cx| { + thread.update(&mut cx, |thread, cx| { match event { LanguageModelCompletionEvent::StartMessage { .. } => { thread.messages.push(Message { @@ -116,6 +125,12 @@ impl Thread { smol::future::yield_now().await; } + thread.update(&mut cx, |thread, _cx| { + thread + .pending_completions + .retain(|completion| completion.id != pending_completion_id); + })?; + anyhow::Ok(stop_reason) }; @@ -123,7 +138,10 @@ impl Thread { let _ = result.log_err(); }); - self.pending_completion_tasks.push(task); + self.pending_completions.push(PendingCompletion { + id: pending_completion_id, + _task: task, + }); } } From 321fd19763da4526845e47e58ba4e7e706787134 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 25 Nov 2024 17:24:25 -0500 Subject: [PATCH 148/886] assistant2: Wire up `assistant2::NewThread` action (#21187) This PR wires up the `assistant2::NewThread` action so that you can create new threads. Release Notes: - N/A --- crates/assistant2/src/assistant_panel.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index f3dd42e4d6..c33e9d520d 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -52,6 +52,17 @@ impl AssistantPanel { _subscriptions: subscriptions, } } + + fn new_thread(&mut self, cx: &mut ViewContext) { + let thread = cx.new_model(Thread::new); + let subscriptions = vec![cx.observe(&thread, |_, _, cx| cx.notify())]; + + self.message_editor = cx.new_view(|cx| MessageEditor::new(thread.clone(), cx)); + self.thread = thread; + self._subscriptions = subscriptions; + + self.message_editor.focus_handle(cx).focus(cx); + } } impl FocusableView for AssistantPanel { @@ -222,8 +233,8 @@ impl Render for AssistantPanel { .key_context("AssistantPanel2") .justify_between() .size_full() - .on_action(cx.listener(|_this, _: &NewThread, _cx| { - println!("Action: New Thread"); + .on_action(cx.listener(|this, _: &NewThread, cx| { + this.new_thread(cx); })) .child(self.render_toolbar(cx)) .child( From 3901d4610115989d1b7e4d5c637a297da8219809 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 25 Nov 2024 18:26:34 -0500 Subject: [PATCH 149/886] Factor tool definitions out of `assistant` (#21189) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR factors the tool definitions out of the `assistant` crate so that they can be shared between `assistant` and `assistant2`. `ToolWorkingSet` now lives in `assistant_tool`. The tool definitions themselves live in `assistant_tools`, with the exception of the `ContextServerTool`, which has been moved to the `context_server` crate. As part of this refactoring I needed to extract the `ContextServerSettings` to a separate `context_server_settings` crate so that the `extension_host`—which is referenced by the `remote_server`—can name the `ContextServerSettings` type without pulling in some undesired dependencies. Release Notes: - N/A --- Cargo.lock | 42 +++++++++++-- Cargo.toml | 8 ++- crates/assistant/Cargo.toml | 2 +- crates/assistant/src/assistant.rs | 12 +--- crates/assistant/src/assistant_panel.rs | 4 +- crates/assistant/src/context.rs | 2 +- crates/assistant/src/context/context_tests.rs | 2 +- crates/assistant/src/context_store.rs | 19 +++--- .../slash_command/context_server_command.rs | 12 ++-- crates/assistant/src/tools.rs | 2 - crates/assistant_tool/src/assistant_tool.rs | 4 +- .../src/tool_working_set.rs | 6 +- crates/assistant_tools/Cargo.toml | 22 +++++++ .../LICENSE-GPL | 0 crates/assistant_tools/src/assistant_tools.rs | 13 ++++ .../tools => assistant_tools/src}/now_tool.rs | 0 crates/collab/Cargo.toml | 3 +- crates/collab/src/tests/integration_tests.rs | 7 ++- .../Cargo.toml | 9 ++- crates/context_server/LICENSE-GPL | 1 + .../src/client.rs | 0 .../src/context_server.rs} | 7 ++- .../src}/context_server_tool.rs | 5 +- .../src/extension_context_server.rs | 3 +- .../src/manager.rs | 56 +---------------- .../src/protocol.rs | 0 .../src/registry.rs | 2 +- .../src/types.rs | 0 crates/context_server_settings/Cargo.toml | 21 +++++++ crates/context_server_settings/LICENSE-GPL | 1 + .../src/context_server_settings.rs | 61 +++++++++++++++++++ crates/extension_host/Cargo.toml | 2 +- .../src/wasm_host/wit/since_v0_2_0.rs | 2 +- crates/zed/Cargo.toml | 1 + crates/zed/src/main.rs | 1 + 35 files changed, 219 insertions(+), 113 deletions(-) delete mode 100644 crates/assistant/src/tools.rs rename crates/{assistant => assistant_tool}/src/tool_working_set.rs (98%) create mode 100644 crates/assistant_tools/Cargo.toml rename crates/{context_servers => assistant_tools}/LICENSE-GPL (100%) create mode 100644 crates/assistant_tools/src/assistant_tools.rs rename crates/{assistant/src/tools => assistant_tools/src}/now_tool.rs (100%) rename crates/{context_servers => context_server}/Cargo.toml (76%) create mode 120000 crates/context_server/LICENSE-GPL rename crates/{context_servers => context_server}/src/client.rs (100%) rename crates/{context_servers/src/context_servers.rs => context_server/src/context_server.rs} (76%) rename crates/{assistant/src/tools => context_server/src}/context_server_tool.rs (97%) rename crates/{context_servers => context_server}/src/extension_context_server.rs (97%) rename crates/{context_servers => context_server}/src/manager.rs (84%) rename crates/{context_servers => context_server}/src/protocol.rs (100%) rename crates/{context_servers => context_server}/src/registry.rs (98%) rename crates/{context_servers => context_server}/src/types.rs (100%) create mode 100644 crates/context_server_settings/Cargo.toml create mode 120000 crates/context_server_settings/LICENSE-GPL create mode 100644 crates/context_server_settings/src/context_server_settings.rs diff --git a/Cargo.lock b/Cargo.lock index b8c24b4594..7152bf8d08 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -383,7 +383,7 @@ dependencies = [ "clock", "collections", "command_palette_hooks", - "context_servers", + "context_server", "ctor", "db", "editor", @@ -506,6 +506,20 @@ dependencies = [ "workspace", ] +[[package]] +name = "assistant_tools" +version = "0.1.0" +dependencies = [ + "anyhow", + "assistant_tool", + "chrono", + "gpui", + "schemars", + "serde", + "serde_json", + "workspace", +] + [[package]] name = "async-attributes" version = "1.1.2" @@ -2613,6 +2627,7 @@ dependencies = [ "anthropic", "anyhow", "assistant", + "assistant_tool", "async-stripe", "async-trait", "async-tungstenite 0.28.0", @@ -2631,7 +2646,7 @@ dependencies = [ "clock", "collab_ui", "collections", - "context_servers", + "context_server", "ctor", "dashmap 6.1.0", "derive_more", @@ -2874,12 +2889,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" [[package]] -name = "context_servers" +name = "context_server" version = "0.1.0" dependencies = [ "anyhow", + "assistant_tool", "collections", "command_palette_hooks", + "context_server_settings", "extension", "futures 0.3.31", "gpui", @@ -2887,13 +2904,27 @@ dependencies = [ "parking_lot", "postage", "project", - "schemars", "serde", "serde_json", "settings", "smol", + "ui", "url", "util", + "workspace", +] + +[[package]] +name = "context_server_settings" +version = "0.1.0" +dependencies = [ + "anyhow", + "collections", + "gpui", + "schemars", + "serde", + "serde_json", + "settings", ] [[package]] @@ -4209,7 +4240,7 @@ dependencies = [ "async-trait", "client", "collections", - "context_servers", + "context_server_settings", "ctor", "env_logger 0.11.5", "extension", @@ -15586,6 +15617,7 @@ dependencies = [ "assets", "assistant", "assistant2", + "assistant_tools", "async-watch", "audio", "auto_update", diff --git a/Cargo.toml b/Cargo.toml index 2e5111e2ff..7c141a1b6c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "crates/assistant2", "crates/assistant_slash_command", "crates/assistant_tool", + "crates/assistant_tools", "crates/audio", "crates/auto_update", "crates/auto_update_ui", @@ -22,7 +23,8 @@ members = [ "crates/collections", "crates/command_palette", "crates/command_palette_hooks", - "crates/context_servers", + "crates/context_server", + "crates/context_server_settings", "crates/copilot", "crates/db", "crates/diagnostics", @@ -191,6 +193,7 @@ assistant = { path = "crates/assistant" } assistant2 = { path = "crates/assistant2" } assistant_slash_command = { path = "crates/assistant_slash_command" } assistant_tool = { path = "crates/assistant_tool" } +assistant_tools = { path = "crates/assistant_tools" } audio = { path = "crates/audio" } auto_update = { path = "crates/auto_update" } auto_update_ui = { path = "crates/auto_update_ui" } @@ -205,7 +208,8 @@ collab_ui = { path = "crates/collab_ui" } collections = { path = "crates/collections" } command_palette = { path = "crates/command_palette" } command_palette_hooks = { path = "crates/command_palette_hooks" } -context_servers = { path = "crates/context_servers" } +context_server = { path = "crates/context_server" } +context_server_settings = { path = "crates/context_server_settings" } copilot = { path = "crates/copilot" } db = { path = "crates/db" } diagnostics = { path = "crates/diagnostics" } diff --git a/crates/assistant/Cargo.toml b/crates/assistant/Cargo.toml index 0799d4bbdb..3b68b5cc9a 100644 --- a/crates/assistant/Cargo.toml +++ b/crates/assistant/Cargo.toml @@ -33,7 +33,7 @@ client.workspace = true clock.workspace = true collections.workspace = true command_palette_hooks.workspace = true -context_servers.workspace = true +context_server.workspace = true db.workspace = true editor.workspace = true feature_flags.workspace = true diff --git a/crates/assistant/src/assistant.rs b/crates/assistant/src/assistant.rs index f6e435bfb8..7e4e38e320 100644 --- a/crates/assistant/src/assistant.rs +++ b/crates/assistant/src/assistant.rs @@ -14,16 +14,12 @@ pub mod slash_command_settings; mod slash_command_working_set; mod streaming_diff; mod terminal_inline_assistant; -mod tool_working_set; -mod tools; use crate::slash_command::project_command::ProjectSlashCommandFeatureFlag; pub use crate::slash_command_working_set::{SlashCommandId, SlashCommandWorkingSet}; -pub use crate::tool_working_set::{ToolId, ToolWorkingSet}; pub use assistant_panel::{AssistantPanel, AssistantPanelEvent}; use assistant_settings::AssistantSettings; use assistant_slash_command::SlashCommandRegistry; -use assistant_tool::ToolRegistry; use client::{proto, Client}; use command_palette_hooks::CommandPaletteFilter; pub use context::*; @@ -246,7 +242,7 @@ pub fn init( assistant_slash_command::init(cx); assistant_tool::init(cx); assistant_panel::init(cx); - context_servers::init(cx); + context_server::init(cx); let prompt_builder = prompts::PromptBuilder::new(Some(PromptLoadingParams { fs: fs.clone(), @@ -259,7 +255,6 @@ pub fn init( .map(Arc::new) .unwrap_or_else(|| Arc::new(prompts::PromptBuilder::new(None).unwrap())); register_slash_commands(Some(prompt_builder.clone()), cx); - register_tools(cx); inline_assistant::init( fs.clone(), prompt_builder.clone(), @@ -423,11 +418,6 @@ fn update_slash_commands_from_settings(cx: &mut AppContext) { } } -fn register_tools(cx: &mut AppContext) { - let tool_registry = ToolRegistry::global(cx); - tool_registry.register_tool(tools::now_tool::NowTool); -} - pub fn humanize_token_count(count: usize) -> String { match count { 0..=999 => count.to_string(), diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 9a7beb96d2..e1ce7c4ab2 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -1,6 +1,5 @@ use crate::slash_command::file_command::codeblock_fence_for_path; use crate::slash_command_working_set::SlashCommandWorkingSet; -use crate::ToolWorkingSet; use crate::{ assistant_settings::{AssistantDockPosition, AssistantSettings}, humanize_token_count, @@ -23,6 +22,7 @@ use crate::{ }; use anyhow::Result; use assistant_slash_command::{SlashCommand, SlashCommandOutputSection}; +use assistant_tool::ToolWorkingSet; use client::{proto, zed_urls, Client, Status}; use collections::{hash_map, BTreeSet, HashMap, HashSet}; use editor::{ @@ -1316,7 +1316,7 @@ impl AssistantPanel { fn restart_context_servers( workspace: &mut Workspace, - _action: &context_servers::Restart, + _action: &context_server::Restart, cx: &mut ViewContext, ) { let Some(assistant_panel) = workspace.panel::(cx) else { diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index 570180ed74..2a7985a8c7 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -2,7 +2,6 @@ mod context_tests; use crate::slash_command_working_set::SlashCommandWorkingSet; -use crate::ToolWorkingSet; use crate::{ prompts::PromptBuilder, slash_command::{file_command::FileCommandMetadata, SlashCommandLine}, @@ -12,6 +11,7 @@ use anyhow::{anyhow, Context as _, Result}; use assistant_slash_command::{ SlashCommandContent, SlashCommandEvent, SlashCommandOutputSection, SlashCommandResult, }; +use assistant_tool::ToolWorkingSet; use client::{self, proto, telemetry::Telemetry}; use clock::ReplicaId; use collections::{HashMap, HashSet}; diff --git a/crates/assistant/src/context/context_tests.rs b/crates/assistant/src/context/context_tests.rs index 84b94c72c3..7f058cc9e7 100644 --- a/crates/assistant/src/context/context_tests.rs +++ b/crates/assistant/src/context/context_tests.rs @@ -1,6 +1,5 @@ use super::{AssistantEdit, MessageCacheMetadata}; use crate::slash_command_working_set::SlashCommandWorkingSet; -use crate::ToolWorkingSet; use crate::{ assistant_panel, prompt_library, slash_command::file_command, AssistantEditKind, CacheStatus, Context, ContextEvent, ContextId, ContextOperation, InvokedSlashCommandId, MessageId, @@ -11,6 +10,7 @@ use assistant_slash_command::{ ArgumentCompletion, SlashCommand, SlashCommandContent, SlashCommandEvent, SlashCommandOutput, SlashCommandOutputSection, SlashCommandRegistry, SlashCommandResult, }; +use assistant_tool::ToolWorkingSet; use collections::{HashMap, HashSet}; use fs::FakeFs; use futures::{ diff --git a/crates/assistant/src/context_store.rs b/crates/assistant/src/context_store.rs index 217d59faa4..34d4e5a700 100644 --- a/crates/assistant/src/context_store.rs +++ b/crates/assistant/src/context_store.rs @@ -1,15 +1,16 @@ use crate::slash_command::context_server_command; +use crate::SlashCommandId; use crate::{ prompts::PromptBuilder, slash_command_working_set::SlashCommandWorkingSet, Context, ContextEvent, ContextId, ContextOperation, ContextVersion, SavedContext, SavedContextMetadata, }; -use crate::{tools, SlashCommandId, ToolId, ToolWorkingSet}; use anyhow::{anyhow, Context as _, Result}; +use assistant_tool::{ToolId, ToolWorkingSet}; use client::{proto, telemetry::Telemetry, Client, TypedEnvelope}; use clock::ReplicaId; use collections::HashMap; -use context_servers::manager::ContextServerManager; -use context_servers::ContextServerFactoryRegistry; +use context_server::manager::ContextServerManager; +use context_server::{ContextServerFactoryRegistry, ContextServerTool}; use fs::Fs; use futures::StreamExt; use fuzzy::StringMatchCandidate; @@ -808,13 +809,13 @@ impl ContextStore { fn handle_context_server_event( &mut self, context_server_manager: Model, - event: &context_servers::manager::Event, + event: &context_server::manager::Event, cx: &mut ModelContext, ) { let slash_command_working_set = self.slash_commands.clone(); let tool_working_set = self.tools.clone(); match event { - context_servers::manager::Event::ServerStarted { server_id } => { + context_server::manager::Event::ServerStarted { server_id } => { if let Some(server) = context_server_manager.read(cx).get_server(server_id) { let context_server_manager = context_server_manager.clone(); cx.spawn({ @@ -825,7 +826,7 @@ impl ContextStore { return; }; - if protocol.capable(context_servers::protocol::ServerCapability::Prompts) { + if protocol.capable(context_server::protocol::ServerCapability::Prompts) { if let Some(prompts) = protocol.list_prompts().await.log_err() { let slash_command_ids = prompts .into_iter() @@ -853,12 +854,12 @@ impl ContextStore { } } - if protocol.capable(context_servers::protocol::ServerCapability::Tools) { + if protocol.capable(context_server::protocol::ServerCapability::Tools) { if let Some(tools) = protocol.list_tools().await.log_err() { let tool_ids = tools.tools.into_iter().map(|tool| { log::info!("registering context server tool: {:?}", tool.name); tool_working_set.insert( - Arc::new(tools::context_server_tool::ContextServerTool::new( + Arc::new(ContextServerTool::new( context_server_manager.clone(), server.id(), tool, @@ -880,7 +881,7 @@ impl ContextStore { .detach(); } } - context_servers::manager::Event::ServerStopped { server_id } => { + context_server::manager::Event::ServerStopped { server_id } => { if let Some(slash_command_ids) = self.context_server_slash_command_ids.remove(server_id) { diff --git a/crates/assistant/src/slash_command/context_server_command.rs b/crates/assistant/src/slash_command/context_server_command.rs index 692b4f6ea7..b183a77f54 100644 --- a/crates/assistant/src/slash_command/context_server_command.rs +++ b/crates/assistant/src/slash_command/context_server_command.rs @@ -4,7 +4,7 @@ use assistant_slash_command::{ SlashCommandOutputSection, SlashCommandResult, }; use collections::HashMap; -use context_servers::{ +use context_server::{ manager::{ContextServer, ContextServerManager}, types::Prompt, }; @@ -95,9 +95,9 @@ impl SlashCommand for ContextServerSlashCommand { let completion_result = protocol .completion( - context_servers::types::CompletionReference::Prompt( - context_servers::types::PromptReference { - r#type: context_servers::types::PromptReferenceType::Prompt, + context_server::types::CompletionReference::Prompt( + context_server::types::PromptReference { + r#type: context_server::types::PromptReferenceType::Prompt, name: prompt_name, }, ), @@ -152,7 +152,7 @@ impl SlashCommand for ContextServerSlashCommand { if result .messages .iter() - .any(|msg| !matches!(msg.role, context_servers::types::Role::User)) + .any(|msg| !matches!(msg.role, context_server::types::Role::User)) { return Err(anyhow!( "Prompt contains non-user roles, which is not supported" @@ -164,7 +164,7 @@ impl SlashCommand for ContextServerSlashCommand { .messages .into_iter() .filter_map(|msg| match msg.content { - context_servers::types::MessageContent::Text { text } => Some(text), + context_server::types::MessageContent::Text { text } => Some(text), _ => None, }) .collect::>() diff --git a/crates/assistant/src/tools.rs b/crates/assistant/src/tools.rs deleted file mode 100644 index 83a396c020..0000000000 --- a/crates/assistant/src/tools.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod context_server_tool; -pub mod now_tool; diff --git a/crates/assistant_tool/src/assistant_tool.rs b/crates/assistant_tool/src/assistant_tool.rs index 179bfe8dd1..c993494495 100644 --- a/crates/assistant_tool/src/assistant_tool.rs +++ b/crates/assistant_tool/src/assistant_tool.rs @@ -1,4 +1,5 @@ mod tool_registry; +mod tool_working_set; use std::sync::Arc; @@ -6,7 +7,8 @@ use anyhow::Result; use gpui::{AppContext, Task, WeakView, WindowContext}; use workspace::Workspace; -pub use tool_registry::*; +pub use crate::tool_registry::*; +pub use crate::tool_working_set::*; pub fn init(cx: &mut AppContext) { ToolRegistry::default_global(cx); diff --git a/crates/assistant/src/tool_working_set.rs b/crates/assistant_tool/src/tool_working_set.rs similarity index 98% rename from crates/assistant/src/tool_working_set.rs rename to crates/assistant_tool/src/tool_working_set.rs index aa2bb7a530..f22f0c7881 100644 --- a/crates/assistant/src/tool_working_set.rs +++ b/crates/assistant_tool/src/tool_working_set.rs @@ -1,8 +1,10 @@ -use assistant_tool::{Tool, ToolRegistry}; +use std::sync::Arc; + use collections::HashMap; use gpui::AppContext; use parking_lot::Mutex; -use std::sync::Arc; + +use crate::{Tool, ToolRegistry}; #[derive(Copy, Clone, PartialEq, Eq, Hash, Default)] pub struct ToolId(usize); diff --git a/crates/assistant_tools/Cargo.toml b/crates/assistant_tools/Cargo.toml new file mode 100644 index 0000000000..4e92d67299 --- /dev/null +++ b/crates/assistant_tools/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "assistant_tools" +version = "0.1.0" +edition = "2021" +publish = false +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/assistant_tools.rs" + +[dependencies] +anyhow.workspace = true +assistant_tool.workspace = true +chrono.workspace = true +gpui.workspace = true +schemars.workspace = true +serde.workspace = true +serde_json.workspace = true +workspace.workspace = true diff --git a/crates/context_servers/LICENSE-GPL b/crates/assistant_tools/LICENSE-GPL similarity index 100% rename from crates/context_servers/LICENSE-GPL rename to crates/assistant_tools/LICENSE-GPL diff --git a/crates/assistant_tools/src/assistant_tools.rs b/crates/assistant_tools/src/assistant_tools.rs new file mode 100644 index 0000000000..7d145c61b7 --- /dev/null +++ b/crates/assistant_tools/src/assistant_tools.rs @@ -0,0 +1,13 @@ +mod now_tool; + +use assistant_tool::ToolRegistry; +use gpui::AppContext; + +use crate::now_tool::NowTool; + +pub fn init(cx: &mut AppContext) { + assistant_tool::init(cx); + + let registry = ToolRegistry::global(cx); + registry.register_tool(NowTool); +} diff --git a/crates/assistant/src/tools/now_tool.rs b/crates/assistant_tools/src/now_tool.rs similarity index 100% rename from crates/assistant/src/tools/now_tool.rs rename to crates/assistant_tools/src/now_tool.rs diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index d3da1c2816..e56507c007 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -79,7 +79,8 @@ uuid.workspace = true [dev-dependencies] assistant = { workspace = true, features = ["test-support"] } -context_servers.workspace = true +assistant_tool.workspace = true +context_server.workspace = true async-trait.workspace = true audio.workspace = true call = { workspace = true, features = ["test-support"] } diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 5ec9a574a1..b6a0247424 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -6,7 +6,8 @@ use crate::{ }, }; use anyhow::{anyhow, Result}; -use assistant::{ContextStore, PromptBuilder, SlashCommandWorkingSet, ToolWorkingSet}; +use assistant::{ContextStore, PromptBuilder, SlashCommandWorkingSet}; +use assistant_tool::ToolWorkingSet; use call::{room, ActiveCall, ParticipantLocation, Room}; use client::{User, RECEIVE_TIMEOUT}; use collections::{HashMap, HashSet}; @@ -6486,8 +6487,8 @@ async fn test_context_collaboration_with_reconnect( assert_eq!(project.collaborators().len(), 1); }); - cx_a.update(context_servers::init); - cx_b.update(context_servers::init); + cx_a.update(context_server::init); + cx_b.update(context_server::init); let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap()); let context_store_a = cx_a .update(|cx| { diff --git a/crates/context_servers/Cargo.toml b/crates/context_server/Cargo.toml similarity index 76% rename from crates/context_servers/Cargo.toml rename to crates/context_server/Cargo.toml index cbd762c8c4..410b897f28 100644 --- a/crates/context_servers/Cargo.toml +++ b/crates/context_server/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "context_servers" +name = "context_server" version = "0.1.0" edition = "2021" publish = false @@ -9,12 +9,14 @@ license = "GPL-3.0-or-later" workspace = true [lib] -path = "src/context_servers.rs" +path = "src/context_server.rs" [dependencies] anyhow.workspace = true +assistant_tool.workspace = true collections.workspace = true command_palette_hooks.workspace = true +context_server_settings.workspace = true extension.workspace = true futures.workspace = true gpui.workspace = true @@ -22,10 +24,11 @@ log.workspace = true parking_lot.workspace = true postage.workspace = true project.workspace = true -schemars.workspace = true serde.workspace = true serde_json.workspace = true settings.workspace = true smol.workspace = true +ui.workspace = true url = { workspace = true, features = ["serde"] } util.workspace = true +workspace.workspace = true diff --git a/crates/context_server/LICENSE-GPL b/crates/context_server/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/context_server/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/context_servers/src/client.rs b/crates/context_server/src/client.rs similarity index 100% rename from crates/context_servers/src/client.rs rename to crates/context_server/src/client.rs diff --git a/crates/context_servers/src/context_servers.rs b/crates/context_server/src/context_server.rs similarity index 76% rename from crates/context_servers/src/context_servers.rs rename to crates/context_server/src/context_server.rs index e6b52aaee2..84c08d7b2a 100644 --- a/crates/context_servers/src/context_servers.rs +++ b/crates/context_server/src/context_server.rs @@ -1,4 +1,5 @@ pub mod client; +mod context_server_tool; mod extension_context_server; pub mod manager; pub mod protocol; @@ -6,10 +7,10 @@ mod registry; pub mod types; use command_palette_hooks::CommandPaletteFilter; +pub use context_server_settings::{ContextServerSettings, ServerCommand, ServerConfig}; use gpui::{actions, AppContext}; -use settings::Settings; -use crate::manager::ContextServerSettings; +pub use crate::context_server_tool::ContextServerTool; pub use crate::registry::ContextServerFactoryRegistry; actions!(context_servers, [Restart]); @@ -18,7 +19,7 @@ actions!(context_servers, [Restart]); pub const CONTEXT_SERVERS_NAMESPACE: &'static str = "context_servers"; pub fn init(cx: &mut AppContext) { - ContextServerSettings::register(cx); + context_server_settings::init(cx); ContextServerFactoryRegistry::default_global(cx); extension_context_server::init(cx); diff --git a/crates/assistant/src/tools/context_server_tool.rs b/crates/context_server/src/context_server_tool.rs similarity index 97% rename from crates/assistant/src/tools/context_server_tool.rs rename to crates/context_server/src/context_server_tool.rs index 8015d94df9..70740f710a 100644 --- a/crates/assistant/src/tools/context_server_tool.rs +++ b/crates/context_server/src/context_server_tool.rs @@ -2,10 +2,11 @@ use std::sync::Arc; use anyhow::{anyhow, bail}; use assistant_tool::Tool; -use context_servers::manager::ContextServerManager; -use context_servers::types; use gpui::{Model, Task}; +use crate::manager::ContextServerManager; +use crate::types; + pub struct ContextServerTool { server_manager: Model, server_id: Arc, diff --git a/crates/context_servers/src/extension_context_server.rs b/crates/context_server/src/extension_context_server.rs similarity index 97% rename from crates/context_servers/src/extension_context_server.rs rename to crates/context_server/src/extension_context_server.rs index 092816b5e6..36fecd2af3 100644 --- a/crates/context_servers/src/extension_context_server.rs +++ b/crates/context_server/src/extension_context_server.rs @@ -3,8 +3,7 @@ use std::sync::Arc; use extension::{Extension, ExtensionContextServerProxy, ExtensionHostProxy, ProjectDelegate}; use gpui::{AppContext, Model}; -use crate::manager::ServerCommand; -use crate::ContextServerFactoryRegistry; +use crate::{ContextServerFactoryRegistry, ServerCommand}; struct ExtensionProject { worktree_ids: Vec, diff --git a/crates/context_servers/src/manager.rs b/crates/context_server/src/manager.rs similarity index 84% rename from crates/context_servers/src/manager.rs rename to crates/context_server/src/manager.rs index c95fcd239d..febbee1cdf 100644 --- a/crates/context_servers/src/manager.rs +++ b/crates/context_server/src/manager.rs @@ -24,66 +24,16 @@ 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}; +use settings::{Settings, SettingsStore}; use util::ResultExt as _; +use crate::{ContextServerSettings, ServerConfig}; + use crate::{ client::{self, Client}, types, ContextServerFactoryRegistry, CONTEXT_SERVERS_NAMESPACE, }; -#[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, -} - -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, - pub args: Vec, - pub env: Option>, -} - -impl Settings for ContextServerSettings { - const KEY: Option<&'static str> = None; - - type FileContent = Self; - - fn load( - sources: SettingsSources, - _: &mut gpui::AppContext, - ) -> anyhow::Result { - sources.json_merge() - } -} - pub struct ContextServer { pub id: Arc, pub config: Arc, diff --git a/crates/context_servers/src/protocol.rs b/crates/context_server/src/protocol.rs similarity index 100% rename from crates/context_servers/src/protocol.rs rename to crates/context_server/src/protocol.rs diff --git a/crates/context_servers/src/registry.rs b/crates/context_server/src/registry.rs similarity index 98% rename from crates/context_servers/src/registry.rs rename to crates/context_server/src/registry.rs index c17c65370a..a4d0f9a804 100644 --- a/crates/context_servers/src/registry.rs +++ b/crates/context_server/src/registry.rs @@ -5,7 +5,7 @@ use collections::HashMap; use gpui::{AppContext, AsyncAppContext, Context, Global, Model, ReadGlobal, Task}; use project::Project; -use crate::manager::ServerCommand; +use crate::ServerCommand; pub type ContextServerFactory = Arc< dyn Fn(Model, &AsyncAppContext) -> Task> + Send + Sync + 'static, diff --git a/crates/context_servers/src/types.rs b/crates/context_server/src/types.rs similarity index 100% rename from crates/context_servers/src/types.rs rename to crates/context_server/src/types.rs diff --git a/crates/context_server_settings/Cargo.toml b/crates/context_server_settings/Cargo.toml new file mode 100644 index 0000000000..ad0d1d9dc0 --- /dev/null +++ b/crates/context_server_settings/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "context_server_settings" +version = "0.1.0" +edition = "2021" +publish = false +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/context_server_settings.rs" + +[dependencies] +anyhow.workspace = true +collections.workspace = true +gpui.workspace = true +schemars.workspace = true +serde.workspace = true +serde_json.workspace = true +settings.workspace = true diff --git a/crates/context_server_settings/LICENSE-GPL b/crates/context_server_settings/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/context_server_settings/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/context_server_settings/src/context_server_settings.rs b/crates/context_server_settings/src/context_server_settings.rs new file mode 100644 index 0000000000..68969ca795 --- /dev/null +++ b/crates/context_server_settings/src/context_server_settings.rs @@ -0,0 +1,61 @@ +use std::sync::Arc; + +use collections::HashMap; +use gpui::AppContext; +use schemars::gen::SchemaGenerator; +use schemars::schema::{InstanceType, Schema, SchemaObject}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use settings::{Settings, SettingsSources}; + +pub fn init(cx: &mut AppContext) { + ContextServerSettings::register(cx); +} + +#[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, +} + +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, + pub args: Vec, + pub env: Option>, +} + +#[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>, +} + +impl Settings for ContextServerSettings { + const KEY: Option<&'static str> = None; + + type FileContent = Self; + + fn load( + sources: SettingsSources, + _: &mut gpui::AppContext, + ) -> anyhow::Result { + sources.json_merge() + } +} diff --git a/crates/extension_host/Cargo.toml b/crates/extension_host/Cargo.toml index 6e78654b7e..53971ade0a 100644 --- a/crates/extension_host/Cargo.toml +++ b/crates/extension_host/Cargo.toml @@ -22,7 +22,7 @@ async-tar.workspace = true async-trait.workspace = true client.workspace = true collections.workspace = true -context_servers.workspace = true +context_server_settings.workspace = true extension.workspace = true fs.workspace = true futures.workspace = true diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs index f7e11e1032..b722d7b235 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs @@ -7,7 +7,7 @@ use anyhow::{anyhow, bail, Context, Result}; use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; use async_trait::async_trait; -use context_servers::manager::ContextServerSettings; +use context_server_settings::ContextServerSettings; use extension::{ ExtensionLanguageServerProxy, KeyValueStoreDelegate, ProjectDelegate, WorktreeDelegate, }; diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 1959fb0e00..5003ca1b81 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -20,6 +20,7 @@ anyhow.workspace = true assets.workspace = true assistant.workspace = true assistant2.workspace = true +assistant_tools.workspace = true async-watch.workspace = true audio.workspace = true auto_update.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index cccd50da96..cfc11ade3f 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -407,6 +407,7 @@ fn main() { cx, ); assistant2::init(cx); + assistant_tools::init(cx); repl::init( app_state.fs.clone(), app_state.client.telemetry().clone(), From f059b6a24bac5a7bd65ea54a8f48b919d928d75e Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 25 Nov 2024 19:44:34 -0500 Subject: [PATCH 150/886] assistant2: Add support for using tools (#21190) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds rudimentary support for using tools to `assistant2`. There are currently no visual affordances for tool use. This is gated behind the `assistant-tool-use` feature flag. Screenshot 2024-11-25 at 7 21 31 PM Release Notes: - N/A --- Cargo.lock | 3 + crates/assistant/src/context.rs | 12 +- crates/assistant2/Cargo.toml | 3 + crates/assistant2/src/assistant_panel.rs | 61 ++++++- crates/assistant2/src/message_editor.rs | 19 ++- crates/assistant2/src/thread.rs | 190 ++++++++++++++++++++-- crates/assistant_tools/src/now_tool.rs | 2 +- crates/feature_flags/src/feature_flags.rs | 10 ++ 8 files changed, 263 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7152bf8d08..5a18caa3d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -455,6 +455,8 @@ name = "assistant2" version = "0.1.0" dependencies = [ "anyhow", + "assistant_tool", + "collections", "command_palette_hooks", "editor", "feature_flags", @@ -463,6 +465,7 @@ dependencies = [ "language_model", "language_model_selector", "proto", + "serde_json", "settings", "smol", "theme", diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index 2a7985a8c7..ac032accc3 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -15,7 +15,7 @@ use assistant_tool::ToolWorkingSet; use client::{self, proto, telemetry::Telemetry}; use clock::ReplicaId; use collections::{HashMap, HashSet}; -use feature_flags::{FeatureFlag, FeatureFlagAppExt}; +use feature_flags::{FeatureFlagAppExt, ToolUseFeatureFlag}; use fs::{Fs, RemoveOptions}; use futures::{future::Shared, FutureExt, StreamExt}; use gpui::{ @@ -3201,16 +3201,6 @@ pub enum PendingSlashCommandStatus { Error(String), } -pub(crate) struct ToolUseFeatureFlag; - -impl FeatureFlag for ToolUseFeatureFlag { - const NAME: &'static str = "assistant-tool-use"; - - fn enabled_for_staff() -> bool { - false - } -} - #[derive(Debug, Clone)] pub struct PendingToolUse { pub id: Arc, diff --git a/crates/assistant2/Cargo.toml b/crates/assistant2/Cargo.toml index 02cbdadb62..60c168079d 100644 --- a/crates/assistant2/Cargo.toml +++ b/crates/assistant2/Cargo.toml @@ -14,6 +14,8 @@ doctest = false [dependencies] anyhow.workspace = true +assistant_tool.workspace = true +collections.workspace = true command_palette_hooks.workspace = true editor.workspace = true feature_flags.workspace = true @@ -23,6 +25,7 @@ language_model.workspace = true language_model_selector.workspace = true proto.workspace = true settings.workspace = true +serde_json.workspace = true smol.workspace = true theme.workspace = true ui.workspace = true diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index c33e9d520d..b05a39a1cd 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -1,4 +1,7 @@ +use std::sync::Arc; + use anyhow::Result; +use assistant_tool::ToolWorkingSet; use gpui::{ prelude::*, px, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusableView, Model, Pixels, Subscription, Task, View, ViewContext, WeakView, WindowContext, @@ -10,7 +13,7 @@ use workspace::dock::{DockPosition, Panel, PanelEvent}; use workspace::Workspace; use crate::message_editor::MessageEditor; -use crate::thread::Thread; +use crate::thread::{Thread, ThreadEvent}; use crate::{NewThread, ToggleFocus, ToggleModelSelector}; pub fn init(cx: &mut AppContext) { @@ -25,8 +28,10 @@ pub fn init(cx: &mut AppContext) { } pub struct AssistantPanel { + workspace: WeakView, thread: Model, message_editor: View, + tools: Arc, _subscriptions: Vec, } @@ -36,26 +41,36 @@ impl AssistantPanel { cx: AsyncWindowContext, ) -> Task>> { cx.spawn(|mut cx| async move { + let tools = Arc::new(ToolWorkingSet::default()); workspace.update(&mut cx, |workspace, cx| { - cx.new_view(|cx| Self::new(workspace, cx)) + cx.new_view(|cx| Self::new(workspace, tools, cx)) }) }) } - fn new(_workspace: &Workspace, cx: &mut ViewContext) -> Self { - let thread = cx.new_model(Thread::new); - let subscriptions = vec![cx.observe(&thread, |_, _, cx| cx.notify())]; + fn new(workspace: &Workspace, tools: Arc, cx: &mut ViewContext) -> Self { + let thread = cx.new_model(|cx| Thread::new(tools.clone(), cx)); + let subscriptions = vec![ + cx.observe(&thread, |_, _, cx| cx.notify()), + cx.subscribe(&thread, Self::handle_thread_event), + ]; Self { + workspace: workspace.weak_handle(), thread: thread.clone(), message_editor: cx.new_view(|cx| MessageEditor::new(thread, cx)), + tools, _subscriptions: subscriptions, } } fn new_thread(&mut self, cx: &mut ViewContext) { - let thread = cx.new_model(Thread::new); - let subscriptions = vec![cx.observe(&thread, |_, _, cx| cx.notify())]; + let tools = self.thread.read(cx).tools().clone(); + let thread = cx.new_model(|cx| Thread::new(tools, cx)); + let subscriptions = vec![ + cx.observe(&thread, |_, _, cx| cx.notify()), + cx.subscribe(&thread, Self::handle_thread_event), + ]; self.message_editor = cx.new_view(|cx| MessageEditor::new(thread.clone(), cx)); self.thread = thread; @@ -63,6 +78,38 @@ impl AssistantPanel { self.message_editor.focus_handle(cx).focus(cx); } + + fn handle_thread_event( + &mut self, + _: Model, + event: &ThreadEvent, + cx: &mut ViewContext, + ) { + match event { + ThreadEvent::StreamedCompletion => {} + ThreadEvent::UsePendingTools => { + let pending_tool_uses = self + .thread + .read(cx) + .pending_tool_uses() + .into_iter() + .filter(|tool_use| tool_use.status.is_idle()) + .cloned() + .collect::>(); + + for tool_use in pending_tool_uses { + if let Some(tool) = self.tools.tool(&tool_use.name, cx) { + let task = tool.run(tool_use.input, self.workspace.clone(), cx); + + self.thread.update(cx, |thread, cx| { + thread.insert_tool_output(tool_use.id.clone(), task, cx); + }); + } + } + } + ThreadEvent::ToolFinished { .. } => {} + } + } } impl FocusableView for AssistantPanel { diff --git a/crates/assistant2/src/message_editor.rs b/crates/assistant2/src/message_editor.rs index f0a8e260bc..c42d66a4d7 100644 --- a/crates/assistant2/src/message_editor.rs +++ b/crates/assistant2/src/message_editor.rs @@ -1,6 +1,7 @@ use editor::{Editor, EditorElement, EditorStyle}; +use feature_flags::{FeatureFlagAppExt, ToolUseFeatureFlag}; use gpui::{AppContext, FocusableView, Model, TextStyle, View}; -use language_model::LanguageModelRegistry; +use language_model::{LanguageModelRegistry, LanguageModelRequestTool}; use settings::Settings; use theme::ThemeSettings; use ui::{prelude::*, ButtonLike, ElevationIndex, KeyBinding}; @@ -55,7 +56,21 @@ impl MessageEditor { self.thread.update(cx, |thread, cx| { thread.insert_user_message(user_message); - let request = thread.to_completion_request(request_kind, cx); + let mut request = thread.to_completion_request(request_kind, cx); + + if cx.has_flag::() { + request.tools = thread + .tools() + .tools(cx) + .into_iter() + .map(|tool| LanguageModelRequestTool { + name: tool.name(), + description: tool.description(), + input_schema: tool.input_schema(), + }) + .collect(); + } + thread.stream_completion(request, model, cx) }); diff --git a/crates/assistant2/src/thread.rs b/crates/assistant2/src/thread.rs index c1df6c76d3..067e82a602 100644 --- a/crates/assistant2/src/thread.rs +++ b/crates/assistant2/src/thread.rs @@ -1,12 +1,16 @@ use std::sync::Arc; -use futures::StreamExt as _; +use anyhow::Result; +use assistant_tool::ToolWorkingSet; +use collections::HashMap; +use futures::future::Shared; +use futures::{FutureExt as _, StreamExt as _}; use gpui::{AppContext, EventEmitter, ModelContext, Task}; use language_model::{ LanguageModel, LanguageModelCompletionEvent, LanguageModelRequest, LanguageModelRequestMessage, - MessageContent, Role, StopReason, + LanguageModelToolResult, LanguageModelToolUse, MessageContent, Role, StopReason, }; -use util::{post_inc, ResultExt as _}; +use util::post_inc; #[derive(Debug, Clone, Copy)] pub enum RequestKind { @@ -14,14 +18,12 @@ pub enum RequestKind { } /// A message in a [`Thread`]. +#[derive(Debug)] pub struct Message { pub role: Role, pub text: String, -} - -struct PendingCompletion { - id: usize, - _task: Task<()>, + pub tool_uses: Vec, + pub tool_results: Vec, } /// A thread of conversation with the LLM. @@ -29,14 +31,20 @@ pub struct Thread { messages: Vec, completion_count: usize, pending_completions: Vec, + tools: Arc, + pending_tool_uses_by_id: HashMap, PendingToolUse>, + completed_tool_uses_by_id: HashMap, String>, } impl Thread { - pub fn new(_cx: &mut ModelContext) -> Self { + pub fn new(tools: Arc, _cx: &mut ModelContext) -> Self { Self { + tools, messages: Vec::new(), completion_count: 0, pending_completions: Vec::new(), + pending_tool_uses_by_id: HashMap::default(), + completed_tool_uses_by_id: HashMap::default(), } } @@ -44,11 +52,31 @@ impl Thread { self.messages.iter() } + pub fn tools(&self) -> &Arc { + &self.tools + } + + pub fn pending_tool_uses(&self) -> Vec<&PendingToolUse> { + self.pending_tool_uses_by_id.values().collect() + } + pub fn insert_user_message(&mut self, text: impl Into) { - self.messages.push(Message { + let mut message = Message { role: Role::User, text: text.into(), - }); + tool_uses: Vec::new(), + tool_results: Vec::new(), + }; + + for (tool_use_id, tool_output) in self.completed_tool_uses_by_id.drain() { + message.tool_results.push(LanguageModelToolResult { + tool_use_id: tool_use_id.to_string(), + content: tool_output, + is_error: false, + }); + } + + self.messages.push(message); } pub fn to_completion_request( @@ -70,9 +98,23 @@ impl Thread { cache: false, }; - request_message - .content - .push(MessageContent::Text(message.text.clone())); + for tool_result in &message.tool_results { + request_message + .content + .push(MessageContent::ToolResult(tool_result.clone())); + } + + if !message.text.is_empty() { + request_message + .content + .push(MessageContent::Text(message.text.clone())); + } + + for tool_use in &message.tool_uses { + request_message + .content + .push(MessageContent::ToolUse(tool_use.clone())); + } request.messages.push(request_message); } @@ -103,6 +145,8 @@ impl Thread { thread.messages.push(Message { role: Role::Assistant, text: String::new(), + tool_uses: Vec::new(), + tool_results: Vec::new(), }); } LanguageModelCompletionEvent::Stop(reason) => { @@ -115,7 +159,24 @@ impl Thread { } } } - LanguageModelCompletionEvent::ToolUse(_tool_use) => {} + LanguageModelCompletionEvent::ToolUse(tool_use) => { + if let Some(last_message) = thread.messages.last_mut() { + if last_message.role == Role::Assistant { + last_message.tool_uses.push(tool_use.clone()); + } + } + + let tool_use_id: Arc = tool_use.id.into(); + thread.pending_tool_uses_by_id.insert( + tool_use_id.clone(), + PendingToolUse { + id: tool_use_id, + name: tool_use.name, + input: tool_use.input, + status: PendingToolUseStatus::Idle, + }, + ); + } } cx.emit(ThreadEvent::StreamedCompletion); @@ -135,7 +196,35 @@ impl Thread { }; let result = stream_completion.await; - let _ = result.log_err(); + + thread + .update(&mut cx, |_thread, cx| { + let error_message = if let Some(error) = result.as_ref().err() { + let error_message = error + .chain() + .map(|err| err.to_string()) + .collect::>() + .join("\n"); + Some(error_message) + } else { + None + }; + + if let Some(error_message) = error_message { + eprintln!("Completion failed: {error_message:?}"); + } + + if let Ok(stop_reason) = result { + match stop_reason { + StopReason::ToolUse => { + cx.emit(ThreadEvent::UsePendingTools); + } + StopReason::EndTurn => {} + StopReason::MaxTokens => {} + } + } + }) + .ok(); }); self.pending_completions.push(PendingCompletion { @@ -143,11 +232,80 @@ impl Thread { _task: task, }); } + + pub fn insert_tool_output( + &mut self, + tool_use_id: Arc, + output: Task>, + cx: &mut ModelContext, + ) { + let insert_output_task = cx.spawn(|thread, mut cx| { + let tool_use_id = tool_use_id.clone(); + async move { + let output = output.await; + thread + .update(&mut cx, |thread, cx| match output { + Ok(output) => { + thread + .completed_tool_uses_by_id + .insert(tool_use_id.clone(), output); + + cx.emit(ThreadEvent::ToolFinished { tool_use_id }); + } + Err(err) => { + if let Some(tool_use) = + thread.pending_tool_uses_by_id.get_mut(&tool_use_id) + { + tool_use.status = PendingToolUseStatus::Error(err.to_string()); + } + } + }) + .ok(); + } + }); + + if let Some(tool_use) = self.pending_tool_uses_by_id.get_mut(&tool_use_id) { + tool_use.status = PendingToolUseStatus::Running { + _task: insert_output_task.shared(), + }; + } + } } #[derive(Debug, Clone)] pub enum ThreadEvent { StreamedCompletion, + UsePendingTools, + ToolFinished { + #[allow(unused)] + tool_use_id: Arc, + }, } impl EventEmitter for Thread {} + +struct PendingCompletion { + id: usize, + _task: Task<()>, +} + +#[derive(Debug, Clone)] +pub struct PendingToolUse { + pub id: Arc, + pub name: String, + pub input: serde_json::Value, + pub status: PendingToolUseStatus, +} + +#[derive(Debug, Clone)] +pub enum PendingToolUseStatus { + Idle, + Running { _task: Shared> }, + Error(#[allow(unused)] String), +} + +impl PendingToolUseStatus { + pub fn is_idle(&self) -> bool { + matches!(self, PendingToolUseStatus::Idle) + } +} diff --git a/crates/assistant_tools/src/now_tool.rs b/crates/assistant_tools/src/now_tool.rs index 99034321b1..707f2be2bd 100644 --- a/crates/assistant_tools/src/now_tool.rs +++ b/crates/assistant_tools/src/now_tool.rs @@ -30,7 +30,7 @@ impl Tool for NowTool { } fn description(&self) -> String { - "Returns the current datetime in RFC 3339 format.".into() + "Returns the current datetime in RFC 3339 format. Only use this tool when the user specifically asks for it or the current task would benefit from knowing the current datetime.".into() } fn input_schema(&self) -> serde_json::Value { diff --git a/crates/feature_flags/src/feature_flags.rs b/crates/feature_flags/src/feature_flags.rs index 416971b36e..48e3cc95b2 100644 --- a/crates/feature_flags/src/feature_flags.rs +++ b/crates/feature_flags/src/feature_flags.rs @@ -49,6 +49,16 @@ impl FeatureFlag for Assistant2FeatureFlag { } } +pub struct ToolUseFeatureFlag; + +impl FeatureFlag for ToolUseFeatureFlag { + const NAME: &'static str = "assistant-tool-use"; + + fn enabled_for_staff() -> bool { + false + } +} + pub struct Remoting {} impl FeatureFlag for Remoting { const NAME: &'static str = "remoting"; From 7e418cc8afda7e54fa1098eb36664dbfe25863de Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 25 Nov 2024 20:49:03 -0500 Subject: [PATCH 151/886] assistant2: Style messages (#21191) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR styles the messages in `assistant2` so they don't look quite as rough: Screenshot 2024-11-25 at 8 36 32 PM Release Notes: - N/A --- crates/assistant2/src/assistant_panel.rs | 47 ++++++++++++++++++------ crates/assistant2/src/thread.rs | 2 +- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index b05a39a1cd..4ebf07e9d4 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -6,14 +6,14 @@ use gpui::{ prelude::*, px, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusableView, Model, Pixels, Subscription, Task, View, ViewContext, WeakView, WindowContext, }; -use language_model::LanguageModelRegistry; +use language_model::{LanguageModelRegistry, Role}; use language_model_selector::LanguageModelSelector; use ui::{prelude::*, ButtonLike, Divider, IconButtonShape, Tab, Tooltip}; use workspace::dock::{DockPosition, Panel, PanelEvent}; use workspace::Workspace; use crate::message_editor::MessageEditor; -use crate::thread::{Thread, ThreadEvent}; +use crate::thread::{Message, Thread, ThreadEvent}; use crate::{NewThread, ToggleFocus, ToggleModelSelector}; pub fn init(cx: &mut AppContext) { @@ -272,10 +272,39 @@ impl AssistantPanel { .tooltip(move |cx| Tooltip::for_action("Change Model", &ToggleModelSelector, cx)), ) } + + fn render_message(&self, message: Message, cx: &mut ViewContext) -> impl IntoElement { + let (role_icon, role_name) = match message.role { + Role::User => (IconName::Person, "You"), + Role::Assistant => (IconName::ZedAssistant, "Assistant"), + Role::System => (IconName::Settings, "System"), + }; + + v_flex() + .border_1() + .border_color(cx.theme().colors().border_variant) + .rounded_md() + .child( + h_flex() + .justify_between() + .p_1p5() + .border_b_1() + .border_color(cx.theme().colors().border_variant) + .child( + h_flex() + .gap_2() + .child(Icon::new(role_icon).size(IconSize::Small)) + .child(Label::new(role_name).size(LabelSize::Small)), + ), + ) + .child(v_flex().p_1p5().child(Label::new(message.text.clone()))) + } } impl Render for AssistantPanel { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let messages = self.thread.read(cx).messages().cloned().collect::>(); + v_flex() .key_context("AssistantPanel2") .justify_between() @@ -292,15 +321,11 @@ impl Render for AssistantPanel { .p_2() .overflow_y_scroll() .bg(cx.theme().colors().panel_background) - .children(self.thread.read(cx).messages().map(|message| { - v_flex() - .p_2() - .border_1() - .border_color(cx.theme().colors().border_variant) - .rounded_md() - .child(Label::new(message.role.to_string())) - .child(Label::new(message.text.clone())) - })), + .children( + messages + .into_iter() + .map(|message| self.render_message(message, cx)), + ), ) .child( h_flex() diff --git a/crates/assistant2/src/thread.rs b/crates/assistant2/src/thread.rs index 067e82a602..d8263d15f7 100644 --- a/crates/assistant2/src/thread.rs +++ b/crates/assistant2/src/thread.rs @@ -18,7 +18,7 @@ pub enum RequestKind { } /// A message in a [`Thread`]. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Message { pub role: Role, pub text: String, From 968ffaa3fd801b3a436551705db636b0c89609b6 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 25 Nov 2024 21:53:27 -0500 Subject: [PATCH 152/886] assistant2: Restructure storage of tool uses and results (#21194) This PR restructures the storage of the tool uses and results in `assistant2` so that they don't live on the individual messages. It also introduces a `LanguageModelToolUseId` newtype for better type safety. Release Notes: - N/A --- Cargo.lock | 1 + crates/assistant/src/assistant_panel.rs | 2 +- crates/assistant/src/context.rs | 21 ++- crates/assistant2/Cargo.toml | 1 + crates/assistant2/src/assistant_panel.rs | 7 +- crates/assistant2/src/thread.rs | 157 +++++++++++------- crates/language_model/src/language_model.rs | 20 ++- crates/language_model/src/request.rs | 2 +- .../language_models/src/provider/anthropic.rs | 2 +- 9 files changed, 136 insertions(+), 77 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5a18caa3d1..166adb6588 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -465,6 +465,7 @@ dependencies = [ "language_model", "language_model_selector", "proto", + "serde", "serde_json", "settings", "smol", diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index e1ce7c4ab2..7467d5dfd4 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -1925,7 +1925,7 @@ impl ContextEditor { Content::ToolUse { range: tool_use.source_range.clone(), tool_use: LanguageModelToolUse { - id: tool_use.id.to_string(), + id: tool_use.id.clone(), name: tool_use.name.clone(), input: tool_use.input.clone(), }, diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index ac032accc3..032a66b4c7 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -27,8 +27,8 @@ use language::{AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, P use language_model::{ LanguageModel, LanguageModelCacheConfiguration, LanguageModelCompletionEvent, LanguageModelImage, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, - LanguageModelRequestTool, LanguageModelToolResult, LanguageModelToolUse, MessageContent, Role, - StopReason, + LanguageModelRequestTool, LanguageModelToolResult, LanguageModelToolUse, + LanguageModelToolUseId, MessageContent, Role, StopReason, }; use language_models::{ provider::cloud::{MaxMonthlySpendReachedError, PaymentRequiredError}, @@ -385,7 +385,7 @@ pub enum ContextEvent { }, UsePendingTools, ToolFinished { - tool_use_id: Arc, + tool_use_id: LanguageModelToolUseId, output_range: Range, }, Operation(ContextOperation), @@ -479,7 +479,7 @@ pub enum Content { }, ToolResult { range: Range, - tool_use_id: Arc, + tool_use_id: LanguageModelToolUseId, }, } @@ -546,7 +546,7 @@ pub struct Context { pub(crate) slash_commands: Arc, pub(crate) tools: Arc, slash_command_output_sections: Vec>, - pending_tool_uses_by_id: HashMap, PendingToolUse>, + pending_tool_uses_by_id: HashMap, message_anchors: Vec, contents: Vec, messages_metadata: HashMap, @@ -1126,7 +1126,7 @@ impl Context { self.pending_tool_uses_by_id.values().collect() } - pub fn get_tool_use_by_id(&self, id: &Arc) -> Option<&PendingToolUse> { + pub fn get_tool_use_by_id(&self, id: &LanguageModelToolUseId) -> Option<&PendingToolUse> { self.pending_tool_uses_by_id.get(id) } @@ -2153,7 +2153,7 @@ impl Context { pub fn insert_tool_output( &mut self, - tool_use_id: Arc, + tool_use_id: LanguageModelToolUseId, output: Task>, cx: &mut ModelContext, ) { @@ -2340,11 +2340,10 @@ impl Context { let source_range = buffer.anchor_after(start_ix) ..buffer.anchor_after(end_ix); - let tool_use_id: Arc = tool_use.id.into(); this.pending_tool_uses_by_id.insert( - tool_use_id.clone(), + tool_use.id.clone(), PendingToolUse { - id: tool_use_id, + id: tool_use.id, name: tool_use.name, input: tool_use.input, status: PendingToolUseStatus::Idle, @@ -3203,7 +3202,7 @@ pub enum PendingSlashCommandStatus { #[derive(Debug, Clone)] pub struct PendingToolUse { - pub id: Arc, + pub id: LanguageModelToolUseId, pub name: String, pub input: serde_json::Value, pub status: PendingToolUseStatus, diff --git a/crates/assistant2/Cargo.toml b/crates/assistant2/Cargo.toml index 60c168079d..ca563b05c8 100644 --- a/crates/assistant2/Cargo.toml +++ b/crates/assistant2/Cargo.toml @@ -25,6 +25,7 @@ language_model.workspace = true language_model_selector.workspace = true proto.workspace = true settings.workspace = true +serde.workspace = true serde_json.workspace = true smol.workspace = true theme.workspace = true diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index 4ebf07e9d4..bf457d6c71 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -102,7 +102,12 @@ impl AssistantPanel { let task = tool.run(tool_use.input, self.workspace.clone(), cx); self.thread.update(cx, |thread, cx| { - thread.insert_tool_output(tool_use.id.clone(), task, cx); + thread.insert_tool_output( + tool_use.assistant_message_id, + tool_use.id.clone(), + task, + cx, + ); }); } } diff --git a/crates/assistant2/src/thread.rs b/crates/assistant2/src/thread.rs index d8263d15f7..0d2aab6905 100644 --- a/crates/assistant2/src/thread.rs +++ b/crates/assistant2/src/thread.rs @@ -8,8 +8,10 @@ use futures::{FutureExt as _, StreamExt as _}; use gpui::{AppContext, EventEmitter, ModelContext, Task}; use language_model::{ LanguageModel, LanguageModelCompletionEvent, LanguageModelRequest, LanguageModelRequestMessage, - LanguageModelToolResult, LanguageModelToolUse, MessageContent, Role, StopReason, + LanguageModelToolResult, LanguageModelToolUse, LanguageModelToolUseId, MessageContent, Role, + StopReason, }; +use serde::{Deserialize, Serialize}; use util::post_inc; #[derive(Debug, Clone, Copy)] @@ -17,34 +19,46 @@ pub enum RequestKind { Chat, } +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)] +pub struct MessageId(usize); + +impl MessageId { + fn post_inc(&mut self) -> Self { + Self(post_inc(&mut self.0)) + } +} + /// A message in a [`Thread`]. #[derive(Debug, Clone)] pub struct Message { + pub id: MessageId, pub role: Role, pub text: String, - pub tool_uses: Vec, - pub tool_results: Vec, } /// A thread of conversation with the LLM. pub struct Thread { messages: Vec, + next_message_id: MessageId, completion_count: usize, pending_completions: Vec, tools: Arc, - pending_tool_uses_by_id: HashMap, PendingToolUse>, - completed_tool_uses_by_id: HashMap, String>, + tool_uses_by_message: HashMap>, + tool_results_by_message: HashMap>, + pending_tool_uses_by_id: HashMap, } impl Thread { pub fn new(tools: Arc, _cx: &mut ModelContext) -> Self { Self { - tools, messages: Vec::new(), + next_message_id: MessageId(0), completion_count: 0, pending_completions: Vec::new(), + tools, + tool_uses_by_message: HashMap::default(), + tool_results_by_message: HashMap::default(), pending_tool_uses_by_id: HashMap::default(), - completed_tool_uses_by_id: HashMap::default(), } } @@ -61,22 +75,11 @@ impl Thread { } pub fn insert_user_message(&mut self, text: impl Into) { - let mut message = Message { + self.messages.push(Message { + id: self.next_message_id.post_inc(), role: Role::User, text: text.into(), - tool_uses: Vec::new(), - tool_results: Vec::new(), - }; - - for (tool_use_id, tool_output) in self.completed_tool_uses_by_id.drain() { - message.tool_results.push(LanguageModelToolResult { - tool_use_id: tool_use_id.to_string(), - content: tool_output, - is_error: false, - }); - } - - self.messages.push(message); + }); } pub fn to_completion_request( @@ -98,10 +101,12 @@ impl Thread { cache: false, }; - for tool_result in &message.tool_results { - request_message - .content - .push(MessageContent::ToolResult(tool_result.clone())); + if let Some(tool_results) = self.tool_results_by_message.get(&message.id) { + for tool_result in tool_results { + request_message + .content + .push(MessageContent::ToolResult(tool_result.clone())); + } } if !message.text.is_empty() { @@ -110,10 +115,12 @@ impl Thread { .push(MessageContent::Text(message.text.clone())); } - for tool_use in &message.tool_uses { - request_message - .content - .push(MessageContent::ToolUse(tool_use.clone())); + if let Some(tool_uses) = self.tool_uses_by_message.get(&message.id) { + for tool_use in tool_uses { + request_message + .content + .push(MessageContent::ToolUse(tool_use.clone())); + } } request.messages.push(request_message); @@ -143,10 +150,9 @@ impl Thread { match event { LanguageModelCompletionEvent::StartMessage { .. } => { thread.messages.push(Message { + id: thread.next_message_id.post_inc(), role: Role::Assistant, text: String::new(), - tool_uses: Vec::new(), - tool_results: Vec::new(), }); } LanguageModelCompletionEvent::Stop(reason) => { @@ -160,22 +166,28 @@ impl Thread { } } LanguageModelCompletionEvent::ToolUse(tool_use) => { - if let Some(last_message) = thread.messages.last_mut() { - if last_message.role == Role::Assistant { - last_message.tool_uses.push(tool_use.clone()); - } - } + if let Some(last_assistant_message) = thread + .messages + .iter() + .rfind(|message| message.role == Role::Assistant) + { + thread + .tool_uses_by_message + .entry(last_assistant_message.id) + .or_default() + .push(tool_use.clone()); - let tool_use_id: Arc = tool_use.id.into(); - thread.pending_tool_uses_by_id.insert( - tool_use_id.clone(), - PendingToolUse { - id: tool_use_id, - name: tool_use.name, - input: tool_use.input, - status: PendingToolUseStatus::Idle, - }, - ); + thread.pending_tool_uses_by_id.insert( + tool_use.id.clone(), + PendingToolUse { + assistant_message_id: last_assistant_message.id, + id: tool_use.id, + name: tool_use.name, + input: tool_use.input, + status: PendingToolUseStatus::Idle, + }, + ); + } } } @@ -235,7 +247,8 @@ impl Thread { pub fn insert_tool_output( &mut self, - tool_use_id: Arc, + assistant_message_id: MessageId, + tool_use_id: LanguageModelToolUseId, output: Task>, cx: &mut ModelContext, ) { @@ -244,19 +257,39 @@ impl Thread { async move { let output = output.await; thread - .update(&mut cx, |thread, cx| match output { - Ok(output) => { - thread - .completed_tool_uses_by_id - .insert(tool_use_id.clone(), output); + .update(&mut cx, |thread, cx| { + // The tool use was requested by an Assistant message, + // so we want to attach the tool results to the next + // user message. + let next_user_message = MessageId(assistant_message_id.0 + 1); - cx.emit(ThreadEvent::ToolFinished { tool_use_id }); - } - Err(err) => { - if let Some(tool_use) = - thread.pending_tool_uses_by_id.get_mut(&tool_use_id) - { - tool_use.status = PendingToolUseStatus::Error(err.to_string()); + let tool_results = thread + .tool_results_by_message + .entry(next_user_message) + .or_default(); + + match output { + Ok(output) => { + tool_results.push(LanguageModelToolResult { + tool_use_id: tool_use_id.to_string(), + content: output, + is_error: false, + }); + + cx.emit(ThreadEvent::ToolFinished { tool_use_id }); + } + Err(err) => { + tool_results.push(LanguageModelToolResult { + tool_use_id: tool_use_id.to_string(), + content: err.to_string(), + is_error: true, + }); + + if let Some(tool_use) = + thread.pending_tool_uses_by_id.get_mut(&tool_use_id) + { + tool_use.status = PendingToolUseStatus::Error(err.to_string()); + } } } }) @@ -278,7 +311,7 @@ pub enum ThreadEvent { UsePendingTools, ToolFinished { #[allow(unused)] - tool_use_id: Arc, + tool_use_id: LanguageModelToolUseId, }, } @@ -291,7 +324,9 @@ struct PendingCompletion { #[derive(Debug, Clone)] pub struct PendingToolUse { - pub id: Arc, + pub id: LanguageModelToolUseId, + /// The ID of the Assistant message in which the tool use was requested. + pub assistant_message_id: MessageId, pub name: String, pub input: serde_json::Value, pub status: PendingToolUseStatus, diff --git a/crates/language_model/src/language_model.rs b/crates/language_model/src/language_model.rs index f9df34a2d1..3c5a00bd85 100644 --- a/crates/language_model/src/language_model.rs +++ b/crates/language_model/src/language_model.rs @@ -63,9 +63,27 @@ pub enum StopReason { ToolUse, } +#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)] +pub struct LanguageModelToolUseId(Arc); + +impl fmt::Display for LanguageModelToolUseId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for LanguageModelToolUseId +where + T: Into>, +{ + fn from(value: T) -> Self { + Self(value.into()) + } +} + #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)] pub struct LanguageModelToolUse { - pub id: String, + pub id: LanguageModelToolUseId, pub name: String, pub input: serde_json::Value, } diff --git a/crates/language_model/src/request.rs b/crates/language_model/src/request.rs index 06dde1862a..e6f7f210c7 100644 --- a/crates/language_model/src/request.rs +++ b/crates/language_model/src/request.rs @@ -347,7 +347,7 @@ impl LanguageModelRequest { } MessageContent::ToolUse(tool_use) => { Some(anthropic::RequestContent::ToolUse { - id: tool_use.id, + id: tool_use.id.to_string(), name: tool_use.name, input: tool_use.input, cache_control, diff --git a/crates/language_models/src/provider/anthropic.rs b/crates/language_models/src/provider/anthropic.rs index 87460b824e..e882bb900d 100644 --- a/crates/language_models/src/provider/anthropic.rs +++ b/crates/language_models/src/provider/anthropic.rs @@ -498,7 +498,7 @@ pub fn map_to_language_model_completion_events( Some(maybe!({ Ok(LanguageModelCompletionEvent::ToolUse( LanguageModelToolUse { - id: tool_use.id, + id: tool_use.id.into(), name: tool_use.name, input: if tool_use.input_json.is_empty() { serde_json::Value::Null From 7d67bb4cf69e6d860d39911cc858d3f969d0ed3a Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 26 Nov 2024 12:23:38 +0200 Subject: [PATCH 153/886] Properly use lsp::CompletionList defaults (#21202) - Closes https://github.com/zed-industries/zed/issues/21185 Release Notes: - Fixed incorrect handling of the completion list defaults --- crates/editor/src/editor_tests.rs | 194 ++++++++++++++++++++++++++++++ crates/lsp/src/lsp.rs | 1 + crates/project/src/lsp_command.rs | 45 ++++++- 3 files changed, 234 insertions(+), 6 deletions(-) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 01507c4e31..669134ef10 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -10541,6 +10541,200 @@ async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) { cx.assert_editor_state(indoc! {"fn main() { let a = Some(2)ˇ; }"}); } +#[gpui::test] +async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string()]), + resolve_provider: Some(true), + ..Default::default() + }), + ..Default::default() + }, + cx, + ) + .await; + + cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"}); + cx.simulate_keystroke("."); + + let default_commit_characters = vec!["?".to_string()]; + let default_data = json!({ "very": "special"}); + let default_insert_text_format = lsp::InsertTextFormat::SNIPPET; + let default_insert_text_mode = lsp::InsertTextMode::AS_IS; + let default_edit_range = lsp::Range { + start: lsp::Position { + line: 0, + character: 5, + }, + end: lsp::Position { + line: 0, + character: 5, + }, + }; + + let completion_data = default_data.clone(); + let completion_characters = default_commit_characters.clone(); + cx.handle_request::(move |_, _, _| { + let default_data = completion_data.clone(); + let default_commit_characters = completion_characters.clone(); + async move { + Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList { + items: vec![ + lsp::CompletionItem { + label: "Some(2)".into(), + insert_text: Some("Some(2)".into()), + data: Some(json!({ "very": "special"})), + insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION), + text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace( + lsp::InsertReplaceEdit { + new_text: "Some(2)".to_string(), + insert: lsp::Range::default(), + replace: lsp::Range::default(), + }, + )), + ..lsp::CompletionItem::default() + }, + lsp::CompletionItem { + label: "vec![2]".into(), + insert_text: Some("vec![2]".into()), + insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT), + ..lsp::CompletionItem::default() + }, + ], + item_defaults: Some(lsp::CompletionListItemDefaults { + data: Some(default_data.clone()), + commit_characters: Some(default_commit_characters.clone()), + edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range( + default_edit_range, + )), + insert_text_format: Some(default_insert_text_format), + insert_text_mode: Some(default_insert_text_mode), + }), + ..lsp::CompletionList::default() + }))) + } + }) + .next() + .await; + + cx.condition(|editor, _| editor.context_menu_visible()) + .await; + + cx.update_editor(|editor, _| { + let menu = editor.context_menu.read(); + match menu.as_ref().expect("should have the completions menu") { + ContextMenu::Completions(completions_menu) => { + assert_eq!( + completions_menu + .matches + .iter() + .map(|c| c.string.as_str()) + .collect::>(), + vec!["Some(2)", "vec![2]"] + ); + } + ContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"), + } + }); + + cx.update_editor(|editor, cx| { + editor.context_menu_first(&ContextMenuFirst, cx); + }); + let first_item_resolve_characters = default_commit_characters.clone(); + cx.handle_request::(move |_, item_to_resolve, _| { + let default_commit_characters = first_item_resolve_characters.clone(); + + async move { + assert_eq!( + item_to_resolve.label, "Some(2)", + "Should have selected the first item" + ); + assert_eq!( + item_to_resolve.data, + Some(json!({ "very": "special"})), + "First item should bring its own data for resolving" + ); + assert_eq!( + item_to_resolve.commit_characters, + Some(default_commit_characters), + "First item had no own commit characters and should inherit the default ones" + ); + assert!( + matches!( + item_to_resolve.text_edit, + Some(lsp::CompletionTextEdit::InsertAndReplace { .. }) + ), + "First item should bring its own edit range for resolving" + ); + assert_eq!( + item_to_resolve.insert_text_format, + Some(default_insert_text_format), + "First item had no own insert text format and should inherit the default one" + ); + assert_eq!( + item_to_resolve.insert_text_mode, + Some(lsp::InsertTextMode::ADJUST_INDENTATION), + "First item should bring its own insert text mode for resolving" + ); + Ok(item_to_resolve) + } + }) + .next() + .await + .unwrap(); + + cx.update_editor(|editor, cx| { + editor.context_menu_last(&ContextMenuLast, cx); + }); + cx.handle_request::(move |_, item_to_resolve, _| { + let default_data = default_data.clone(); + let default_commit_characters = default_commit_characters.clone(); + async move { + assert_eq!( + item_to_resolve.label, "vec![2]", + "Should have selected the last item" + ); + assert_eq!( + item_to_resolve.data, + Some(default_data), + "Last item has no own resolve data and should inherit the default one" + ); + assert_eq!( + item_to_resolve.commit_characters, + Some(default_commit_characters), + "Last item had no own commit characters and should inherit the default ones" + ); + assert_eq!( + item_to_resolve.text_edit, + Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: default_edit_range, + new_text: "vec![2]".to_string() + })), + "Last item had no own edit range and should inherit the default one" + ); + assert_eq!( + item_to_resolve.insert_text_format, + Some(lsp::InsertTextFormat::PLAIN_TEXT), + "Last item should bring its own insert text format for resolving" + ); + assert_eq!( + item_to_resolve.insert_text_mode, + Some(default_insert_text_mode), + "Last item had no own insert text mode and should inherit the default one" + ); + + Ok(item_to_resolve) + } + }) + .next() + .await + .unwrap(); +} + #[gpui::test] async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 87c04030bd..98755583e3 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -697,6 +697,7 @@ impl LanguageServer { "commitCharacters".to_owned(), "editRange".to_owned(), "insertTextMode".to_owned(), + "insertTextFormat".to_owned(), "data".to_owned(), ]), }), diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 6de4902746..d317f5a4d4 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1775,21 +1775,54 @@ impl LspCommand for GetCompletions { if let Some(item_defaults) = item_defaults { let default_data = item_defaults.data.as_ref(); let default_commit_characters = item_defaults.commit_characters.as_ref(); + let default_edit_range = item_defaults.edit_range.as_ref(); + let default_insert_text_format = item_defaults.insert_text_format.as_ref(); let default_insert_text_mode = item_defaults.insert_text_mode.as_ref(); if default_data.is_some() || default_commit_characters.is_some() + || default_edit_range.is_some() + || default_insert_text_format.is_some() || default_insert_text_mode.is_some() { for item in completions.iter_mut() { - if let Some(data) = default_data { - item.data = Some(data.clone()) + if item.data.is_none() && default_data.is_some() { + item.data = default_data.cloned() } - if let Some(characters) = default_commit_characters { - item.commit_characters = Some(characters.clone()) + if item.commit_characters.is_none() && default_commit_characters.is_some() { + item.commit_characters = default_commit_characters.cloned() } - if let Some(text_mode) = default_insert_text_mode { - item.insert_text_mode = Some(*text_mode) + if item.text_edit.is_none() { + if let Some(default_edit_range) = default_edit_range { + match default_edit_range { + CompletionListItemDefaultsEditRange::Range(range) => { + item.text_edit = + Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: *range, + new_text: item.label.clone(), + })) + } + CompletionListItemDefaultsEditRange::InsertAndReplace { + insert, + replace, + } => { + item.text_edit = + Some(lsp::CompletionTextEdit::InsertAndReplace( + lsp::InsertReplaceEdit { + new_text: item.label.clone(), + insert: *insert, + replace: *replace, + }, + )) + } + } + } + } + if item.insert_text_format.is_none() && default_insert_text_format.is_some() { + item.insert_text_format = default_insert_text_format.cloned() + } + if item.insert_text_mode.is_none() && default_insert_text_mode.is_some() { + item.insert_text_mode = default_insert_text_mode.cloned() } } } From 9999c31859210654dd572d54dfa42b67c00b33b0 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 26 Nov 2024 14:29:54 +0200 Subject: [PATCH 154/886] Avoid endless loop of the diagnostic updates (#21209) Follow-up of https://github.com/zed-industries/zed/pull/21173 Rust-analyzer with `checkOnSave` enabled will push diagnostics for a file after each diagnostics refresh (e.g. save, file open, file close). If there's a file that is not open in any pane and has only warnings, and the diagnostics editor has warnings toggled off, then 0. rust-analyzer will push the corresponding warnings after initial load 1. the diagnostics editor code registers `project::Event::DiagnosticsUpdated` for the corresponding file path and opens the corresponding buffer to read its associated diagnostics from the snapshot 2. opening the buffer would send `textDocument/didOpen` which would trigger rust-analyzer to push the same diagnostics 3. meanwhile, the diagnostics editor would filter out all diagnostics for that buffer, dropping the open buffer and effectively closing it 4. closing the buffer will send `textDocument/didClose` which would trigger rust-analyzer to push the same diagnostics again, as those are `cargo check` ones, still present in the file 5. GOTO 1 Release Notes: - Fixed diagnostics editor not scrolling properly under certain conditions --- crates/diagnostics/src/diagnostics.rs | 41 ++++++++++++++++++--------- crates/project/src/lsp_store.rs | 15 ++++++++++ 2 files changed, 43 insertions(+), 13 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index be8da5c130..6db831c1ff 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -134,16 +134,27 @@ impl ProjectDiagnosticsEditor { language_server_id, path, } => { - this.paths_to_update - .insert((path.clone(), Some(*language_server_id))); - this.summary = project.read(cx).diagnostic_summary(false, cx); - cx.emit(EditorEvent::TitleChanged); + let max_severity = this.max_severity(); + let has_diagnostics_to_display = project.read(cx).lsp_store().read(cx).diagnostics_for_buffer(path) + .into_iter().flatten() + .filter(|(server_id, _)| language_server_id == server_id) + .flat_map(|(_, diagnostics)| diagnostics) + .any(|diagnostic| diagnostic.diagnostic.severity <= max_severity); - if this.editor.focus_handle(cx).contains_focused(cx) || this.focus_handle.contains_focused(cx) { - log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. recording change"); + if has_diagnostics_to_display { + this.paths_to_update + .insert((path.clone(), Some(*language_server_id))); + this.summary = project.read(cx).diagnostic_summary(false, cx); + cx.emit(EditorEvent::TitleChanged); + + if this.editor.focus_handle(cx).contains_focused(cx) || this.focus_handle.contains_focused(cx) { + log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. recording change"); + } else { + log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. updating excerpts"); + this.update_stale_excerpts(cx); + } } else { - log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. updating excerpts"); - this.update_stale_excerpts(cx); + log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. no diagnostics to display"); } } _ => {} @@ -329,16 +340,12 @@ impl ProjectDiagnosticsEditor { ExcerptId::min() }; + let max_severity = self.max_severity(); let path_state = &mut self.path_states[path_ix]; let mut new_group_ixs = Vec::new(); let mut blocks_to_add = Vec::new(); let mut blocks_to_remove = HashSet::default(); let mut first_excerpt_id = None; - let max_severity = if self.include_warnings { - DiagnosticSeverity::WARNING - } else { - DiagnosticSeverity::ERROR - }; let excerpts_snapshot = self.excerpts.update(cx, |excerpts, cx| { let mut old_groups = mem::take(&mut path_state.diagnostic_groups) .into_iter() @@ -627,6 +634,14 @@ impl ProjectDiagnosticsEditor { prev_path = Some(path); } } + + fn max_severity(&self) -> DiagnosticSeverity { + if self.include_warnings { + DiagnosticSeverity::WARNING + } else { + DiagnosticSeverity::ERROR + } + } } impl FocusableView for ProjectDiagnosticsEditor { diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index cc326285cb..29a0afcfe5 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -2919,6 +2919,21 @@ impl LspStore { }) } + pub fn diagnostics_for_buffer( + &self, + path: &ProjectPath, + ) -> Option< + &[( + LanguageServerId, + Vec>>, + )], + > { + self.diagnostics + .get(&path.worktree_id)? + .get(&path.path) + .map(|diagnostics| diagnostics.as_slice()) + } + pub fn started_language_servers(&self) -> Vec<(WorktreeId, LanguageServerName)> { self.language_server_ids.keys().cloned().collect() } From fdc17c57d71d7b9d5cd1dfa0eb7dc65566f602a0 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Tue, 26 Nov 2024 12:58:45 +0000 Subject: [PATCH 155/886] macos: Keybind improvements for binds involving shift (#21207) Fix cmd-pipe Remove redudnant jetbrains/sublime keybinds (these exist as `cmd-{` and `cmd-}` under default vscode keymap) and were broken as part of the recent keybind changes. Remove excess JSON whitespace from tests to make them more readable. --- assets/keymaps/default-macos.json | 2 +- crates/zed/src/zed.rs | 88 +++++-------------------------- 2 files changed, 15 insertions(+), 75 deletions(-) diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index c8bc80a9c0..ddbbdd3faf 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -355,7 +355,7 @@ "alt-cmd-f12": "editor::GoToTypeDefinitionSplit", "alt-shift-f12": "editor::FindAllReferences", "ctrl-m": "editor::MoveToEnclosingBracket", - "cmd-shift-\\": "editor::MoveToEnclosingBracket", + "cmd-|": "editor::MoveToEnclosingBracket", "alt-cmd-[": "editor::Fold", "alt-cmd-]": "editor::UnfoldLines", "cmd-k cmd-l": "editor::ToggleFold", diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 5ba63b9c1f..4e3d05d2fb 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -3190,12 +3190,7 @@ mod tests { .fs .save( "/settings.json".as_ref(), - &r#" - { - "base_keymap": "Atom" - } - "# - .into(), + &r#"{"base_keymap": "Atom"}"#.into(), Default::default(), ) .await @@ -3205,16 +3200,7 @@ mod tests { .fs .save( "/keymap.json".as_ref(), - &r#" - [ - { - "bindings": { - "backspace": "test1::A" - } - } - ] - "# - .into(), + &r#"[{"bindings": {"backspace": "test1::A"}}]"#.into(), Default::default(), ) .await @@ -3257,16 +3243,7 @@ mod tests { .fs .save( "/keymap.json".as_ref(), - &r#" - [ - { - "bindings": { - "backspace": "test1::B" - } - } - ] - "# - .into(), + &r#"[{"bindings": {"backspace": "test1::B"}}]"#.into(), Default::default(), ) .await @@ -3286,12 +3263,7 @@ mod tests { .fs .save( "/settings.json".as_ref(), - &r#" - { - "base_keymap": "JetBrains" - } - "# - .into(), + &r#"{"base_keymap": "JetBrains"}"#.into(), Default::default(), ) .await @@ -3318,24 +3290,20 @@ mod tests { // From the Atom keymap use workspace::ActivatePreviousPane; // From the JetBrains keymap - use pane::ActivatePrevItem; + use diagnostics::Deploy; + workspace .update(cx, |workspace, _| { - workspace - .register_action(|_, _: &A, _| {}) - .register_action(|_, _: &B, _| {}); + workspace.register_action(|_, _: &A, _cx| {}); + workspace.register_action(|_, _: &B, _cx| {}); + workspace.register_action(|_, _: &Deploy, _cx| {}); }) .unwrap(); app_state .fs .save( "/settings.json".as_ref(), - &r#" - { - "base_keymap": "Atom" - } - "# - .into(), + &r#"{"base_keymap": "Atom"}"#.into(), Default::default(), ) .await @@ -3344,16 +3312,7 @@ mod tests { .fs .save( "/keymap.json".as_ref(), - &r#" - [ - { - "bindings": { - "backspace": "test2::A" - } - } - ] - "# - .into(), + &r#"[{"bindings": {"backspace": "test2::A"}}]"#.into(), Default::default(), ) .await @@ -3391,16 +3350,7 @@ mod tests { .fs .save( "/keymap.json".as_ref(), - &r#" - [ - { - "bindings": { - "backspace": null - } - } - ] - "# - .into(), + &r#"[{"bindings": {"backspace": null}}]"#.into(), Default::default(), ) .await @@ -3420,12 +3370,7 @@ mod tests { .fs .save( "/settings.json".as_ref(), - &r#" - { - "base_keymap": "JetBrains" - } - "# - .into(), + &r#"{"base_keymap": "JetBrains"}"#.into(), Default::default(), ) .await @@ -3433,12 +3378,7 @@ mod tests { cx.background_executor.run_until_parked(); - assert_key_bindings_for( - workspace.into(), - cx, - vec![("[", &ActivatePrevItem)], - line!(), - ); + assert_key_bindings_for(workspace.into(), cx, vec![("6", &Deploy)], line!()); } #[gpui::test] From 8f1ec3d11b76399473ac76be374443ee9692b58d Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 26 Nov 2024 10:48:48 -0500 Subject: [PATCH 156/886] assistant2: Add a checkbox to control tool use (#21215) This PR adds a checkbox to the `assistant2` message editor to control whether tools should be used for a given message. Release Notes: - N/A --- crates/assistant2/src/message_editor.rs | 31 ++++++++++++++++++------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/crates/assistant2/src/message_editor.rs b/crates/assistant2/src/message_editor.rs index c42d66a4d7..7f789587c6 100644 --- a/crates/assistant2/src/message_editor.rs +++ b/crates/assistant2/src/message_editor.rs @@ -1,10 +1,9 @@ use editor::{Editor, EditorElement, EditorStyle}; -use feature_flags::{FeatureFlagAppExt, ToolUseFeatureFlag}; use gpui::{AppContext, FocusableView, Model, TextStyle, View}; use language_model::{LanguageModelRegistry, LanguageModelRequestTool}; use settings::Settings; use theme::ThemeSettings; -use ui::{prelude::*, ButtonLike, ElevationIndex, KeyBinding}; +use ui::{prelude::*, ButtonLike, CheckboxWithLabel, ElevationIndex, KeyBinding}; use crate::thread::{RequestKind, Thread}; use crate::Chat; @@ -12,6 +11,7 @@ use crate::Chat; pub struct MessageEditor { thread: Model, editor: View, + use_tools: bool, } impl MessageEditor { @@ -24,6 +24,7 @@ impl MessageEditor { editor }), + use_tools: false, } } @@ -58,7 +59,7 @@ impl MessageEditor { thread.insert_user_message(user_message); let mut request = thread.to_completion_request(request_kind, cx); - if cx.has_flag::() { + if self.use_tools { request.tools = thread .tools() .tools(cx) @@ -123,12 +124,24 @@ impl Render for MessageEditor { h_flex() .justify_between() .child( - h_flex().child( - Button::new("add-context", "Add Context") - .style(ButtonStyle::Filled) - .icon(IconName::Plus) - .icon_position(IconPosition::Start), - ), + h_flex() + .child( + Button::new("add-context", "Add Context") + .style(ButtonStyle::Filled) + .icon(IconName::Plus) + .icon_position(IconPosition::Start), + ) + .child(CheckboxWithLabel::new( + "use-tools", + Label::new("Tools"), + self.use_tools.into(), + cx.listener(|this, selection, _cx| { + this.use_tools = match selection { + Selection::Selected => true, + Selection::Unselected | Selection::Indeterminate => false, + }; + }), + )), ) .child( h_flex() From 884748038e9c99b83b943d4550dd3cf515563071 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Tue, 26 Nov 2024 11:09:43 -0500 Subject: [PATCH 157/886] Styling for Apply/Discard buttons (#21017) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change the "Apply" and "Discard" buttons to match @danilo-leal's design! Here are some different states: ### Cursor in the first hunk Now that the cursor is in a particular hunk, we show the "Apply" and "Discard" names, and the keyboard shortcut. If I press the keyboard shortcut, it will only apply to this hunk. Screenshot 2024-11-23 at 10 54 45 PM ### Cursor in the second hunk Moving the cursor to a different hunk changes which buttons get the keyboard shortcut treatment. Now the keyboard shortcut is shown next to the hunk that will actually be affected if you press that shortcut. Screenshot 2024-11-23 at 10 56 27 PM Release Notes: - Restyled Apply/Discard buttons --------- Co-authored-by: Max Co-authored-by: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Co-authored-by: Danilo Leal Co-authored-by: Bennet Bo Fenner <53836821+bennetbo@users.noreply.github.com> --- assets/keymaps/default-linux.json | 2 +- assets/keymaps/default-macos.json | 2 +- crates/editor/src/actions.rs | 2 +- crates/editor/src/editor.rs | 64 ++- crates/editor/src/editor_tests.rs | 2 +- crates/editor/src/element.rs | 23 +- crates/editor/src/hunk_diff.rs | 483 ++++++++++--------- crates/editor/src/proposed_changes_editor.rs | 118 ++++- crates/zed/src/zed.rs | 8 +- 9 files changed, 411 insertions(+), 293 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 2eedc1c839..9ba416c210 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -522,7 +522,7 @@ { "context": "ProposedChangesEditor", "bindings": { - "ctrl-shift-y": "editor::ApplyDiffHunk", + "ctrl-shift-y": "editor::ApplySelectedDiffHunks", "ctrl-alt-a": "editor::ApplyAllDiffHunks" } }, diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index ddbbdd3faf..a4eae2af52 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -562,7 +562,7 @@ { "context": "ProposedChangesEditor", "bindings": { - "cmd-shift-y": "editor::ApplyDiffHunk", + "cmd-shift-y": "editor::ApplySelectedDiffHunks", "cmd-shift-a": "editor::ApplyAllDiffHunks" } }, diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 5b11b18bc2..719a35a009 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -209,7 +209,7 @@ gpui::actions!( AddSelectionAbove, AddSelectionBelow, ApplyAllDiffHunks, - ApplyDiffHunk, + ApplySelectedDiffHunks, Backspace, Cancel, CancelLanguageServerWork, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 78f0aab5a5..eeaaeb5c2b 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -99,7 +99,8 @@ use language::{ use language::{point_to_lsp, BufferRow, CharClassifier, Runnable, RunnableRange}; use linked_editing_ranges::refresh_linked_ranges; pub use proposed_changes_editor::{ - ProposedChangeLocation, ProposedChangesEditor, ProposedChangesEditorToolbar, + ProposedChangeLocation, ProposedChangesEditor, ProposedChangesToolbar, + ProposedChangesToolbarControls, }; use similar::{ChangeTag, TextDiff}; use std::iter::Peekable; @@ -160,7 +161,7 @@ use theme::{ }; use ui::{ h_flex, prelude::*, ButtonSize, ButtonStyle, Disclosure, IconButton, IconName, IconSize, - ListItem, Popover, PopoverMenuHandle, Tooltip, + ListItem, Popover, Tooltip, }; use util::{defer, maybe, post_inc, RangeExt, ResultExt, TryFutureExt}; use workspace::item::{ItemHandle, PreviewTabsSettings}; @@ -590,7 +591,6 @@ pub struct Editor { nav_history: Option, context_menu: RwLock>, mouse_context_menu: Option, - hunk_controls_menu_handle: PopoverMenuHandle, completion_tasks: Vec<(CompletionId, Task>)>, signature_help_state: SignatureHelpState, auto_signature_help: Option, @@ -2112,7 +2112,6 @@ impl Editor { nav_history: None, context_menu: RwLock::new(None), mouse_context_menu: None, - hunk_controls_menu_handle: PopoverMenuHandle::default(), completion_tasks: Default::default(), signature_help_state: SignatureHelpState::default(), auto_signature_help: None, @@ -13558,20 +13557,24 @@ fn test_wrap_with_prefix() { ); } +fn is_hunk_selected(hunk: &MultiBufferDiffHunk, selections: &[Selection]) -> bool { + let mut buffer_rows_for_selections = selections.iter().map(|selection| { + let start = MultiBufferRow(selection.start.row); + let end = MultiBufferRow(selection.end.row); + start..end + }); + + buffer_rows_for_selections.any(|range| does_selection_touch_hunk(&range, hunk)) +} + fn hunks_for_selections( multi_buffer_snapshot: &MultiBufferSnapshot, selections: &[Selection], ) -> Vec { let buffer_rows_for_selections = selections.iter().map(|selection| { - let head = selection.head(); - let tail = selection.tail(); - let start = MultiBufferRow(tail.to_point(multi_buffer_snapshot).row); - let end = MultiBufferRow(head.to_point(multi_buffer_snapshot).row); - if start > end { - end..start - } else { - start..end - } + let start = MultiBufferRow(selection.start.to_point(multi_buffer_snapshot).row); + let end = MultiBufferRow(selection.end.to_point(multi_buffer_snapshot).row); + start..end }); hunks_for_rows(buffer_rows_for_selections, multi_buffer_snapshot) @@ -13588,19 +13591,8 @@ pub fn hunks_for_rows( let query_rows = selected_multi_buffer_rows.start..selected_multi_buffer_rows.end.next_row(); for hunk in multi_buffer_snapshot.git_diff_hunks_in_range(query_rows.clone()) { - // Deleted hunk is an empty row range, no caret can be placed there and Zed allows to revert it - // when the caret is just above or just below the deleted hunk. - let allow_adjacent = hunk_status(&hunk) == DiffHunkStatus::Removed; - let related_to_selection = if allow_adjacent { - hunk.row_range.overlaps(&query_rows) - || hunk.row_range.start == query_rows.end - || hunk.row_range.end == query_rows.start - } else { - // `selected_multi_buffer_rows` are inclusive (e.g. [2..2] means 2nd row is selected) - // `hunk.row_range` is exclusive (e.g. [2..3] means 2nd row is selected) - hunk.row_range.overlaps(&selected_multi_buffer_rows) - || selected_multi_buffer_rows.end == hunk.row_range.start - }; + let related_to_selection = + does_selection_touch_hunk(&selected_multi_buffer_rows, &hunk); if related_to_selection { if !processed_buffer_rows .entry(hunk.buffer_id) @@ -13617,6 +13609,26 @@ pub fn hunks_for_rows( hunks } +fn does_selection_touch_hunk( + selected_multi_buffer_rows: &Range, + hunk: &MultiBufferDiffHunk, +) -> bool { + let query_rows = selected_multi_buffer_rows.start..selected_multi_buffer_rows.end.next_row(); + // Deleted hunk is an empty row range, no caret can be placed there and Zed allows to revert it + // when the caret is just above or just below the deleted hunk. + let allow_adjacent = hunk_status(hunk) == DiffHunkStatus::Removed; + if allow_adjacent { + hunk.row_range.overlaps(&query_rows) + || hunk.row_range.start == query_rows.end + || hunk.row_range.end == query_rows.start + } else { + // `selected_multi_buffer_rows` are inclusive (e.g. [2..2] means 2nd row is selected) + // `hunk.row_range` is exclusive (e.g. [2..3] means 2nd row is selected) + hunk.row_range.overlaps(selected_multi_buffer_rows) + || selected_multi_buffer_rows.end == hunk.row_range.start + } +} + pub trait CollaborationHub { fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap; fn user_participant_indices<'a>( diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 669134ef10..397d5e46d4 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -12552,7 +12552,7 @@ async fn test_edits_around_expanded_insertion_hunks( executor.run_until_parked(); cx.assert_diff_hunks( r#" - use some::mod1; + - use some::mod1; - use some::mod2; - - const A: u32 = 42; diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 7f4bc3fb77..19c1f3bf39 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -2509,6 +2509,7 @@ impl EditorElement { element, available_space: size(AvailableSpace::MinContent, element_size.height.into()), style: BlockStyle::Fixed, + is_zero_height: block.height() == 0, }); } for (row, block) in non_fixed_blocks { @@ -2555,6 +2556,7 @@ impl EditorElement { element, available_space: size(width.into(), element_size.height.into()), style, + is_zero_height: block.height() == 0, }); } @@ -2602,6 +2604,7 @@ impl EditorElement { element, available_space: size(width, element_size.height.into()), style, + is_zero_height: block.height() == 0, }); } } @@ -3947,8 +3950,23 @@ impl EditorElement { } fn paint_blocks(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) { - for mut block in layout.blocks.drain(..) { - block.element.paint(cx); + cx.paint_layer(layout.text_hitbox.bounds, |cx| { + layout.blocks.retain_mut(|block| { + if !block.is_zero_height { + block.element.paint(cx); + } + + block.is_zero_height + }); + }); + + // Paint all the zero-height blocks in a higher layer (if there were any remaining to paint). + if !layout.blocks.is_empty() { + cx.paint_layer(layout.text_hitbox.bounds, |cx| { + for mut block in layout.blocks.drain(..) { + block.element.paint(cx); + } + }); } } @@ -6011,6 +6029,7 @@ struct BlockLayout { element: AnyElement, available_space: Size, style: BlockStyle, + is_zero_height: bool, } fn layout_line( diff --git a/crates/editor/src/hunk_diff.rs b/crates/editor/src/hunk_diff.rs index 27bb8ac557..5c6d5ff7a3 100644 --- a/crates/editor/src/hunk_diff.rs +++ b/crates/editor/src/hunk_diff.rs @@ -1,6 +1,8 @@ use collections::{hash_map, HashMap, HashSet}; use git::diff::DiffHunkStatus; -use gpui::{Action, AnchorCorner, AppContext, CursorStyle, Hsla, Model, MouseButton, Task, View}; +use gpui::{ + AppContext, ClickEvent, CursorStyle, FocusableView, Hsla, Model, MouseButton, Task, View, +}; use language::{Buffer, BufferId, Point}; use multi_buffer::{ Anchor, AnchorRangeExt, ExcerptRange, MultiBuffer, MultiBufferDiffHunk, MultiBufferRow, @@ -9,17 +11,18 @@ use multi_buffer::{ use std::{ops::Range, sync::Arc}; use text::OffsetRangeExt; use ui::{ - prelude::*, ActiveTheme, ContextMenu, IconButtonShape, InteractiveElement, IntoElement, - ParentElement, PopoverMenu, Styled, Tooltip, ViewContext, VisualContext, + prelude::*, ActiveTheme, IconButtonShape, InteractiveElement, IntoElement, KeyBinding, + ParentElement, Styled, TintColor, Tooltip, ViewContext, VisualContext, }; use util::RangeExt; use workspace::Item; use crate::{ - editor_settings::CurrentLineHighlight, hunk_status, hunks_for_selections, ApplyAllDiffHunks, - ApplyDiffHunk, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, DiffRowHighlight, - DisplayRow, DisplaySnapshot, Editor, EditorElement, ExpandAllHunkDiffs, GoToHunk, GoToPrevHunk, - RevertFile, RevertSelectedHunks, ToDisplayPoint, ToggleHunkDiff, + editor_settings::CurrentLineHighlight, hunk_status, hunks_for_selections, is_hunk_selected, + ApplyAllDiffHunks, ApplySelectedDiffHunks, BlockPlacement, BlockProperties, BlockStyle, + CustomBlockId, DiffRowHighlight, DisplayRow, DisplaySnapshot, Editor, EditorElement, + ExpandAllHunkDiffs, GoToHunk, GoToPrevHunk, RevertSelectedHunks, ToDisplayPoint, + ToggleHunkDiff, }; #[derive(Debug, Clone)] @@ -57,7 +60,6 @@ pub enum DisplayDiffHunk { Folded { display_row: DisplayRow, }, - Unfolded { diff_base_byte_range: Range, display_row_range: Range, @@ -371,26 +373,35 @@ impl Editor { pub(crate) fn apply_selected_diff_hunks( &mut self, - _: &ApplyDiffHunk, + _: &ApplySelectedDiffHunks, cx: &mut ViewContext, ) { let snapshot = self.buffer.read(cx).snapshot(cx); let hunks = hunks_for_selections(&snapshot, &self.selections.disjoint_anchors()); - let mut ranges_by_buffer = HashMap::default(); - self.transact(cx, |editor, cx| { - for hunk in hunks { - if let Some(buffer) = editor.buffer.read(cx).buffer(hunk.buffer_id) { - ranges_by_buffer - .entry(buffer.clone()) - .or_insert_with(Vec::new) - .push(hunk.buffer_range.to_offset(buffer.read(cx))); - } - } - for (buffer, ranges) in ranges_by_buffer { - buffer.update(cx, |buffer, cx| { - buffer.merge_into_base(ranges, cx); - }); + self.transact(cx, |editor, cx| { + if hunks.is_empty() { + // If there are no selected hunks, e.g. because we're using the keybinding with nothing selected, apply the first hunk. + if let Some(first_hunk) = editor.expanded_hunks.hunks.first() { + editor.apply_diff_hunks_in_range(first_hunk.hunk_range.clone(), cx); + } + } else { + let mut ranges_by_buffer = HashMap::default(); + + for hunk in hunks { + if let Some(buffer) = editor.buffer.read(cx).buffer(hunk.buffer_id) { + ranges_by_buffer + .entry(buffer.clone()) + .or_insert_with(Vec::new) + .push(hunk.buffer_range.to_offset(buffer.read(cx))); + } + } + + for (buffer, ranges) in ranges_by_buffer { + buffer.update(cx, |buffer, cx| { + buffer.merge_into_base(ranges, cx); + }); + } } }); @@ -412,246 +423,238 @@ impl Editor { buffer.read(cx).diff_base_buffer().is_some() }); - let border_color = cx.theme().colors().border_variant; - let bg_color = cx.theme().colors().editor_background; - let gutter_color = match hunk.status { - DiffHunkStatus::Added => cx.theme().status().created, - DiffHunkStatus::Modified => cx.theme().status().modified, - DiffHunkStatus::Removed => cx.theme().status().deleted, - }; - BlockProperties { placement: BlockPlacement::Above(hunk.multi_buffer_range.start), - height: 1, + height: 0, style: BlockStyle::Sticky, - priority: 0, + priority: 1, render: Arc::new({ let editor = cx.view().clone(); let hunk = hunk.clone(); move |cx| { - let hunk_controls_menu_handle = - editor.read(cx).hunk_controls_menu_handle.clone(); + let is_hunk_selected = editor.update(&mut **cx, |editor, cx| { + let snapshot = editor.buffer.read(cx).snapshot(cx); + let selections = &editor.selections.all::(cx); + + if editor.focus_handle(cx).is_focused(cx) && !selections.is_empty() { + if let Some(hunk) = to_diff_hunk(&hunk, &snapshot) { + is_hunk_selected(&hunk, selections) + } else { + false + } + } else { + // If we have no cursor, or aren't focused, then default to the first hunk + // because that's what the keyboard shortcuts do. + editor + .expanded_hunks + .hunks + .first() + .map(|first_hunk| first_hunk.hunk_range == hunk.multi_buffer_range) + .unwrap_or(false) + } + }); + + let focus_handle = editor.focus_handle(cx); + + let handle_discard_click = { + let editor = editor.clone(); + let hunk = hunk.clone(); + move |_event: &ClickEvent, cx: &mut WindowContext| { + let multi_buffer = editor.read(cx).buffer().clone(); + let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx); + let mut revert_changes = HashMap::default(); + if let Some(hunk) = + crate::hunk_diff::to_diff_hunk(&hunk, &multi_buffer_snapshot) + { + Editor::prepare_revert_change( + &mut revert_changes, + &multi_buffer, + &hunk, + cx, + ); + } + if !revert_changes.is_empty() { + editor.update(cx, |editor, cx| editor.revert(revert_changes, cx)); + } + } + }; + + let handle_apply_click = { + let editor = editor.clone(); + let hunk = hunk.clone(); + move |_event: &ClickEvent, cx: &mut WindowContext| { + editor.update(cx, |editor, cx| { + editor + .apply_diff_hunks_in_range(hunk.multi_buffer_range.clone(), cx); + }); + } + }; + + let discard_key_binding = + KeyBinding::for_action_in(&RevertSelectedHunks, &focus_handle, cx); + + let discard_tooltip = { + let focus_handle = editor.focus_handle(cx); + move |cx: &mut WindowContext| { + Tooltip::for_action_in( + "Discard Hunk", + &RevertSelectedHunks, + &focus_handle, + cx, + ) + } + }; h_flex() .id(cx.block_id) - .block_mouse_down() - .h(cx.line_height()) + .pr_5() .w_full() - .border_t_1() - .border_color(border_color) - .bg(bg_color) - .child( - div() - .id("gutter-strip") - .w(EditorElement::diff_hunk_strip_width(cx.line_height())) - .h_full() - .bg(gutter_color) - .cursor(CursorStyle::PointingHand) - .on_click({ - let editor = editor.clone(); - let hunk = hunk.clone(); - move |_event, cx| { - editor.update(cx, |editor, cx| { - editor.toggle_hovered_hunk(&hunk, cx); - }); - } - }), - ) + .justify_end() .child( h_flex() - .px_6() - .size_full() - .justify_end() - .child( - h_flex() - .gap_1() - .when(!is_branch_buffer, |row| { - row.child( - IconButton::new("next-hunk", IconName::ArrowDown) - .shape(IconButtonShape::Square) - .icon_size(IconSize::Small) - .tooltip({ - let focus_handle = editor.focus_handle(cx); - move |cx| { - Tooltip::for_action_in( - "Next Hunk", - &GoToHunk, - &focus_handle, - cx, - ) - } - }) - .on_click({ - let editor = editor.clone(); - let hunk = hunk.clone(); - move |_event, cx| { - editor.update(cx, |editor, cx| { - editor.go_to_subsequent_hunk( - hunk.multi_buffer_range.end, - cx, - ); - }); - } - }), - ) - .child( - IconButton::new("prev-hunk", IconName::ArrowUp) - .shape(IconButtonShape::Square) - .icon_size(IconSize::Small) - .tooltip({ - let focus_handle = editor.focus_handle(cx); - move |cx| { - Tooltip::for_action_in( - "Previous Hunk", - &GoToPrevHunk, - &focus_handle, - cx, - ) - } - }) - .on_click({ - let editor = editor.clone(); - let hunk = hunk.clone(); - move |_event, cx| { - editor.update(cx, |editor, cx| { - editor.go_to_preceding_hunk( - hunk.multi_buffer_range.start, - cx, - ); - }); - } - }), - ) - }) - .child( - IconButton::new("discard", IconName::Undo) + .h(cx.line_height()) + .gap_1() + .px_1() + .pb_1() + .border_x_1() + .border_b_1() + .border_color(cx.theme().colors().border_variant) + .rounded_b_lg() + .bg(cx.theme().colors().editor_background) + .shadow(smallvec::smallvec![gpui::BoxShadow { + color: gpui::hsla(0.0, 0.0, 0.0, 0.1), + blur_radius: px(1.0), + spread_radius: px(1.0), + offset: gpui::point(px(0.), px(1.0)), + }]) + .when(!is_branch_buffer, |row| { + row.child( + IconButton::new("next-hunk", IconName::ArrowDown) + .shape(IconButtonShape::Square) + .icon_size(IconSize::Small) + .tooltip({ + let focus_handle = editor.focus_handle(cx); + move |cx| { + Tooltip::for_action_in( + "Next Hunk", + &GoToHunk, + &focus_handle.clone(), + cx, + ) + } + }) + .on_click({ + let editor = editor.clone(); + let hunk = hunk.clone(); + move |_event, cx| { + editor.update(cx, |editor, cx| { + editor.go_to_subsequent_hunk( + hunk.multi_buffer_range.end, + cx, + ); + }); + } + }), + ) + .child( + IconButton::new("prev-hunk", IconName::ArrowUp) + .shape(IconButtonShape::Square) + .icon_size(IconSize::Small) + .tooltip({ + let focus_handle = editor.focus_handle(cx); + move |cx| { + Tooltip::for_action_in( + "Previous Hunk", + &GoToPrevHunk, + &focus_handle, + cx, + ) + } + }) + .on_click({ + let editor = editor.clone(); + let hunk = hunk.clone(); + move |_event, cx| { + editor.update(cx, |editor, cx| { + editor.go_to_preceding_hunk( + hunk.multi_buffer_range.start, + cx, + ); + }); + } + }), + ) + }) + .child(if is_branch_buffer { + if is_hunk_selected { + Button::new("discard", "Discard") + .style(ButtonStyle::Tinted(TintColor::Negative)) + .label_size(LabelSize::Small) + .key_binding(discard_key_binding) + .on_click(handle_discard_click.clone()) + .into_any_element() + } else { + IconButton::new("discard", IconName::Close) + .style(ButtonStyle::Tinted(TintColor::Negative)) + .shape(IconButtonShape::Square) + .icon_size(IconSize::XSmall) + .tooltip(discard_tooltip.clone()) + .on_click(handle_discard_click.clone()) + .into_any_element() + } + } else { + if is_hunk_selected { + Button::new("undo", "Undo") + .style(ButtonStyle::Tinted(TintColor::Negative)) + .label_size(LabelSize::Small) + .key_binding(discard_key_binding) + .on_click(handle_discard_click.clone()) + .into_any_element() + } else { + IconButton::new("undo", IconName::Undo) + .shape(IconButtonShape::Square) + .icon_size(IconSize::Small) + .tooltip(discard_tooltip.clone()) + .on_click(handle_discard_click.clone()) + .into_any_element() + } + }) + .when(is_branch_buffer, |this| { + this.child({ + let button = Button::new("apply", "Apply") + .style(ButtonStyle::Tinted(TintColor::Positive)) + .label_size(LabelSize::Small) + .key_binding(KeyBinding::for_action_in( + &ApplySelectedDiffHunks, + &focus_handle, + cx, + )) + .on_click(handle_apply_click.clone()) + .into_any_element(); + if is_hunk_selected { + button + } else { + IconButton::new("apply", IconName::Check) + .style(ButtonStyle::Tinted(TintColor::Positive)) .shape(IconButtonShape::Square) - .icon_size(IconSize::Small) + .icon_size(IconSize::XSmall) .tooltip({ let focus_handle = editor.focus_handle(cx); move |cx| { Tooltip::for_action_in( - "Discard Hunk", - &RevertSelectedHunks, + "Apply Hunk", + &ApplySelectedDiffHunks, &focus_handle, cx, ) } }) - .on_click({ - let editor = editor.clone(); - let hunk = hunk.clone(); - move |_event, cx| { - let multi_buffer = - editor.read(cx).buffer().clone(); - let multi_buffer_snapshot = - multi_buffer.read(cx).snapshot(cx); - let mut revert_changes = HashMap::default(); - if let Some(hunk) = - crate::hunk_diff::to_diff_hunk( - &hunk, - &multi_buffer_snapshot, - ) - { - Editor::prepare_revert_change( - &mut revert_changes, - &multi_buffer, - &hunk, - cx, - ); - } - if !revert_changes.is_empty() { - editor.update(cx, |editor, cx| { - editor.revert(revert_changes, cx) - }); - } - } - }), - ) - .map(|this| { - if is_branch_buffer { - this.child( - IconButton::new("apply", IconName::Check) - .shape(IconButtonShape::Square) - .icon_size(IconSize::Small) - .tooltip({ - let focus_handle = - editor.focus_handle(cx); - move |cx| { - Tooltip::for_action_in( - "Apply Hunk", - &ApplyDiffHunk, - &focus_handle, - cx, - ) - } - }) - .on_click({ - let editor = editor.clone(); - let hunk = hunk.clone(); - move |_event, cx| { - editor.update(cx, |editor, cx| { - editor - .apply_diff_hunks_in_range( - hunk.multi_buffer_range - .clone(), - cx, - ); - }); - } - }), - ) - } else { - this.child({ - let focus = editor.focus_handle(cx); - PopoverMenu::new("hunk-controls-dropdown") - .trigger( - IconButton::new( - "toggle_editor_selections_icon", - IconName::EllipsisVertical, - ) - .shape(IconButtonShape::Square) - .icon_size(IconSize::Small) - .style(ButtonStyle::Subtle) - .selected( - hunk_controls_menu_handle - .is_deployed(), - ) - .when( - !hunk_controls_menu_handle - .is_deployed(), - |this| { - this.tooltip(|cx| { - Tooltip::text( - "Hunk Controls", - cx, - ) - }) - }, - ), - ) - .anchor(AnchorCorner::TopRight) - .with_handle(hunk_controls_menu_handle) - .menu(move |cx| { - let focus = focus.clone(); - let menu = ContextMenu::build( - cx, - move |menu, _| { - menu.context(focus.clone()) - .action( - "Discard All Hunks", - RevertFile - .boxed_clone(), - ) - }, - ); - Some(menu) - }) - }) - } - }), - ) + .on_click(handle_apply_click.clone()) + .into_any_element() + } + }) + }) .when(!is_branch_buffer, |div| { div.child( IconButton::new("collapse", IconName::Close) @@ -707,7 +710,7 @@ impl Editor { placement: BlockPlacement::Above(hunk.multi_buffer_range.start), height, style: BlockStyle::Flex, - priority: 0, + priority: 1, render: Arc::new(move |cx| { let width = EditorElement::diff_hunk_strip_width(cx.line_height()); let gutter_dimensions = editor.read(cx.context).gutter_dimensions; diff --git a/crates/editor/src/proposed_changes_editor.rs b/crates/editor/src/proposed_changes_editor.rs index ac97fe18da..3a9509eb39 100644 --- a/crates/editor/src/proposed_changes_editor.rs +++ b/crates/editor/src/proposed_changes_editor.rs @@ -5,10 +5,11 @@ use gpui::{AppContext, EventEmitter, FocusableView, Model, Render, Subscription, use language::{Buffer, BufferEvent, Capability}; use multi_buffer::{ExcerptRange, MultiBuffer}; use project::Project; +use settings::Settings; use smol::stream::StreamExt; use std::{any::TypeId, ops::Range, rc::Rc, time::Duration}; use text::ToOffset; -use ui::{prelude::*, ButtonLike, KeyBinding}; +use ui::{prelude::*, KeyBinding}; use workspace::{ searchable::SearchableItemHandle, Item, ItemHandle as _, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, @@ -34,7 +35,11 @@ struct BufferEntry { _subscription: Subscription, } -pub struct ProposedChangesEditorToolbar { +pub struct ProposedChangesToolbarControls { + current_editor: Option>, +} + +pub struct ProposedChangesToolbar { current_editor: Option>, } @@ -228,6 +233,10 @@ impl ProposedChangesEditor { _ => (), } } + + fn all_changes_accepted(&self) -> bool { + false // In the future, we plan to compute this based on the current state of patches. + } } impl Render for ProposedChangesEditor { @@ -251,7 +260,11 @@ impl Item for ProposedChangesEditor { type Event = EditorEvent; fn tab_icon(&self, _cx: &ui::WindowContext) -> Option { - Some(Icon::new(IconName::Diff)) + if self.all_changes_accepted() { + Some(Icon::new(IconName::Check).color(Color::Success)) + } else { + Some(Icon::new(IconName::ZedAssistant)) + } } fn tab_content_text(&self, _cx: &WindowContext) -> Option { @@ -317,7 +330,7 @@ impl Item for ProposedChangesEditor { } } -impl ProposedChangesEditorToolbar { +impl ProposedChangesToolbarControls { pub fn new() -> Self { Self { current_editor: None, @@ -333,28 +346,97 @@ impl ProposedChangesEditorToolbar { } } -impl Render for ProposedChangesEditorToolbar { +impl Render for ProposedChangesToolbarControls { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let button_like = ButtonLike::new("apply-changes").child(Label::new("Apply All")); + if let Some(editor) = &self.current_editor { + let focus_handle = editor.focus_handle(cx); + let action = &ApplyAllDiffHunks; + let keybinding = KeyBinding::for_action_in(action, &focus_handle, cx); - match &self.current_editor { - Some(editor) => { - let focus_handle = editor.focus_handle(cx); - let keybinding = KeyBinding::for_action_in(&ApplyAllDiffHunks, &focus_handle, cx) - .map(|binding| binding.into_any_element()); + let editor = editor.read(cx); - button_like.children(keybinding).on_click({ - move |_event, cx| focus_handle.dispatch_action(&ApplyAllDiffHunks, cx) - }) - } - None => button_like.disabled(true), + let apply_all_button = if editor.all_changes_accepted() { + None + } else { + Some( + Button::new("apply-changes", "Apply All") + .style(ButtonStyle::Filled) + .key_binding(keybinding) + .on_click(move |_event, cx| focus_handle.dispatch_action(action, cx)), + ) + }; + + h_flex() + .gap_1() + .children([apply_all_button].into_iter().flatten()) + .into_any_element() + } else { + gpui::Empty.into_any_element() } } } -impl EventEmitter for ProposedChangesEditorToolbar {} +impl EventEmitter for ProposedChangesToolbarControls {} -impl ToolbarItemView for ProposedChangesEditorToolbar { +impl ToolbarItemView for ProposedChangesToolbarControls { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn workspace::ItemHandle>, + _cx: &mut ViewContext, + ) -> workspace::ToolbarItemLocation { + self.current_editor = + active_pane_item.and_then(|item| item.downcast::()); + self.get_toolbar_item_location() + } +} + +impl ProposedChangesToolbar { + pub fn new() -> Self { + Self { + current_editor: None, + } + } + + fn get_toolbar_item_location(&self) -> ToolbarItemLocation { + if self.current_editor.is_some() { + ToolbarItemLocation::PrimaryLeft + } else { + ToolbarItemLocation::Hidden + } + } +} + +impl Render for ProposedChangesToolbar { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + if let Some(editor) = &self.current_editor { + let editor = editor.read(cx); + let all_changes_accepted = editor.all_changes_accepted(); + let icon = if all_changes_accepted { + Icon::new(IconName::Check).color(Color::Success) + } else { + Icon::new(IconName::ZedAssistant) + }; + + h_flex() + .gap_2p5() + .font(theme::ThemeSettings::get_global(cx).buffer_font.clone()) + .child(icon.size(IconSize::Small)) + .child( + Label::new(editor.title.clone()) + .color(Color::Muted) + .single_line() + .strikethrough(all_changes_accepted), + ) + .into_any_element() + } else { + gpui::Empty.into_any_element() + } + } +} + +impl EventEmitter for ProposedChangesToolbar {} + +impl ToolbarItemView for ProposedChangesToolbar { fn set_active_pane_item( &mut self, active_pane_item: Option<&dyn workspace::ItemHandle>, diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 4e3d05d2fb..f5c0259b1a 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -17,8 +17,8 @@ use breadcrumbs::Breadcrumbs; use client::{zed_urls, ZED_URL_SCHEME}; use collections::VecDeque; use command_palette_hooks::CommandPaletteFilter; -use editor::ProposedChangesEditorToolbar; use editor::{scroll::Autoscroll, Editor, MultiBuffer}; +use editor::{ProposedChangesToolbar, ProposedChangesToolbarControls}; use feature_flags::FeatureFlagAppExt; use futures::{channel::mpsc, select_biased, StreamExt}; use gpui::{ @@ -644,8 +644,10 @@ fn initialize_pane(workspace: &Workspace, pane: &View, cx: &mut ViewContex let buffer_search_bar = cx.new_view(search::BufferSearchBar::new); toolbar.add_item(buffer_search_bar.clone(), cx); - let proposed_change_bar = cx.new_view(|_| ProposedChangesEditorToolbar::new()); - toolbar.add_item(proposed_change_bar, cx); + let proposed_changes_bar = cx.new_view(|_| ProposedChangesToolbar::new()); + toolbar.add_item(proposed_changes_bar, cx); + let proposed_changes_controls = cx.new_view(|_| ProposedChangesToolbarControls::new()); + toolbar.add_item(proposed_changes_controls, cx); let quick_action_bar = cx.new_view(|cx| QuickActionBar::new(buffer_search_bar, workspace, cx)); toolbar.add_item(quick_action_bar, cx); From 6dbe2ef10c52d040a2d0419dbd43b371cd52491c Mon Sep 17 00:00:00 2001 From: yoleuh Date: Tue, 26 Nov 2024 11:10:28 -0500 Subject: [PATCH 158/886] docs: Fix default value for `relative_line_numbers` in vim (#21196) ![image](https://github.com/user-attachments/assets/91c00938-f056-4778-8999-6a805bc12247) Changes: `true` to `false` Reasoning: matches zed default settings as well as the settings changes portion of the vim docs ![image](https://github.com/user-attachments/assets/cb3240bc-8c55-4802-88c0-dd069992ca30) ![image](https://github.com/user-attachments/assets/747fbe8a-b24c-45f2-b3ab-f09bccdb4ec3) Release Notes: - N/A --- docs/src/vim.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/vim.md b/docs/src/vim.md index 8bfa6aa73f..254c5a0934 100644 --- a/docs/src/vim.md +++ b/docs/src/vim.md @@ -445,7 +445,7 @@ Here are a few general Zed settings that can help you fine-tune your Vim experie | Property | Description | Default Value | | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | | cursor_blink | If `true`, the cursor blinks. | `true` | -| relative_line_numbers | If `true`, line numbers in the left gutter are relative to the cursor. | `true` | +| relative_line_numbers | If `true`, line numbers in the left gutter are relative to the cursor. | `false` | | scrollbar | Object that controls the scrollbar display. Set to `{ "show": "never" }` to hide the scroll bar. | `{ "show": "always" }` | | scroll_beyond_last_line | If set to `"one_page"`, allows scrolling up to one page beyond the last line. Set to `"off"` to prevent this behavior. | `"one_page"` | | vertical_scroll_margin | The number of lines to keep above or below the cursor when scrolling. Set to `0` to allow the cursor to go up to the edges of the screen vertically. | `3` | From 64708527e7a994401076b367f67eebc5280c13a3 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 26 Nov 2024 10:19:13 -0800 Subject: [PATCH 159/886] Revert "Styling for Apply/Discard buttons (#21017)" This reverts commit 884748038e9c99b83b943d4550dd3cf515563071. --- assets/keymaps/default-linux.json | 2 +- assets/keymaps/default-macos.json | 2 +- crates/editor/src/actions.rs | 2 +- crates/editor/src/editor.rs | 64 +-- crates/editor/src/editor_tests.rs | 2 +- crates/editor/src/element.rs | 23 +- crates/editor/src/hunk_diff.rs | 479 +++++++++---------- crates/editor/src/proposed_changes_editor.rs | 118 +---- crates/zed/src/zed.rs | 8 +- 9 files changed, 291 insertions(+), 409 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 9ba416c210..2eedc1c839 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -522,7 +522,7 @@ { "context": "ProposedChangesEditor", "bindings": { - "ctrl-shift-y": "editor::ApplySelectedDiffHunks", + "ctrl-shift-y": "editor::ApplyDiffHunk", "ctrl-alt-a": "editor::ApplyAllDiffHunks" } }, diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index a4eae2af52..ddbbdd3faf 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -562,7 +562,7 @@ { "context": "ProposedChangesEditor", "bindings": { - "cmd-shift-y": "editor::ApplySelectedDiffHunks", + "cmd-shift-y": "editor::ApplyDiffHunk", "cmd-shift-a": "editor::ApplyAllDiffHunks" } }, diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 719a35a009..5b11b18bc2 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -209,7 +209,7 @@ gpui::actions!( AddSelectionAbove, AddSelectionBelow, ApplyAllDiffHunks, - ApplySelectedDiffHunks, + ApplyDiffHunk, Backspace, Cancel, CancelLanguageServerWork, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index eeaaeb5c2b..78f0aab5a5 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -99,8 +99,7 @@ use language::{ use language::{point_to_lsp, BufferRow, CharClassifier, Runnable, RunnableRange}; use linked_editing_ranges::refresh_linked_ranges; pub use proposed_changes_editor::{ - ProposedChangeLocation, ProposedChangesEditor, ProposedChangesToolbar, - ProposedChangesToolbarControls, + ProposedChangeLocation, ProposedChangesEditor, ProposedChangesEditorToolbar, }; use similar::{ChangeTag, TextDiff}; use std::iter::Peekable; @@ -161,7 +160,7 @@ use theme::{ }; use ui::{ h_flex, prelude::*, ButtonSize, ButtonStyle, Disclosure, IconButton, IconName, IconSize, - ListItem, Popover, Tooltip, + ListItem, Popover, PopoverMenuHandle, Tooltip, }; use util::{defer, maybe, post_inc, RangeExt, ResultExt, TryFutureExt}; use workspace::item::{ItemHandle, PreviewTabsSettings}; @@ -591,6 +590,7 @@ pub struct Editor { nav_history: Option, context_menu: RwLock>, mouse_context_menu: Option, + hunk_controls_menu_handle: PopoverMenuHandle, completion_tasks: Vec<(CompletionId, Task>)>, signature_help_state: SignatureHelpState, auto_signature_help: Option, @@ -2112,6 +2112,7 @@ impl Editor { nav_history: None, context_menu: RwLock::new(None), mouse_context_menu: None, + hunk_controls_menu_handle: PopoverMenuHandle::default(), completion_tasks: Default::default(), signature_help_state: SignatureHelpState::default(), auto_signature_help: None, @@ -13557,24 +13558,20 @@ fn test_wrap_with_prefix() { ); } -fn is_hunk_selected(hunk: &MultiBufferDiffHunk, selections: &[Selection]) -> bool { - let mut buffer_rows_for_selections = selections.iter().map(|selection| { - let start = MultiBufferRow(selection.start.row); - let end = MultiBufferRow(selection.end.row); - start..end - }); - - buffer_rows_for_selections.any(|range| does_selection_touch_hunk(&range, hunk)) -} - fn hunks_for_selections( multi_buffer_snapshot: &MultiBufferSnapshot, selections: &[Selection], ) -> Vec { let buffer_rows_for_selections = selections.iter().map(|selection| { - let start = MultiBufferRow(selection.start.to_point(multi_buffer_snapshot).row); - let end = MultiBufferRow(selection.end.to_point(multi_buffer_snapshot).row); - start..end + let head = selection.head(); + let tail = selection.tail(); + let start = MultiBufferRow(tail.to_point(multi_buffer_snapshot).row); + let end = MultiBufferRow(head.to_point(multi_buffer_snapshot).row); + if start > end { + end..start + } else { + start..end + } }); hunks_for_rows(buffer_rows_for_selections, multi_buffer_snapshot) @@ -13591,8 +13588,19 @@ pub fn hunks_for_rows( let query_rows = selected_multi_buffer_rows.start..selected_multi_buffer_rows.end.next_row(); for hunk in multi_buffer_snapshot.git_diff_hunks_in_range(query_rows.clone()) { - let related_to_selection = - does_selection_touch_hunk(&selected_multi_buffer_rows, &hunk); + // Deleted hunk is an empty row range, no caret can be placed there and Zed allows to revert it + // when the caret is just above or just below the deleted hunk. + let allow_adjacent = hunk_status(&hunk) == DiffHunkStatus::Removed; + let related_to_selection = if allow_adjacent { + hunk.row_range.overlaps(&query_rows) + || hunk.row_range.start == query_rows.end + || hunk.row_range.end == query_rows.start + } else { + // `selected_multi_buffer_rows` are inclusive (e.g. [2..2] means 2nd row is selected) + // `hunk.row_range` is exclusive (e.g. [2..3] means 2nd row is selected) + hunk.row_range.overlaps(&selected_multi_buffer_rows) + || selected_multi_buffer_rows.end == hunk.row_range.start + }; if related_to_selection { if !processed_buffer_rows .entry(hunk.buffer_id) @@ -13609,26 +13617,6 @@ pub fn hunks_for_rows( hunks } -fn does_selection_touch_hunk( - selected_multi_buffer_rows: &Range, - hunk: &MultiBufferDiffHunk, -) -> bool { - let query_rows = selected_multi_buffer_rows.start..selected_multi_buffer_rows.end.next_row(); - // Deleted hunk is an empty row range, no caret can be placed there and Zed allows to revert it - // when the caret is just above or just below the deleted hunk. - let allow_adjacent = hunk_status(hunk) == DiffHunkStatus::Removed; - if allow_adjacent { - hunk.row_range.overlaps(&query_rows) - || hunk.row_range.start == query_rows.end - || hunk.row_range.end == query_rows.start - } else { - // `selected_multi_buffer_rows` are inclusive (e.g. [2..2] means 2nd row is selected) - // `hunk.row_range` is exclusive (e.g. [2..3] means 2nd row is selected) - hunk.row_range.overlaps(selected_multi_buffer_rows) - || selected_multi_buffer_rows.end == hunk.row_range.start - } -} - pub trait CollaborationHub { fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap; fn user_participant_indices<'a>( diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 397d5e46d4..669134ef10 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -12552,7 +12552,7 @@ async fn test_edits_around_expanded_insertion_hunks( executor.run_until_parked(); cx.assert_diff_hunks( r#" - - use some::mod1; + use some::mod1; - use some::mod2; - - const A: u32 = 42; diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 19c1f3bf39..7f4bc3fb77 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -2509,7 +2509,6 @@ impl EditorElement { element, available_space: size(AvailableSpace::MinContent, element_size.height.into()), style: BlockStyle::Fixed, - is_zero_height: block.height() == 0, }); } for (row, block) in non_fixed_blocks { @@ -2556,7 +2555,6 @@ impl EditorElement { element, available_space: size(width.into(), element_size.height.into()), style, - is_zero_height: block.height() == 0, }); } @@ -2604,7 +2602,6 @@ impl EditorElement { element, available_space: size(width, element_size.height.into()), style, - is_zero_height: block.height() == 0, }); } } @@ -3950,23 +3947,8 @@ impl EditorElement { } fn paint_blocks(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) { - cx.paint_layer(layout.text_hitbox.bounds, |cx| { - layout.blocks.retain_mut(|block| { - if !block.is_zero_height { - block.element.paint(cx); - } - - block.is_zero_height - }); - }); - - // Paint all the zero-height blocks in a higher layer (if there were any remaining to paint). - if !layout.blocks.is_empty() { - cx.paint_layer(layout.text_hitbox.bounds, |cx| { - for mut block in layout.blocks.drain(..) { - block.element.paint(cx); - } - }); + for mut block in layout.blocks.drain(..) { + block.element.paint(cx); } } @@ -6029,7 +6011,6 @@ struct BlockLayout { element: AnyElement, available_space: Size, style: BlockStyle, - is_zero_height: bool, } fn layout_line( diff --git a/crates/editor/src/hunk_diff.rs b/crates/editor/src/hunk_diff.rs index 5c6d5ff7a3..27bb8ac557 100644 --- a/crates/editor/src/hunk_diff.rs +++ b/crates/editor/src/hunk_diff.rs @@ -1,8 +1,6 @@ use collections::{hash_map, HashMap, HashSet}; use git::diff::DiffHunkStatus; -use gpui::{ - AppContext, ClickEvent, CursorStyle, FocusableView, Hsla, Model, MouseButton, Task, View, -}; +use gpui::{Action, AnchorCorner, AppContext, CursorStyle, Hsla, Model, MouseButton, Task, View}; use language::{Buffer, BufferId, Point}; use multi_buffer::{ Anchor, AnchorRangeExt, ExcerptRange, MultiBuffer, MultiBufferDiffHunk, MultiBufferRow, @@ -11,18 +9,17 @@ use multi_buffer::{ use std::{ops::Range, sync::Arc}; use text::OffsetRangeExt; use ui::{ - prelude::*, ActiveTheme, IconButtonShape, InteractiveElement, IntoElement, KeyBinding, - ParentElement, Styled, TintColor, Tooltip, ViewContext, VisualContext, + prelude::*, ActiveTheme, ContextMenu, IconButtonShape, InteractiveElement, IntoElement, + ParentElement, PopoverMenu, Styled, Tooltip, ViewContext, VisualContext, }; use util::RangeExt; use workspace::Item; use crate::{ - editor_settings::CurrentLineHighlight, hunk_status, hunks_for_selections, is_hunk_selected, - ApplyAllDiffHunks, ApplySelectedDiffHunks, BlockPlacement, BlockProperties, BlockStyle, - CustomBlockId, DiffRowHighlight, DisplayRow, DisplaySnapshot, Editor, EditorElement, - ExpandAllHunkDiffs, GoToHunk, GoToPrevHunk, RevertSelectedHunks, ToDisplayPoint, - ToggleHunkDiff, + editor_settings::CurrentLineHighlight, hunk_status, hunks_for_selections, ApplyAllDiffHunks, + ApplyDiffHunk, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, DiffRowHighlight, + DisplayRow, DisplaySnapshot, Editor, EditorElement, ExpandAllHunkDiffs, GoToHunk, GoToPrevHunk, + RevertFile, RevertSelectedHunks, ToDisplayPoint, ToggleHunkDiff, }; #[derive(Debug, Clone)] @@ -60,6 +57,7 @@ pub enum DisplayDiffHunk { Folded { display_row: DisplayRow, }, + Unfolded { diff_base_byte_range: Range, display_row_range: Range, @@ -373,35 +371,26 @@ impl Editor { pub(crate) fn apply_selected_diff_hunks( &mut self, - _: &ApplySelectedDiffHunks, + _: &ApplyDiffHunk, cx: &mut ViewContext, ) { let snapshot = self.buffer.read(cx).snapshot(cx); let hunks = hunks_for_selections(&snapshot, &self.selections.disjoint_anchors()); - + let mut ranges_by_buffer = HashMap::default(); self.transact(cx, |editor, cx| { - if hunks.is_empty() { - // If there are no selected hunks, e.g. because we're using the keybinding with nothing selected, apply the first hunk. - if let Some(first_hunk) = editor.expanded_hunks.hunks.first() { - editor.apply_diff_hunks_in_range(first_hunk.hunk_range.clone(), cx); + for hunk in hunks { + if let Some(buffer) = editor.buffer.read(cx).buffer(hunk.buffer_id) { + ranges_by_buffer + .entry(buffer.clone()) + .or_insert_with(Vec::new) + .push(hunk.buffer_range.to_offset(buffer.read(cx))); } - } else { - let mut ranges_by_buffer = HashMap::default(); + } - for hunk in hunks { - if let Some(buffer) = editor.buffer.read(cx).buffer(hunk.buffer_id) { - ranges_by_buffer - .entry(buffer.clone()) - .or_insert_with(Vec::new) - .push(hunk.buffer_range.to_offset(buffer.read(cx))); - } - } - - for (buffer, ranges) in ranges_by_buffer { - buffer.update(cx, |buffer, cx| { - buffer.merge_into_base(ranges, cx); - }); - } + for (buffer, ranges) in ranges_by_buffer { + buffer.update(cx, |buffer, cx| { + buffer.merge_into_base(ranges, cx); + }); } }); @@ -423,238 +412,246 @@ impl Editor { buffer.read(cx).diff_base_buffer().is_some() }); + let border_color = cx.theme().colors().border_variant; + let bg_color = cx.theme().colors().editor_background; + let gutter_color = match hunk.status { + DiffHunkStatus::Added => cx.theme().status().created, + DiffHunkStatus::Modified => cx.theme().status().modified, + DiffHunkStatus::Removed => cx.theme().status().deleted, + }; + BlockProperties { placement: BlockPlacement::Above(hunk.multi_buffer_range.start), - height: 0, + height: 1, style: BlockStyle::Sticky, - priority: 1, + priority: 0, render: Arc::new({ let editor = cx.view().clone(); let hunk = hunk.clone(); move |cx| { - let is_hunk_selected = editor.update(&mut **cx, |editor, cx| { - let snapshot = editor.buffer.read(cx).snapshot(cx); - let selections = &editor.selections.all::(cx); - - if editor.focus_handle(cx).is_focused(cx) && !selections.is_empty() { - if let Some(hunk) = to_diff_hunk(&hunk, &snapshot) { - is_hunk_selected(&hunk, selections) - } else { - false - } - } else { - // If we have no cursor, or aren't focused, then default to the first hunk - // because that's what the keyboard shortcuts do. - editor - .expanded_hunks - .hunks - .first() - .map(|first_hunk| first_hunk.hunk_range == hunk.multi_buffer_range) - .unwrap_or(false) - } - }); - - let focus_handle = editor.focus_handle(cx); - - let handle_discard_click = { - let editor = editor.clone(); - let hunk = hunk.clone(); - move |_event: &ClickEvent, cx: &mut WindowContext| { - let multi_buffer = editor.read(cx).buffer().clone(); - let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx); - let mut revert_changes = HashMap::default(); - if let Some(hunk) = - crate::hunk_diff::to_diff_hunk(&hunk, &multi_buffer_snapshot) - { - Editor::prepare_revert_change( - &mut revert_changes, - &multi_buffer, - &hunk, - cx, - ); - } - if !revert_changes.is_empty() { - editor.update(cx, |editor, cx| editor.revert(revert_changes, cx)); - } - } - }; - - let handle_apply_click = { - let editor = editor.clone(); - let hunk = hunk.clone(); - move |_event: &ClickEvent, cx: &mut WindowContext| { - editor.update(cx, |editor, cx| { - editor - .apply_diff_hunks_in_range(hunk.multi_buffer_range.clone(), cx); - }); - } - }; - - let discard_key_binding = - KeyBinding::for_action_in(&RevertSelectedHunks, &focus_handle, cx); - - let discard_tooltip = { - let focus_handle = editor.focus_handle(cx); - move |cx: &mut WindowContext| { - Tooltip::for_action_in( - "Discard Hunk", - &RevertSelectedHunks, - &focus_handle, - cx, - ) - } - }; + let hunk_controls_menu_handle = + editor.read(cx).hunk_controls_menu_handle.clone(); h_flex() .id(cx.block_id) - .pr_5() + .block_mouse_down() + .h(cx.line_height()) .w_full() - .justify_end() + .border_t_1() + .border_color(border_color) + .bg(bg_color) + .child( + div() + .id("gutter-strip") + .w(EditorElement::diff_hunk_strip_width(cx.line_height())) + .h_full() + .bg(gutter_color) + .cursor(CursorStyle::PointingHand) + .on_click({ + let editor = editor.clone(); + let hunk = hunk.clone(); + move |_event, cx| { + editor.update(cx, |editor, cx| { + editor.toggle_hovered_hunk(&hunk, cx); + }); + } + }), + ) .child( h_flex() - .h(cx.line_height()) - .gap_1() - .px_1() - .pb_1() - .border_x_1() - .border_b_1() - .border_color(cx.theme().colors().border_variant) - .rounded_b_lg() - .bg(cx.theme().colors().editor_background) - .shadow(smallvec::smallvec![gpui::BoxShadow { - color: gpui::hsla(0.0, 0.0, 0.0, 0.1), - blur_radius: px(1.0), - spread_radius: px(1.0), - offset: gpui::point(px(0.), px(1.0)), - }]) - .when(!is_branch_buffer, |row| { - row.child( - IconButton::new("next-hunk", IconName::ArrowDown) - .shape(IconButtonShape::Square) - .icon_size(IconSize::Small) - .tooltip({ - let focus_handle = editor.focus_handle(cx); - move |cx| { - Tooltip::for_action_in( - "Next Hunk", - &GoToHunk, - &focus_handle.clone(), - cx, - ) - } - }) - .on_click({ - let editor = editor.clone(); - let hunk = hunk.clone(); - move |_event, cx| { - editor.update(cx, |editor, cx| { - editor.go_to_subsequent_hunk( - hunk.multi_buffer_range.end, - cx, - ); - }); - } - }), - ) - .child( - IconButton::new("prev-hunk", IconName::ArrowUp) - .shape(IconButtonShape::Square) - .icon_size(IconSize::Small) - .tooltip({ - let focus_handle = editor.focus_handle(cx); - move |cx| { - Tooltip::for_action_in( - "Previous Hunk", - &GoToPrevHunk, - &focus_handle, - cx, - ) - } - }) - .on_click({ - let editor = editor.clone(); - let hunk = hunk.clone(); - move |_event, cx| { - editor.update(cx, |editor, cx| { - editor.go_to_preceding_hunk( - hunk.multi_buffer_range.start, - cx, - ); - }); - } - }), - ) - }) - .child(if is_branch_buffer { - if is_hunk_selected { - Button::new("discard", "Discard") - .style(ButtonStyle::Tinted(TintColor::Negative)) - .label_size(LabelSize::Small) - .key_binding(discard_key_binding) - .on_click(handle_discard_click.clone()) - .into_any_element() - } else { - IconButton::new("discard", IconName::Close) - .style(ButtonStyle::Tinted(TintColor::Negative)) - .shape(IconButtonShape::Square) - .icon_size(IconSize::XSmall) - .tooltip(discard_tooltip.clone()) - .on_click(handle_discard_click.clone()) - .into_any_element() - } - } else { - if is_hunk_selected { - Button::new("undo", "Undo") - .style(ButtonStyle::Tinted(TintColor::Negative)) - .label_size(LabelSize::Small) - .key_binding(discard_key_binding) - .on_click(handle_discard_click.clone()) - .into_any_element() - } else { - IconButton::new("undo", IconName::Undo) - .shape(IconButtonShape::Square) - .icon_size(IconSize::Small) - .tooltip(discard_tooltip.clone()) - .on_click(handle_discard_click.clone()) - .into_any_element() - } - }) - .when(is_branch_buffer, |this| { - this.child({ - let button = Button::new("apply", "Apply") - .style(ButtonStyle::Tinted(TintColor::Positive)) - .label_size(LabelSize::Small) - .key_binding(KeyBinding::for_action_in( - &ApplySelectedDiffHunks, - &focus_handle, - cx, - )) - .on_click(handle_apply_click.clone()) - .into_any_element(); - if is_hunk_selected { - button - } else { - IconButton::new("apply", IconName::Check) - .style(ButtonStyle::Tinted(TintColor::Positive)) + .px_6() + .size_full() + .justify_end() + .child( + h_flex() + .gap_1() + .when(!is_branch_buffer, |row| { + row.child( + IconButton::new("next-hunk", IconName::ArrowDown) + .shape(IconButtonShape::Square) + .icon_size(IconSize::Small) + .tooltip({ + let focus_handle = editor.focus_handle(cx); + move |cx| { + Tooltip::for_action_in( + "Next Hunk", + &GoToHunk, + &focus_handle, + cx, + ) + } + }) + .on_click({ + let editor = editor.clone(); + let hunk = hunk.clone(); + move |_event, cx| { + editor.update(cx, |editor, cx| { + editor.go_to_subsequent_hunk( + hunk.multi_buffer_range.end, + cx, + ); + }); + } + }), + ) + .child( + IconButton::new("prev-hunk", IconName::ArrowUp) + .shape(IconButtonShape::Square) + .icon_size(IconSize::Small) + .tooltip({ + let focus_handle = editor.focus_handle(cx); + move |cx| { + Tooltip::for_action_in( + "Previous Hunk", + &GoToPrevHunk, + &focus_handle, + cx, + ) + } + }) + .on_click({ + let editor = editor.clone(); + let hunk = hunk.clone(); + move |_event, cx| { + editor.update(cx, |editor, cx| { + editor.go_to_preceding_hunk( + hunk.multi_buffer_range.start, + cx, + ); + }); + } + }), + ) + }) + .child( + IconButton::new("discard", IconName::Undo) .shape(IconButtonShape::Square) - .icon_size(IconSize::XSmall) + .icon_size(IconSize::Small) .tooltip({ let focus_handle = editor.focus_handle(cx); move |cx| { Tooltip::for_action_in( - "Apply Hunk", - &ApplySelectedDiffHunks, + "Discard Hunk", + &RevertSelectedHunks, &focus_handle, cx, ) } }) - .on_click(handle_apply_click.clone()) - .into_any_element() - } - }) - }) + .on_click({ + let editor = editor.clone(); + let hunk = hunk.clone(); + move |_event, cx| { + let multi_buffer = + editor.read(cx).buffer().clone(); + let multi_buffer_snapshot = + multi_buffer.read(cx).snapshot(cx); + let mut revert_changes = HashMap::default(); + if let Some(hunk) = + crate::hunk_diff::to_diff_hunk( + &hunk, + &multi_buffer_snapshot, + ) + { + Editor::prepare_revert_change( + &mut revert_changes, + &multi_buffer, + &hunk, + cx, + ); + } + if !revert_changes.is_empty() { + editor.update(cx, |editor, cx| { + editor.revert(revert_changes, cx) + }); + } + } + }), + ) + .map(|this| { + if is_branch_buffer { + this.child( + IconButton::new("apply", IconName::Check) + .shape(IconButtonShape::Square) + .icon_size(IconSize::Small) + .tooltip({ + let focus_handle = + editor.focus_handle(cx); + move |cx| { + Tooltip::for_action_in( + "Apply Hunk", + &ApplyDiffHunk, + &focus_handle, + cx, + ) + } + }) + .on_click({ + let editor = editor.clone(); + let hunk = hunk.clone(); + move |_event, cx| { + editor.update(cx, |editor, cx| { + editor + .apply_diff_hunks_in_range( + hunk.multi_buffer_range + .clone(), + cx, + ); + }); + } + }), + ) + } else { + this.child({ + let focus = editor.focus_handle(cx); + PopoverMenu::new("hunk-controls-dropdown") + .trigger( + IconButton::new( + "toggle_editor_selections_icon", + IconName::EllipsisVertical, + ) + .shape(IconButtonShape::Square) + .icon_size(IconSize::Small) + .style(ButtonStyle::Subtle) + .selected( + hunk_controls_menu_handle + .is_deployed(), + ) + .when( + !hunk_controls_menu_handle + .is_deployed(), + |this| { + this.tooltip(|cx| { + Tooltip::text( + "Hunk Controls", + cx, + ) + }) + }, + ), + ) + .anchor(AnchorCorner::TopRight) + .with_handle(hunk_controls_menu_handle) + .menu(move |cx| { + let focus = focus.clone(); + let menu = ContextMenu::build( + cx, + move |menu, _| { + menu.context(focus.clone()) + .action( + "Discard All Hunks", + RevertFile + .boxed_clone(), + ) + }, + ); + Some(menu) + }) + }) + } + }), + ) .when(!is_branch_buffer, |div| { div.child( IconButton::new("collapse", IconName::Close) @@ -710,7 +707,7 @@ impl Editor { placement: BlockPlacement::Above(hunk.multi_buffer_range.start), height, style: BlockStyle::Flex, - priority: 1, + priority: 0, render: Arc::new(move |cx| { let width = EditorElement::diff_hunk_strip_width(cx.line_height()); let gutter_dimensions = editor.read(cx.context).gutter_dimensions; diff --git a/crates/editor/src/proposed_changes_editor.rs b/crates/editor/src/proposed_changes_editor.rs index 3a9509eb39..ac97fe18da 100644 --- a/crates/editor/src/proposed_changes_editor.rs +++ b/crates/editor/src/proposed_changes_editor.rs @@ -5,11 +5,10 @@ use gpui::{AppContext, EventEmitter, FocusableView, Model, Render, Subscription, use language::{Buffer, BufferEvent, Capability}; use multi_buffer::{ExcerptRange, MultiBuffer}; use project::Project; -use settings::Settings; use smol::stream::StreamExt; use std::{any::TypeId, ops::Range, rc::Rc, time::Duration}; use text::ToOffset; -use ui::{prelude::*, KeyBinding}; +use ui::{prelude::*, ButtonLike, KeyBinding}; use workspace::{ searchable::SearchableItemHandle, Item, ItemHandle as _, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, @@ -35,11 +34,7 @@ struct BufferEntry { _subscription: Subscription, } -pub struct ProposedChangesToolbarControls { - current_editor: Option>, -} - -pub struct ProposedChangesToolbar { +pub struct ProposedChangesEditorToolbar { current_editor: Option>, } @@ -233,10 +228,6 @@ impl ProposedChangesEditor { _ => (), } } - - fn all_changes_accepted(&self) -> bool { - false // In the future, we plan to compute this based on the current state of patches. - } } impl Render for ProposedChangesEditor { @@ -260,11 +251,7 @@ impl Item for ProposedChangesEditor { type Event = EditorEvent; fn tab_icon(&self, _cx: &ui::WindowContext) -> Option { - if self.all_changes_accepted() { - Some(Icon::new(IconName::Check).color(Color::Success)) - } else { - Some(Icon::new(IconName::ZedAssistant)) - } + Some(Icon::new(IconName::Diff)) } fn tab_content_text(&self, _cx: &WindowContext) -> Option { @@ -330,7 +317,7 @@ impl Item for ProposedChangesEditor { } } -impl ProposedChangesToolbarControls { +impl ProposedChangesEditorToolbar { pub fn new() -> Self { Self { current_editor: None, @@ -346,97 +333,28 @@ impl ProposedChangesToolbarControls { } } -impl Render for ProposedChangesToolbarControls { +impl Render for ProposedChangesEditorToolbar { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - if let Some(editor) = &self.current_editor { - let focus_handle = editor.focus_handle(cx); - let action = &ApplyAllDiffHunks; - let keybinding = KeyBinding::for_action_in(action, &focus_handle, cx); + let button_like = ButtonLike::new("apply-changes").child(Label::new("Apply All")); - let editor = editor.read(cx); + match &self.current_editor { + Some(editor) => { + let focus_handle = editor.focus_handle(cx); + let keybinding = KeyBinding::for_action_in(&ApplyAllDiffHunks, &focus_handle, cx) + .map(|binding| binding.into_any_element()); - let apply_all_button = if editor.all_changes_accepted() { - None - } else { - Some( - Button::new("apply-changes", "Apply All") - .style(ButtonStyle::Filled) - .key_binding(keybinding) - .on_click(move |_event, cx| focus_handle.dispatch_action(action, cx)), - ) - }; - - h_flex() - .gap_1() - .children([apply_all_button].into_iter().flatten()) - .into_any_element() - } else { - gpui::Empty.into_any_element() + button_like.children(keybinding).on_click({ + move |_event, cx| focus_handle.dispatch_action(&ApplyAllDiffHunks, cx) + }) + } + None => button_like.disabled(true), } } } -impl EventEmitter for ProposedChangesToolbarControls {} +impl EventEmitter for ProposedChangesEditorToolbar {} -impl ToolbarItemView for ProposedChangesToolbarControls { - fn set_active_pane_item( - &mut self, - active_pane_item: Option<&dyn workspace::ItemHandle>, - _cx: &mut ViewContext, - ) -> workspace::ToolbarItemLocation { - self.current_editor = - active_pane_item.and_then(|item| item.downcast::()); - self.get_toolbar_item_location() - } -} - -impl ProposedChangesToolbar { - pub fn new() -> Self { - Self { - current_editor: None, - } - } - - fn get_toolbar_item_location(&self) -> ToolbarItemLocation { - if self.current_editor.is_some() { - ToolbarItemLocation::PrimaryLeft - } else { - ToolbarItemLocation::Hidden - } - } -} - -impl Render for ProposedChangesToolbar { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - if let Some(editor) = &self.current_editor { - let editor = editor.read(cx); - let all_changes_accepted = editor.all_changes_accepted(); - let icon = if all_changes_accepted { - Icon::new(IconName::Check).color(Color::Success) - } else { - Icon::new(IconName::ZedAssistant) - }; - - h_flex() - .gap_2p5() - .font(theme::ThemeSettings::get_global(cx).buffer_font.clone()) - .child(icon.size(IconSize::Small)) - .child( - Label::new(editor.title.clone()) - .color(Color::Muted) - .single_line() - .strikethrough(all_changes_accepted), - ) - .into_any_element() - } else { - gpui::Empty.into_any_element() - } - } -} - -impl EventEmitter for ProposedChangesToolbar {} - -impl ToolbarItemView for ProposedChangesToolbar { +impl ToolbarItemView for ProposedChangesEditorToolbar { fn set_active_pane_item( &mut self, active_pane_item: Option<&dyn workspace::ItemHandle>, diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index f5c0259b1a..4e3d05d2fb 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -17,8 +17,8 @@ use breadcrumbs::Breadcrumbs; use client::{zed_urls, ZED_URL_SCHEME}; use collections::VecDeque; use command_palette_hooks::CommandPaletteFilter; +use editor::ProposedChangesEditorToolbar; use editor::{scroll::Autoscroll, Editor, MultiBuffer}; -use editor::{ProposedChangesToolbar, ProposedChangesToolbarControls}; use feature_flags::FeatureFlagAppExt; use futures::{channel::mpsc, select_biased, StreamExt}; use gpui::{ @@ -644,10 +644,8 @@ fn initialize_pane(workspace: &Workspace, pane: &View, cx: &mut ViewContex let buffer_search_bar = cx.new_view(search::BufferSearchBar::new); toolbar.add_item(buffer_search_bar.clone(), cx); - let proposed_changes_bar = cx.new_view(|_| ProposedChangesToolbar::new()); - toolbar.add_item(proposed_changes_bar, cx); - let proposed_changes_controls = cx.new_view(|_| ProposedChangesToolbarControls::new()); - toolbar.add_item(proposed_changes_controls, cx); + let proposed_change_bar = cx.new_view(|_| ProposedChangesEditorToolbar::new()); + toolbar.add_item(proposed_change_bar, cx); let quick_action_bar = cx.new_view(|cx| QuickActionBar::new(buffer_search_bar, workspace, cx)); toolbar.add_item(quick_action_bar, cx); From 597e5f8304ee3ffc74c7a312edf70108e93d59e2 Mon Sep 17 00:00:00 2001 From: vultix Date: Tue, 26 Nov 2024 13:54:36 -0700 Subject: [PATCH 160/886] vim: Add indent text object (#21121) Added support for the popular vim [indent-text-object](https://github.com/michaeljsmith/vim-indent-object). This is especially useful in indentation-sensitive languages like python. Release Notes: - vim: Added `vii`, `vai` and `vaI` for selecting [indent-text-object](https://github.com/michaeljsmith/vim-indent-object)s. --- assets/keymaps/vim.json | 4 +- crates/vim/src/object.rs | 169 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 168 insertions(+), 5 deletions(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 1be3e8c9c1..d0c7ae192b 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -381,7 +381,9 @@ "shift-b": "vim::CurlyBrackets", "<": "vim::AngleBrackets", ">": "vim::AngleBrackets", - "a": "vim::Argument" + "a": "vim::Argument", + "i": "vim::IndentObj", + "shift-i": ["vim::IndentObj", { "includeBelow": true }] } }, { diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index f97312e7f8..7ed97358ff 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -28,6 +28,7 @@ pub enum Object { CurlyBrackets, AngleBrackets, Argument, + IndentObj { include_below: bool }, Tag, } @@ -37,8 +38,14 @@ struct Word { #[serde(default)] ignore_punctuation: bool, } +#[derive(Clone, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +struct IndentObj { + #[serde(default)] + include_below: bool, +} -impl_actions!(vim, [Word]); +impl_actions!(vim, [Word, IndentObj]); actions!( vim, @@ -100,6 +107,13 @@ pub fn register(editor: &mut Editor, cx: &mut ViewContext) { Vim::action(editor, cx, |vim, _: &Argument, cx| { vim.object(Object::Argument, cx) }); + Vim::action( + editor, + cx, + |vim, &IndentObj { include_below }: &IndentObj, cx| { + vim.object(Object::IndentObj { include_below }, cx) + }, + ); } impl Vim { @@ -129,13 +143,18 @@ impl Object { | Object::AngleBrackets | Object::CurlyBrackets | Object::SquareBrackets - | Object::Argument => true, + | Object::Argument + | Object::IndentObj { .. } => true, } } pub fn always_expands_both_ways(self) -> bool { match self { - Object::Word { .. } | Object::Sentence | Object::Paragraph | Object::Argument => false, + Object::Word { .. } + | Object::Sentence + | Object::Paragraph + | Object::Argument + | Object::IndentObj { .. } => false, Object::Quotes | Object::BackQuotes | Object::DoubleQuotes @@ -167,7 +186,8 @@ impl Object { | Object::AngleBrackets | Object::VerticalBars | Object::Tag - | Object::Argument => Mode::Visual, + | Object::Argument + | Object::IndentObj { .. } => Mode::Visual, Object::Paragraph => Mode::VisualLine, } } @@ -219,6 +239,7 @@ impl Object { surrounding_markers(map, relative_to, around, self.is_multiline(), '<', '>') } Object::Argument => argument(map, relative_to, around), + Object::IndentObj { include_below } => indent(map, relative_to, around, include_below), } } @@ -569,6 +590,58 @@ fn argument( } } +fn indent( + map: &DisplaySnapshot, + relative_to: DisplayPoint, + around: bool, + include_below: bool, +) -> Option> { + let point = relative_to.to_point(map); + let row = point.row; + + let desired_indent = map.line_indent_for_buffer_row(MultiBufferRow(row)); + + // Loop backwards until we find a non-blank line with less indent + let mut start_row = row; + for prev_row in (0..row).rev() { + let indent = map.line_indent_for_buffer_row(MultiBufferRow(prev_row)); + if indent.is_line_empty() { + continue; + } + if indent.spaces < desired_indent.spaces || indent.tabs < desired_indent.tabs { + if around { + // When around is true, include the first line with less indent + start_row = prev_row; + } + break; + } + start_row = prev_row; + } + + // Loop forwards until we find a non-blank line with less indent + let mut end_row = row; + let max_rows = map.max_buffer_row().0; + for next_row in (row + 1)..=max_rows { + let indent = map.line_indent_for_buffer_row(MultiBufferRow(next_row)); + if indent.is_line_empty() { + continue; + } + if indent.spaces < desired_indent.spaces || indent.tabs < desired_indent.tabs { + if around && include_below { + // When around is true and including below, include this line + end_row = next_row; + } + break; + } + end_row = next_row; + } + + let end_len = map.buffer_snapshot.line_len(MultiBufferRow(end_row)); + let start = map.point_to_display_point(Point::new(start_row, 0), Bias::Right); + let end = map.point_to_display_point(Point::new(end_row, end_len), Bias::Left); + Some(start..end) +} + fn sentence( map: &DisplaySnapshot, relative_to: DisplayPoint, @@ -1458,6 +1531,94 @@ mod test { cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual); } + #[gpui::test] + async fn test_indent_object(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + // Base use case + cx.set_state( + indoc! {" + fn boop() { + // Comment + baz();ˇ + + loop { + bar(1); + bar(2); + } + + result + } + "}, + Mode::Normal, + ); + cx.simulate_keystrokes("v i i"); + cx.assert_state( + indoc! {" + fn boop() { + « // Comment + baz(); + + loop { + bar(1); + bar(2); + } + + resultˇ» + } + "}, + Mode::Visual, + ); + + // Around indent (include line above) + cx.set_state( + indoc! {" + const ABOVE: str = true; + fn boop() { + + hello(); + worˇld() + } + "}, + Mode::Normal, + ); + cx.simulate_keystrokes("v a i"); + cx.assert_state( + indoc! {" + const ABOVE: str = true; + «fn boop() { + + hello(); + world()ˇ» + } + "}, + Mode::Visual, + ); + + // Around indent (include line above & below) + cx.set_state( + indoc! {" + const ABOVE: str = true; + fn boop() { + hellˇo(); + world() + + } + const BELOW: str = true; + "}, + Mode::Normal, + ); + cx.simulate_keystrokes("c a shift-i"); + cx.assert_state( + indoc! {" + const ABOVE: str = true; + ˇ + const BELOW: str = true; + "}, + Mode::Insert, + ); + } + #[gpui::test] async fn test_delete_surrounding_character_objects(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; From 57e4540759734d5be46a075141b69ed75a3b946e Mon Sep 17 00:00:00 2001 From: Helge Mahrt <5497139+helgemahrt@users.noreply.github.com> Date: Tue, 26 Nov 2024 23:08:54 +0100 Subject: [PATCH 161/886] vim: Add "unmatched" motions `]}`, `])`, `[{` and `[(` (#21098) Closes #20791 Release Notes: - Added vim ["unmatched" motions](https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1238-L1255) `]}`, `])`, `[{` and `[(` --------- Co-authored-by: Conrad Irwin --- assets/keymaps/vim.json | 4 + crates/vim/src/motion.rs | 234 +++++++++++++++++- .../test_data/test_unmatched_backward.json | 24 ++ .../vim/test_data/test_unmatched_forward.json | 28 +++ 4 files changed, 289 insertions(+), 1 deletion(-) create mode 100644 crates/vim/test_data/test_unmatched_backward.json create mode 100644 crates/vim/test_data/test_unmatched_forward.json diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index d0c7ae192b..67db22b5e2 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -55,6 +55,10 @@ "n": "vim::MoveToNextMatch", "shift-n": "vim::MoveToPrevMatch", "%": "vim::Matching", + "] }": ["vim::UnmatchedForward", { "char": "}" } ], + "[ {": ["vim::UnmatchedBackward", { "char": "{" } ], + "] )": ["vim::UnmatchedForward", { "char": ")" } ], + "[ (": ["vim::UnmatchedBackward", { "char": "(" } ], "f": ["vim::PushOperator", { "FindForward": { "before": false } }], "t": ["vim::PushOperator", { "FindForward": { "before": true } }], "shift-f": ["vim::PushOperator", { "FindBackward": { "after": false } }], diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 9f7a30afe9..7c628626cb 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -72,6 +72,12 @@ pub enum Motion { StartOfDocument, EndOfDocument, Matching, + UnmatchedForward { + char: char, + }, + UnmatchedBackward { + char: char, + }, FindForward { before: bool, char: char, @@ -203,6 +209,20 @@ pub struct StartOfLine { pub(crate) display_lines: bool, } +#[derive(Clone, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +struct UnmatchedForward { + #[serde(default)] + char: char, +} + +#[derive(Clone, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +struct UnmatchedBackward { + #[serde(default)] + char: char, +} + impl_actions!( vim, [ @@ -219,6 +239,8 @@ impl_actions!( NextSubwordEnd, PreviousSubwordStart, PreviousSubwordEnd, + UnmatchedForward, + UnmatchedBackward ] ); @@ -326,7 +348,20 @@ pub fn register(editor: &mut Editor, cx: &mut ViewContext) { Vim::action(editor, cx, |vim, _: &Matching, cx| { vim.motion(Motion::Matching, cx) }); - + Vim::action( + editor, + cx, + |vim, &UnmatchedForward { char }: &UnmatchedForward, cx| { + vim.motion(Motion::UnmatchedForward { char }, cx) + }, + ); + Vim::action( + editor, + cx, + |vim, &UnmatchedBackward { char }: &UnmatchedBackward, cx| { + vim.motion(Motion::UnmatchedBackward { char }, cx) + }, + ); Vim::action( editor, cx, @@ -504,6 +539,8 @@ impl Motion { | Jump { line: true, .. } => true, EndOfLine { .. } | Matching + | UnmatchedForward { .. } + | UnmatchedBackward { .. } | FindForward { .. } | Left | Backspace @@ -537,6 +574,8 @@ impl Motion { | Up { .. } | EndOfLine { .. } | Matching + | UnmatchedForward { .. } + | UnmatchedBackward { .. } | FindForward { .. } | RepeatFind { .. } | Left @@ -583,6 +622,8 @@ impl Motion { | EndOfLine { .. } | EndOfLineDownward | Matching + | UnmatchedForward { .. } + | UnmatchedBackward { .. } | FindForward { .. } | WindowTop | WindowMiddle @@ -707,6 +748,14 @@ impl Motion { SelectionGoal::None, ), Matching => (matching(map, point), SelectionGoal::None), + UnmatchedForward { char } => ( + unmatched_forward(map, point, *char, times), + SelectionGoal::None, + ), + UnmatchedBackward { char } => ( + unmatched_backward(map, point, *char, times), + SelectionGoal::None, + ), // t f FindForward { before, @@ -1792,6 +1841,92 @@ fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint } } +fn unmatched_forward( + map: &DisplaySnapshot, + mut display_point: DisplayPoint, + char: char, + times: usize, +) -> DisplayPoint { + for _ in 0..times { + // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1245 + let point = display_point.to_point(map); + let offset = point.to_offset(&map.buffer_snapshot); + + let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point); + let Some(ranges) = ranges else { break }; + let mut closest_closing_destination = None; + let mut closest_distance = usize::MAX; + + for (_, close_range) in ranges { + if close_range.start > offset { + let mut chars = map.buffer_snapshot.chars_at(close_range.start); + if Some(char) == chars.next() { + let distance = close_range.start - offset; + if distance < closest_distance { + closest_closing_destination = Some(close_range.start); + closest_distance = distance; + continue; + } + } + } + } + + let new_point = closest_closing_destination + .map(|destination| destination.to_display_point(map)) + .unwrap_or(display_point); + if new_point == display_point { + break; + } + display_point = new_point; + } + return display_point; +} + +fn unmatched_backward( + map: &DisplaySnapshot, + mut display_point: DisplayPoint, + char: char, + times: usize, +) -> DisplayPoint { + for _ in 0..times { + // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1239 + let point = display_point.to_point(map); + let offset = point.to_offset(&map.buffer_snapshot); + + let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point); + let Some(ranges) = ranges else { + break; + }; + + let mut closest_starting_destination = None; + let mut closest_distance = usize::MAX; + + for (start_range, _) in ranges { + if start_range.start < offset { + let mut chars = map.buffer_snapshot.chars_at(start_range.start); + if Some(char) == chars.next() { + let distance = offset - start_range.start; + if distance < closest_distance { + closest_starting_destination = Some(start_range.start); + closest_distance = distance; + continue; + } + } + } + } + + let new_point = closest_starting_destination + .map(|destination| destination.to_display_point(map)) + .unwrap_or(display_point); + if new_point == display_point { + break; + } else { + display_point = new_point; + } + } + display_point +} + fn find_forward( map: &DisplaySnapshot, from: DisplayPoint, @@ -2118,6 +2253,103 @@ mod test { cx.shared_state().await.assert_eq("func boop(ˇ) {\n}"); } + #[gpui::test] + async fn test_unmatched_forward(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + // test it works with curly braces + cx.set_shared_state(indoc! {r"func (a string) { + do(something(with.anˇd_arrays[0, 2])) + }"}) + .await; + cx.simulate_shared_keystrokes("] }").await; + cx.shared_state() + .await + .assert_eq(indoc! {r"func (a string) { + do(something(with.and_arrays[0, 2])) + ˇ}"}); + + // test it works with brackets + cx.set_shared_state(indoc! {r"func (a string) { + do(somethiˇng(with.and_arrays[0, 2])) + }"}) + .await; + cx.simulate_shared_keystrokes("] )").await; + cx.shared_state() + .await + .assert_eq(indoc! {r"func (a string) { + do(something(with.and_arrays[0, 2])ˇ) + }"}); + + cx.set_shared_state(indoc! {r"func (a string) { a((b, cˇ))}"}) + .await; + cx.simulate_shared_keystrokes("] )").await; + cx.shared_state() + .await + .assert_eq(indoc! {r"func (a string) { a((b, c)ˇ)}"}); + + // test it works on immediate nesting + cx.set_shared_state("{ˇ {}{}}").await; + cx.simulate_shared_keystrokes("] }").await; + cx.shared_state().await.assert_eq("{ {}{}ˇ}"); + cx.set_shared_state("(ˇ ()())").await; + cx.simulate_shared_keystrokes("] )").await; + cx.shared_state().await.assert_eq("( ()()ˇ)"); + + // test it works on immediate nesting inside braces + cx.set_shared_state("{\n ˇ {()}\n}").await; + cx.simulate_shared_keystrokes("] }").await; + cx.shared_state().await.assert_eq("{\n {()}\nˇ}"); + cx.set_shared_state("(\n ˇ {()}\n)").await; + cx.simulate_shared_keystrokes("] )").await; + cx.shared_state().await.assert_eq("(\n {()}\nˇ)"); + } + + #[gpui::test] + async fn test_unmatched_backward(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + // test it works with curly braces + cx.set_shared_state(indoc! {r"func (a string) { + do(something(with.anˇd_arrays[0, 2])) + }"}) + .await; + cx.simulate_shared_keystrokes("[ {").await; + cx.shared_state() + .await + .assert_eq(indoc! {r"func (a string) ˇ{ + do(something(with.and_arrays[0, 2])) + }"}); + + // test it works with brackets + cx.set_shared_state(indoc! {r"func (a string) { + do(somethiˇng(with.and_arrays[0, 2])) + }"}) + .await; + cx.simulate_shared_keystrokes("[ (").await; + cx.shared_state() + .await + .assert_eq(indoc! {r"func (a string) { + doˇ(something(with.and_arrays[0, 2])) + }"}); + + // test it works on immediate nesting + cx.set_shared_state("{{}{} ˇ }").await; + cx.simulate_shared_keystrokes("[ {").await; + cx.shared_state().await.assert_eq("ˇ{{}{} }"); + cx.set_shared_state("(()() ˇ )").await; + cx.simulate_shared_keystrokes("[ (").await; + cx.shared_state().await.assert_eq("ˇ(()() )"); + + // test it works on immediate nesting inside braces + cx.set_shared_state("{\n {()} ˇ\n}").await; + cx.simulate_shared_keystrokes("[ {").await; + cx.shared_state().await.assert_eq("ˇ{\n {()} \n}"); + cx.set_shared_state("(\n {()} ˇ\n)").await; + cx.simulate_shared_keystrokes("[ (").await; + cx.shared_state().await.assert_eq("ˇ(\n {()} \n)"); + } + #[gpui::test] async fn test_matching_tags(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new_html(cx).await; diff --git a/crates/vim/test_data/test_unmatched_backward.json b/crates/vim/test_data/test_unmatched_backward.json new file mode 100644 index 0000000000..bb3825dcd2 --- /dev/null +++ b/crates/vim/test_data/test_unmatched_backward.json @@ -0,0 +1,24 @@ +{"Put":{"state":"func (a string) {\n do(something(with.anˇd_arrays[0, 2]))\n}"}} +{"Key":"["} +{"Key":"{"} +{"Get":{"state":"func (a string) ˇ{\n do(something(with.and_arrays[0, 2]))\n}","mode":"Normal"}} +{"Put":{"state":"func (a string) {\n do(somethiˇng(with.and_arrays[0, 2]))\n}"}} +{"Key":"["} +{"Key":"("} +{"Get":{"state":"func (a string) {\n doˇ(something(with.and_arrays[0, 2]))\n}","mode":"Normal"}} +{"Put":{"state":"{{}{} ˇ }"}} +{"Key":"["} +{"Key":"{"} +{"Get":{"state":"ˇ{{}{} }","mode":"Normal"}} +{"Put":{"state":"(()() ˇ )"}} +{"Key":"["} +{"Key":"("} +{"Get":{"state":"ˇ(()() )","mode":"Normal"}} +{"Put":{"state":"{\n {()} ˇ\n}"}} +{"Key":"["} +{"Key":"{"} +{"Get":{"state":"ˇ{\n {()} \n}","mode":"Normal"}} +{"Put":{"state":"(\n {()} ˇ\n)"}} +{"Key":"["} +{"Key":"("} +{"Get":{"state":"ˇ(\n {()} \n)","mode":"Normal"}} diff --git a/crates/vim/test_data/test_unmatched_forward.json b/crates/vim/test_data/test_unmatched_forward.json new file mode 100644 index 0000000000..a6b4a38f29 --- /dev/null +++ b/crates/vim/test_data/test_unmatched_forward.json @@ -0,0 +1,28 @@ +{"Put":{"state":"func (a string) {\n do(something(with.anˇd_arrays[0, 2]))\n}"}} +{"Key":"]"} +{"Key":"}"} +{"Get":{"state":"func (a string) {\n do(something(with.and_arrays[0, 2]))\nˇ}","mode":"Normal"}} +{"Put":{"state":"func (a string) {\n do(somethiˇng(with.and_arrays[0, 2]))\n}"}} +{"Key":"]"} +{"Key":")"} +{"Get":{"state":"func (a string) {\n do(something(with.and_arrays[0, 2])ˇ)\n}","mode":"Normal"}} +{"Put":{"state":"func (a string) { a((b, cˇ))}"}} +{"Key":"]"} +{"Key":")"} +{"Get":{"state":"func (a string) { a((b, c)ˇ)}","mode":"Normal"}} +{"Put":{"state":"{ˇ {}{}}"}} +{"Key":"]"} +{"Key":"}"} +{"Get":{"state":"{ {}{}ˇ}","mode":"Normal"}} +{"Put":{"state":"(ˇ ()())"}} +{"Key":"]"} +{"Key":")"} +{"Get":{"state":"( ()()ˇ)","mode":"Normal"}} +{"Put":{"state":"{\n ˇ {()}\n}"}} +{"Key":"]"} +{"Key":"}"} +{"Get":{"state":"{\n {()}\nˇ}","mode":"Normal"}} +{"Put":{"state":"(\n ˇ {()}\n)"}} +{"Key":"]"} +{"Key":")"} +{"Get":{"state":"(\n {()}\nˇ)","mode":"Normal"}} From d75d34576a5ed80142666dddc68cbbc2652aeb61 Mon Sep 17 00:00:00 2001 From: tims <0xtimsb@gmail.com> Date: Wed, 27 Nov 2024 04:53:01 +0530 Subject: [PATCH 162/886] Fix file missing or duplicated when copying multiple items in project panel + Fix marked files not being deselected after selecting a directory (#20859) Closes #20858 This fix depends on the sanitization logic implemented in PR #20577. Since that branch may undergo further changes, this branch will be periodically rebased on it. Once #20577 is merged, the dependency will no longer apply. Release Notes: - Fix missing or duplicated files when copying multiple items in the project panel. - Fix marked files not being deselected after selecting a directory on primary click. - Fix "copy path" and "copy path relative" with multiple items selected in project panel. **Problem**: In this case, `dir1` is selected while `dir2`, `dir3`, and `dir1/file` are marked. Using the `marked_entries` function results in only `dir1`, which is incorrect. Currently, the `marked_entries` function is used in five actions, which all produce incorrect results: 1. Delete (via the disjoint function) 2. Copy 3. Cut 4. Copy Path 5. Copy Path Relative **Solution**: 1. `marked_entries` function should not use "When currently selected entry is not marked, it's treated as the only marked entry." logic. There is no grand scheme behind this logic as confirmed by piotr [here](https://github.com/zed-industries/zed/issues/17746#issuecomment-2464765963). 2. `copy` and `cut` actions should use the disjoint function to prevent obivous failures. 3. `copy path` and `copy path relative` should keep using *fixed* `marked_entries` as that is expected behavior for these actions. --- 1. Before/After: Partial Copy Select `dir1` and `c.txt` (in that order, reverse order works!), and copy it and paste in `dir2`. `c.txt` is not copied in `dir2`. --- 2. Before/After: Duplicate Copy Select `a.txt`, `dir1` and `c.txt` (in that order), and copy it and paste in `dir2`. `a.txt` is duplicated in `dir2`. --- 3. Before/After: Directory Selection Simply primary click on any file, now primary click on any dir. That previous file is still marked. --- 4. Before/After: Copy Path and Copy Path Relative Upon `copy path` (ctrl + alt + c): Before: Only `/home/tims/test/dir2/a.txt` was copied. After: All three paths `/home/tims/test/dir2`, `/home/tims/test/c.txt` and `/home/tims/test/dir2/a.txt` are copied. This is also how VSCode also copies path when multiple are selected. --- crates/project_panel/src/project_panel.rs | 203 +++++++++++++++++++--- 1 file changed, 181 insertions(+), 22 deletions(-) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index c757924727..9803742966 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1185,7 +1185,7 @@ impl ProjectPanel { fn remove(&mut self, trash: bool, skip_prompt: bool, cx: &mut ViewContext<'_, ProjectPanel>) { maybe!({ - let items_to_delete = self.disjoint_entries_for_removal(cx); + let items_to_delete = self.disjoint_entries(cx); if items_to_delete.is_empty() { return None; } @@ -1546,7 +1546,7 @@ impl ProjectPanel { } fn cut(&mut self, _: &Cut, cx: &mut ViewContext) { - let entries = self.marked_entries(); + let entries = self.disjoint_entries(cx); if !entries.is_empty() { self.clipboard = Some(ClipboardEntry::Cut(entries)); cx.notify(); @@ -1554,7 +1554,7 @@ impl ProjectPanel { } fn copy(&mut self, _: &Copy, cx: &mut ViewContext) { - let entries = self.marked_entries(); + let entries = self.disjoint_entries(cx); if !entries.is_empty() { self.clipboard = Some(ClipboardEntry::Copied(entries)); cx.notify(); @@ -1928,7 +1928,7 @@ impl ProjectPanel { None } - fn disjoint_entries_for_removal(&self, cx: &AppContext) -> BTreeSet { + fn disjoint_entries(&self, cx: &AppContext) -> BTreeSet { let marked_entries = self.marked_entries(); let mut sanitized_entries = BTreeSet::new(); if marked_entries.is_empty() { @@ -1976,25 +1976,25 @@ impl ProjectPanel { 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. + // Returns the union of the currently selected entry and all marked entries. fn marked_entries(&self) -> BTreeSet { - let Some(mut selection) = self.selection else { - return Default::default(); - }; - if self.marked_entries.contains(&selection) { - self.marked_entries - .iter() - .copied() - .map(|mut entry| { - entry.entry_id = self.resolve_entry(entry.entry_id); - entry - }) - .collect() - } else { - selection.entry_id = self.resolve_entry(selection.entry_id); - BTreeSet::from_iter([selection]) + let mut entries = self + .marked_entries + .iter() + .map(|entry| SelectedEntry { + entry_id: self.resolve_entry(entry.entry_id), + worktree_id: entry.worktree_id, + }) + .collect::>(); + + if let Some(selection) = self.selection { + entries.insert(SelectedEntry { + entry_id: self.resolve_entry(selection.entry_id), + worktree_id: selection.worktree_id, + }); } + + entries } /// Finds the currently selected subentry for a given leaf entry id. If a given entry @@ -2915,6 +2915,7 @@ impl ProjectPanel { this.marked_entries.remove(&selection); } } else if kind.is_dir() { + this.marked_entries.clear(); this.toggle_expanded(entry_id, cx); } else { let preview_tabs_enabled = PreviewTabsSettings::get_global(cx).enabled; @@ -3051,7 +3052,8 @@ impl ProjectPanel { .single_line() .color(filename_text_color) .when( - is_active && index == active_index, + index == active_index + && (is_active || is_marked), |this| this.underline(true), ), ); @@ -5177,6 +5179,163 @@ mod tests { ); } + #[gpui::test] + async fn test_copy_paste_directory_with_sibling_file(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/test", + json!({ + "dir1": { + "a.txt": "", + "b.txt": "", + }, + "dir2": {}, + "c.txt": "", + "d.txt": "", + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/test".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, "test/dir1", cx); + + cx.simulate_modifiers_change(gpui::Modifiers { + control: true, + ..Default::default() + }); + + select_path_with_mark(&panel, "test/dir1", cx); + select_path_with_mark(&panel, "test/c.txt", cx); + + assert_eq!( + visible_entries_as_strings(&panel, 0..15, cx), + &[ + "v test", + " v dir1 <== marked", + " a.txt", + " b.txt", + " > dir2", + " c.txt <== selected <== marked", + " d.txt", + ], + "Initial state before copying dir1 and c.txt" + ); + + panel.update(cx, |panel, cx| { + panel.copy(&Default::default(), cx); + }); + select_path(&panel, "test/dir2", cx); + panel.update(cx, |panel, cx| { + panel.paste(&Default::default(), cx); + }); + cx.executor().run_until_parked(); + + toggle_expand_dir(&panel, "test/dir2/dir1", cx); + + assert_eq!( + visible_entries_as_strings(&panel, 0..15, cx), + &[ + "v test", + " v dir1 <== marked", + " a.txt", + " b.txt", + " v dir2", + " v dir1 <== selected", + " a.txt", + " b.txt", + " c.txt", + " c.txt <== marked", + " d.txt", + ], + "Should copy dir1 as well as c.txt into dir2" + ); + } + + #[gpui::test] + async fn test_copy_paste_nested_and_root_entries(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/test", + json!({ + "dir1": { + "a.txt": "", + "b.txt": "", + }, + "dir2": {}, + "c.txt": "", + "d.txt": "", + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/test".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, "test/dir1", cx); + + cx.simulate_modifiers_change(gpui::Modifiers { + control: true, + ..Default::default() + }); + + select_path_with_mark(&panel, "test/dir1/a.txt", cx); + select_path_with_mark(&panel, "test/dir1", cx); + select_path_with_mark(&panel, "test/c.txt", cx); + + assert_eq!( + visible_entries_as_strings(&panel, 0..15, cx), + &[ + "v test", + " v dir1 <== marked", + " a.txt <== marked", + " b.txt", + " > dir2", + " c.txt <== selected <== marked", + " d.txt", + ], + "Initial state before copying a.txt, dir1 and c.txt" + ); + + panel.update(cx, |panel, cx| { + panel.copy(&Default::default(), cx); + }); + select_path(&panel, "test/dir2", cx); + panel.update(cx, |panel, cx| { + panel.paste(&Default::default(), cx); + }); + cx.executor().run_until_parked(); + + toggle_expand_dir(&panel, "test/dir2/dir1", cx); + + assert_eq!( + visible_entries_as_strings(&panel, 0..20, cx), + &[ + "v test", + " v dir1 <== marked", + " a.txt <== marked", + " b.txt", + " v dir2", + " v dir1 <== selected", + " a.txt", + " b.txt", + " c.txt", + " c.txt <== marked", + " d.txt", + ], + "Should copy dir1 and c.txt into dir2. a.txt is already present in copied dir1." + ); + } + #[gpui::test] async fn test_remove_opened_file(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); From f702575255f54d7abe7b41a73ad0ac9d06a9c3bd Mon Sep 17 00:00:00 2001 From: AidanV <84053180+AidanV@users.noreply.github.com> Date: Tue, 26 Nov 2024 16:24:29 -0800 Subject: [PATCH 163/886] Add support for resizing panes using vim motions (#21038) Closes #8628 Release Notes: - Added support for resizing the current pane using vim keybinds with the intention to follow the functionality of vim - "ctrl-w +" to make a pane taller - "ctrl-w -" to make the pane shorter - "ctrl-w >" to make a pane wider - "ctrl-w <" to make the pane narrower - Changed vim pre_count and post_count to globals to allow for other crates to use the vim count. In this case, it allows for resizing by more than one unit. For example, "10 ctrl-w -" will decrease the height of the pane 10 times more than "ctrl-w -" - This pr does **not** add keybinds for making all panes in an axis equal size and does **not** add support for resizing docks. This is mentioned because these could be implied by the original issue --------- Co-authored-by: Conrad Irwin --- Cargo.lock | 1 + assets/keymaps/vim.json | 4 + crates/vim/Cargo.toml | 1 + crates/vim/src/change_list.rs | 2 +- crates/vim/src/command.rs | 2 +- crates/vim/src/indent.rs | 4 +- crates/vim/src/insert.rs | 2 +- crates/vim/src/mode_indicator.rs | 14 ++- crates/vim/src/motion.rs | 4 +- crates/vim/src/normal.rs | 18 ++-- crates/vim/src/normal/case.rs | 2 +- crates/vim/src/normal/increment.rs | 4 +- crates/vim/src/normal/paste.rs | 2 +- crates/vim/src/normal/repeat.rs | 4 +- crates/vim/src/normal/scroll.rs | 2 +- crates/vim/src/normal/search.rs | 6 +- crates/vim/src/normal/substitute.rs | 4 +- crates/vim/src/replace.rs | 2 +- crates/vim/src/rewrap.rs | 2 +- crates/vim/src/state.rs | 5 + crates/vim/src/surrounds.rs | 2 +- crates/vim/src/vim.rs | 77 +++++++++++----- crates/vim/src/visual.rs | 12 +-- crates/workspace/src/pane_group.rs | 137 +++++++++++++++++++++++++++- crates/workspace/src/workspace.rs | 6 ++ 25 files changed, 251 insertions(+), 68 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 166adb6588..41532b9773 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13832,6 +13832,7 @@ dependencies = [ "serde_derive", "serde_json", "settings", + "theme", "tokio", "ui", "util", diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 67db22b5e2..858a1b8d31 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -557,6 +557,10 @@ "ctrl-w shift-l": ["workspace::SwapPaneInDirection", "Right"], "ctrl-w shift-k": ["workspace::SwapPaneInDirection", "Up"], "ctrl-w shift-j": ["workspace::SwapPaneInDirection", "Down"], + "ctrl-w >": ["vim::ResizePane", "Widen"], + "ctrl-w <": ["vim::ResizePane", "Narrow"], + "ctrl-w -": ["vim::ResizePane", "Shorten"], + "ctrl-w +": ["vim::ResizePane", "Lengthen"], "ctrl-w g t": "pane::ActivateNextItem", "ctrl-w ctrl-g t": "pane::ActivateNextItem", "ctrl-w g shift-t": "pane::ActivatePrevItem", diff --git a/crates/vim/Cargo.toml b/crates/vim/Cargo.toml index ddf738d067..02d4136faa 100644 --- a/crates/vim/Cargo.toml +++ b/crates/vim/Cargo.toml @@ -36,6 +36,7 @@ serde.workspace = true serde_derive.workspace = true serde_json.workspace = true settings.workspace = true +theme.workspace = true tokio = { version = "1.15", features = ["full"], optional = true } ui.workspace = true util.workspace = true diff --git a/crates/vim/src/change_list.rs b/crates/vim/src/change_list.rs index 69fcdd8319..adf553983b 100644 --- a/crates/vim/src/change_list.rs +++ b/crates/vim/src/change_list.rs @@ -16,7 +16,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext) { impl Vim { fn move_to_change(&mut self, direction: Direction, cx: &mut ViewContext) { - let count = self.take_count(cx).unwrap_or(1); + let count = Vim::take_count(cx).unwrap_or(1); if self.change_list.is_empty() { return; } diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index 2fa75c8579..5a958da012 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -101,7 +101,7 @@ pub fn register(editor: &mut Editor, cx: &mut ViewContext) { let Some(workspace) = vim.workspace(cx) else { return; }; - let count = vim.take_count(cx).unwrap_or(1); + let count = Vim::take_count(cx).unwrap_or(1); workspace.update(cx, |workspace, cx| { command_palette::CommandPalette::toggle( workspace, diff --git a/crates/vim/src/indent.rs b/crates/vim/src/indent.rs index b6ca2de34c..8e4f27271b 100644 --- a/crates/vim/src/indent.rs +++ b/crates/vim/src/indent.rs @@ -16,7 +16,7 @@ actions!(vim, [Indent, Outdent,]); pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext) { Vim::action(editor, cx, |vim, _: &Indent, cx| { vim.record_current_action(cx); - let count = vim.take_count(cx).unwrap_or(1); + let count = Vim::take_count(cx).unwrap_or(1); vim.store_visual_marks(cx); vim.update_editor(cx, |vim, editor, cx| { editor.transact(cx, |editor, cx| { @@ -34,7 +34,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext) { Vim::action(editor, cx, |vim, _: &Outdent, cx| { vim.record_current_action(cx); - let count = vim.take_count(cx).unwrap_or(1); + let count = Vim::take_count(cx).unwrap_or(1); vim.store_visual_marks(cx); vim.update_editor(cx, |vim, editor, cx| { editor.transact(cx, |editor, cx| { diff --git a/crates/vim/src/insert.rs b/crates/vim/src/insert.rs index ba83e2125b..b1e7af9b10 100644 --- a/crates/vim/src/insert.rs +++ b/crates/vim/src/insert.rs @@ -17,7 +17,7 @@ impl Vim { self.sync_vim_settings(cx); return; } - let count = self.take_count(cx).unwrap_or(1); + let count = Vim::take_count(cx).unwrap_or(1); self.stop_recording_immediately(action.boxed_clone(), cx); if count <= 1 || Vim::globals(cx).dot_replaying { self.create_mark("^".into(), false, cx); diff --git a/crates/vim/src/mode_indicator.rs b/crates/vim/src/mode_indicator.rs index 619bb6e1f4..8b608fdfe3 100644 --- a/crates/vim/src/mode_indicator.rs +++ b/crates/vim/src/mode_indicator.rs @@ -2,7 +2,7 @@ use gpui::{div, Element, Render, Subscription, View, ViewContext, WeakView}; use itertools::Itertools; use workspace::{item::ItemHandle, ui::prelude::*, StatusItemView}; -use crate::{Vim, VimEvent}; +use crate::{Vim, VimEvent, VimGlobals}; /// The ModeIndicator displays the current mode in the status bar. pub struct ModeIndicator { @@ -68,14 +68,22 @@ impl ModeIndicator { let vim = vim.read(cx); recording - .chain(vim.pre_count.map(|count| format!("{}", count))) + .chain( + cx.global::() + .pre_count + .map(|count| format!("{}", count)), + ) .chain(vim.selected_register.map(|reg| format!("\"{reg}"))) .chain( vim.operator_stack .iter() .map(|item| item.status().to_string()), ) - .chain(vim.post_count.map(|count| format!("{}", count))) + .chain( + cx.global::() + .post_count + .map(|count| format!("{}", count)), + ) .collect::>() .join("") } diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 7c628626cb..9c770fb63f 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -490,7 +490,7 @@ impl Vim { self.pop_operator(cx); } - let count = self.take_count(cx); + let count = Vim::take_count(cx); let active_operator = self.active_operator(); let mut waiting_operator: Option = None; match self.mode { @@ -510,7 +510,7 @@ impl Vim { self.clear_operator(cx); if let Some(operator) = waiting_operator { self.push_operator(operator, cx); - self.pre_count = count + Vim::globals(cx).pre_count = count } } } diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 37a8115e33..24e8e7bed4 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -77,17 +77,17 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext) { Vim::action(editor, cx, |vim, _: &DeleteLeft, cx| { vim.record_current_action(cx); - let times = vim.take_count(cx); + let times = Vim::take_count(cx); vim.delete_motion(Motion::Left, times, cx); }); Vim::action(editor, cx, |vim, _: &DeleteRight, cx| { vim.record_current_action(cx); - let times = vim.take_count(cx); + let times = Vim::take_count(cx); vim.delete_motion(Motion::Right, times, cx); }); Vim::action(editor, cx, |vim, _: &ChangeToEndOfLine, cx| { vim.start_recording(cx); - let times = vim.take_count(cx); + let times = Vim::take_count(cx); vim.change_motion( Motion::EndOfLine { display_lines: false, @@ -98,7 +98,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext) { }); Vim::action(editor, cx, |vim, _: &DeleteToEndOfLine, cx| { vim.record_current_action(cx); - let times = vim.take_count(cx); + let times = Vim::take_count(cx); vim.delete_motion( Motion::EndOfLine { display_lines: false, @@ -109,7 +109,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext) { }); Vim::action(editor, cx, |vim, _: &JoinLines, cx| { vim.record_current_action(cx); - let mut times = vim.take_count(cx).unwrap_or(1); + let mut times = Vim::take_count(cx).unwrap_or(1); if vim.mode.is_visual() { times = 1; } else if times > 1 { @@ -130,7 +130,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext) { }); Vim::action(editor, cx, |vim, _: &Undo, cx| { - let times = vim.take_count(cx); + let times = Vim::take_count(cx); vim.update_editor(cx, |_, editor, cx| { for _ in 0..times.unwrap_or(1) { editor.undo(&editor::actions::Undo, cx); @@ -138,7 +138,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext) { }); }); Vim::action(editor, cx, |vim, _: &Redo, cx| { - let times = vim.take_count(cx); + let times = Vim::take_count(cx); vim.update_editor(cx, |_, editor, cx| { for _ in 0..times.unwrap_or(1) { editor.redo(&editor::actions::Redo, cx); @@ -396,7 +396,7 @@ impl Vim { } fn yank_line(&mut self, _: &YankLine, cx: &mut ViewContext) { - let count = self.take_count(cx); + let count = Vim::take_count(cx); self.yank_motion(motion::Motion::CurrentLine, count, cx) } @@ -416,7 +416,7 @@ impl Vim { } pub(crate) fn normal_replace(&mut self, text: Arc, cx: &mut ViewContext) { - let count = self.take_count(cx).unwrap_or(1); + let count = Vim::take_count(cx).unwrap_or(1); self.stop_recording(cx); self.update_editor(cx, |_, editor, cx| { editor.transact(cx, |editor, cx| { diff --git a/crates/vim/src/normal/case.rs b/crates/vim/src/normal/case.rs index 2c591a1f1f..0aeb4c7e98 100644 --- a/crates/vim/src/normal/case.rs +++ b/crates/vim/src/normal/case.rs @@ -118,7 +118,7 @@ impl Vim { { self.record_current_action(cx); self.store_visual_marks(cx); - let count = self.take_count(cx).unwrap_or(1) as u32; + let count = Vim::take_count(cx).unwrap_or(1) as u32; self.update_editor(cx, |vim, editor, cx| { let mut ranges = Vec::new(); diff --git a/crates/vim/src/normal/increment.rs b/crates/vim/src/normal/increment.rs index ec24064b31..ca300fc1be 100644 --- a/crates/vim/src/normal/increment.rs +++ b/crates/vim/src/normal/increment.rs @@ -26,13 +26,13 @@ impl_actions!(vim, [Increment, Decrement]); pub fn register(editor: &mut Editor, cx: &mut ViewContext) { Vim::action(editor, cx, |vim, action: &Increment, cx| { vim.record_current_action(cx); - let count = vim.take_count(cx).unwrap_or(1); + let count = Vim::take_count(cx).unwrap_or(1); let step = if action.step { 1 } else { 0 }; vim.increment(count as i64, step, cx) }); Vim::action(editor, cx, |vim, action: &Decrement, cx| { vim.record_current_action(cx); - let count = vim.take_count(cx).unwrap_or(1); + let count = Vim::take_count(cx).unwrap_or(1); let step = if action.step { -1 } else { 0 }; vim.increment(-(count as i64), step, cx) }); diff --git a/crates/vim/src/normal/paste.rs b/crates/vim/src/normal/paste.rs index feb060d594..8d49a6802c 100644 --- a/crates/vim/src/normal/paste.rs +++ b/crates/vim/src/normal/paste.rs @@ -25,7 +25,7 @@ impl Vim { pub fn paste(&mut self, action: &Paste, cx: &mut ViewContext) { self.record_current_action(cx); self.store_visual_marks(cx); - let count = self.take_count(cx).unwrap_or(1); + let count = Vim::take_count(cx).unwrap_or(1); self.update_editor(cx, |vim, editor, cx| { let text_layout_details = editor.text_layout_details(cx); diff --git a/crates/vim/src/normal/repeat.rs b/crates/vim/src/normal/repeat.rs index c89b63ecc6..41c89269f1 100644 --- a/crates/vim/src/normal/repeat.rs +++ b/crates/vim/src/normal/repeat.rs @@ -158,7 +158,7 @@ impl Vim { } pub(crate) fn replay_register(&mut self, mut register: char, cx: &mut ViewContext) { - let mut count = self.take_count(cx).unwrap_or(1); + let mut count = Vim::take_count(cx).unwrap_or(1); self.clear_operator(cx); let globals = Vim::globals(cx); @@ -184,7 +184,7 @@ impl Vim { } pub(crate) fn repeat(&mut self, from_insert_mode: bool, cx: &mut ViewContext) { - let count = self.take_count(cx); + let count = Vim::take_count(cx); let Some((mut actions, selection, mode)) = Vim::update_globals(cx, |globals, _| { let actions = globals.recorded_actions.clone(); if actions.is_empty() { diff --git a/crates/vim/src/normal/scroll.rs b/crates/vim/src/normal/scroll.rs index 8d1443e633..3f71401e2e 100644 --- a/crates/vim/src/normal/scroll.rs +++ b/crates/vim/src/normal/scroll.rs @@ -53,7 +53,7 @@ impl Vim { cx: &mut ViewContext, by: fn(c: Option) -> ScrollAmount, ) { - let amount = by(self.take_count(cx).map(|c| c as f32)); + let amount = by(Vim::take_count(cx).map(|c| c as f32)); self.update_editor(cx, |_, editor, cx| { scroll_editor(editor, move_cursor, &amount, cx) }); diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index 5d78c8937e..103d33f8af 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -120,7 +120,7 @@ impl Vim { } else { Direction::Next }; - let count = self.take_count(cx).unwrap_or(1); + let count = Vim::take_count(cx).unwrap_or(1); let prior_selections = self.editor_selections(cx); pane.update(cx, |pane, cx| { if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { @@ -226,7 +226,7 @@ impl Vim { pub fn move_to_match_internal(&mut self, direction: Direction, cx: &mut ViewContext) { let Some(pane) = self.pane(cx) else { return }; - let count = self.take_count(cx).unwrap_or(1); + let count = Vim::take_count(cx).unwrap_or(1); let prior_selections = self.editor_selections(cx); let success = pane.update(cx, |pane, cx| { @@ -264,7 +264,7 @@ impl Vim { cx: &mut ViewContext, ) { let Some(pane) = self.pane(cx) else { return }; - let count = self.take_count(cx).unwrap_or(1); + let count = Vim::take_count(cx).unwrap_or(1); let prior_selections = self.editor_selections(cx); let vim = cx.view().clone(); diff --git a/crates/vim/src/normal/substitute.rs b/crates/vim/src/normal/substitute.rs index dc27e2b219..c2b27227ca 100644 --- a/crates/vim/src/normal/substitute.rs +++ b/crates/vim/src/normal/substitute.rs @@ -9,7 +9,7 @@ actions!(vim, [Substitute, SubstituteLine]); pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext) { Vim::action(editor, cx, |vim, _: &Substitute, cx| { vim.start_recording(cx); - let count = vim.take_count(cx); + let count = Vim::take_count(cx); vim.substitute(count, vim.mode == Mode::VisualLine, cx); }); @@ -18,7 +18,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext) { if matches!(vim.mode, Mode::VisualBlock | Mode::Visual) { vim.switch_mode(Mode::VisualLine, false, cx) } - let count = vim.take_count(cx); + let count = Vim::take_count(cx); vim.substitute(count, true, cx) }); } diff --git a/crates/vim/src/replace.rs b/crates/vim/src/replace.rs index 753eec0971..8b84849043 100644 --- a/crates/vim/src/replace.rs +++ b/crates/vim/src/replace.rs @@ -22,7 +22,7 @@ pub fn register(editor: &mut Editor, cx: &mut ViewContext) { if vim.mode != Mode::Replace { return; } - let count = vim.take_count(cx); + let count = Vim::take_count(cx); vim.undo_replace(count, cx) }); } diff --git a/crates/vim/src/rewrap.rs b/crates/vim/src/rewrap.rs index db54c4ed57..1ef4a3fc03 100644 --- a/crates/vim/src/rewrap.rs +++ b/crates/vim/src/rewrap.rs @@ -10,7 +10,7 @@ actions!(vim, [Rewrap]); pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext) { Vim::action(editor, cx, |vim, _: &Rewrap, cx| { vim.record_current_action(cx); - vim.take_count(cx); + Vim::take_count(cx); vim.store_visual_marks(cx); vim.update_editor(cx, |vim, editor, cx| { editor.transact(cx, |editor, cx| { diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index 510ed6557d..47742fb0c3 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -150,6 +150,11 @@ pub struct VimGlobals { pub dot_recording: bool, pub dot_replaying: bool, + /// pre_count is the number before an operator is specified (3 in 3d2d) + pub pre_count: Option, + /// post_count is the number after an operator is specified (2 in 3d2d) + pub post_count: Option, + pub stop_recording_after_next_action: bool, pub ignore_current_insertion: bool, pub recorded_count: Option, diff --git a/crates/vim/src/surrounds.rs b/crates/vim/src/surrounds.rs index 88bcb6a2e1..719a147062 100644 --- a/crates/vim/src/surrounds.rs +++ b/crates/vim/src/surrounds.rs @@ -35,7 +35,7 @@ impl Vim { cx: &mut ViewContext, ) { self.stop_recording(cx); - let count = self.take_count(cx); + let count = Vim::take_count(cx); let mode = self.mode; self.update_editor(cx, |_, editor, cx| { let text_layout_details = editor.text_layout_details(cx); diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index dd3bf297cb..0f206a88cc 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -25,8 +25,8 @@ use editor::{ Anchor, Bias, Editor, EditorEvent, EditorMode, ToPoint, }; use gpui::{ - actions, impl_actions, Action, AppContext, Entity, EventEmitter, KeyContext, KeystrokeEvent, - Render, Subscription, View, ViewContext, WeakView, + actions, impl_actions, Action, AppContext, Axis, Entity, EventEmitter, KeyContext, + KeystrokeEvent, Render, Subscription, View, ViewContext, WeakView, }; use insert::{NormalBefore, TemporaryNormal}; use language::{CursorShape, Point, Selection, SelectionGoal, TransactionId}; @@ -40,12 +40,17 @@ use settings::{update_settings_file, Settings, SettingsSources, SettingsStore}; use state::{Mode, Operator, RecordedSelection, SearchState, VimGlobals}; use std::{mem, ops::Range, sync::Arc}; use surrounds::SurroundsType; +use theme::ThemeSettings; use ui::{IntoElement, VisualContext}; use vim_mode_setting::VimModeSetting; -use workspace::{self, Pane, Workspace}; +use workspace::{self, Pane, ResizeIntent, Workspace}; use crate::state::ReplayableAction; +/// Used to resize the current pane +#[derive(Clone, Deserialize, PartialEq)] +pub struct ResizePane(pub ResizeIntent); + /// An Action to Switch between modes #[derive(Clone, Deserialize, PartialEq)] pub struct SwitchMode(pub Mode); @@ -81,7 +86,10 @@ actions!( // in the workspace namespace so it's not filtered out when vim is disabled. actions!(workspace, [ToggleVimMode]); -impl_actions!(vim, [SwitchMode, PushOperator, Number, SelectRegister]); +impl_actions!( + vim, + [ResizePane, SwitchMode, PushOperator, Number, SelectRegister] +); /// Initializes the `vim` crate. pub fn init(cx: &mut AppContext) { @@ -109,6 +117,30 @@ pub fn init(cx: &mut AppContext) { }); }); + workspace.register_action(|workspace, action: &ResizePane, cx| { + let count = Vim::take_count(cx.window_context()).unwrap_or(1) as f32; + let theme = ThemeSettings::get_global(cx); + let Ok(font_id) = cx.text_system().font_id(&theme.buffer_font) else { + return; + }; + let Ok(width) = cx + .text_system() + .advance(font_id, theme.buffer_font_size(cx), 'm') + else { + return; + }; + let height = theme.buffer_font_size(cx) * theme.buffer_line_height.value(); + + let (axis, amount) = match action.0 { + ResizeIntent::Lengthen => (Axis::Vertical, height), + ResizeIntent::Shorten => (Axis::Vertical, height * -1.), + ResizeIntent::Widen => (Axis::Horizontal, width.width), + ResizeIntent::Narrow => (Axis::Horizontal, width.width * -1.), + }; + + workspace.resize_pane(axis, amount * count, cx); + }); + workspace.register_action(|workspace, _: &SearchSubmit, cx| { let vim = workspace .focused_pane(cx) @@ -131,7 +163,7 @@ pub(crate) struct VimAddon { impl editor::Addon for VimAddon { fn extend_key_context(&self, key_context: &mut KeyContext, cx: &AppContext) { - self.view.read(cx).extend_key_context(key_context) + self.view.read(cx).extend_key_context(key_context, cx) } fn to_any(&self) -> &dyn std::any::Any { @@ -146,11 +178,6 @@ pub(crate) struct Vim { pub temp_mode: bool, pub exit_temporary_mode: bool, - /// pre_count is the number before an operator is specified (3 in 3d2d) - pre_count: Option, - /// post_count is the number after an operator is specified (2 in 3d2d) - post_count: Option, - operator_stack: Vec, pub(crate) replacements: Vec<(Range, String)>, @@ -197,8 +224,6 @@ impl Vim { last_mode: Mode::Normal, temp_mode: false, exit_temporary_mode: false, - pre_count: None, - post_count: None, operator_stack: Vec::new(), replacements: Vec::new(), @@ -471,7 +496,7 @@ impl Vim { self.current_anchor.take(); } if mode != Mode::Insert && mode != Mode::Replace { - self.take_count(cx); + Vim::take_count(cx); } // Sync editor settings like clip mode @@ -551,22 +576,24 @@ impl Vim { }); } - fn take_count(&mut self, cx: &mut ViewContext) -> Option { + pub fn take_count(cx: &mut AppContext) -> Option { let global_state = cx.global_mut::(); if global_state.dot_replaying { return global_state.recorded_count; } - let count = if self.post_count.is_none() && self.pre_count.is_none() { + let count = if global_state.post_count.is_none() && global_state.pre_count.is_none() { return None; } else { - Some(self.post_count.take().unwrap_or(1) * self.pre_count.take().unwrap_or(1)) + Some( + global_state.post_count.take().unwrap_or(1) + * global_state.pre_count.take().unwrap_or(1), + ) }; if global_state.dot_recording { global_state.recorded_count = count; } - self.sync_vim_settings(cx); count } @@ -613,7 +640,7 @@ impl Vim { } } - pub fn extend_key_context(&self, context: &mut KeyContext) { + pub fn extend_key_context(&self, context: &mut KeyContext, cx: &AppContext) { let mut mode = match self.mode { Mode::Normal => "normal", Mode::Visual | Mode::VisualLine | Mode::VisualBlock => "visual", @@ -625,8 +652,8 @@ impl Vim { let mut operator_id = "none"; let active_operator = self.active_operator(); - if active_operator.is_none() && self.pre_count.is_some() - || active_operator.is_some() && self.post_count.is_some() + if active_operator.is_none() && cx.global::().pre_count.is_some() + || active_operator.is_some() && cx.global::().post_count.is_some() { context.add("VimCount"); } @@ -837,18 +864,18 @@ impl Vim { fn push_count_digit(&mut self, number: usize, cx: &mut ViewContext) { if self.active_operator().is_some() { - let post_count = self.post_count.unwrap_or(0); + let post_count = Vim::globals(cx).post_count.unwrap_or(0); - self.post_count = Some( + Vim::globals(cx).post_count = Some( post_count .checked_mul(10) .and_then(|post_count| post_count.checked_add(number)) .unwrap_or(post_count), ) } else { - let pre_count = self.pre_count.unwrap_or(0); + let pre_count = Vim::globals(cx).pre_count.unwrap_or(0); - self.pre_count = Some( + Vim::globals(cx).pre_count = Some( pre_count .checked_mul(10) .and_then(|pre_count| pre_count.checked_add(number)) @@ -880,7 +907,7 @@ impl Vim { } fn clear_operator(&mut self, cx: &mut ViewContext) { - self.take_count(cx); + Vim::take_count(cx); self.selected_register.take(); self.operator_stack.clear(); self.sync_vim_settings(cx); diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 47aa618b5c..813be6dda1 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -538,9 +538,8 @@ impl Vim { } pub fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { - let count = self - .take_count(cx) - .unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 }); + let count = + Vim::take_count(cx).unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 }); self.update_editor(cx, |_, editor, cx| { editor.set_clip_at_line_ends(false, cx); for _ in 0..count { @@ -556,9 +555,8 @@ impl Vim { } pub fn select_previous(&mut self, _: &SelectPrevious, cx: &mut ViewContext) { - let count = self - .take_count(cx) - .unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 }); + let count = + Vim::take_count(cx).unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 }); self.update_editor(cx, |_, editor, cx| { for _ in 0..count { if editor @@ -573,7 +571,7 @@ impl Vim { } pub fn select_match(&mut self, direction: Direction, cx: &mut ViewContext) { - let count = self.take_count(cx).unwrap_or(1); + let count = Vim::take_count(cx).unwrap_or(1); let Some(pane) = self.pane(cx) else { return; }; diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 390fa6d174..46975eb8f3 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -8,8 +8,8 @@ use call::{ActiveCall, ParticipantLocation}; use client::proto::PeerId; use collections::HashMap; use gpui::{ - point, size, AnyView, AnyWeakView, Axis, Bounds, IntoElement, Model, MouseButton, Pixels, - Point, StyleRefinement, View, ViewContext, + point, size, Along, AnyView, AnyWeakView, Axis, Bounds, IntoElement, Model, MouseButton, + Pixels, Point, StyleRefinement, View, ViewContext, }; use parking_lot::Mutex; use project::Project; @@ -90,6 +90,21 @@ impl PaneGroup { } } + pub fn resize( + &mut self, + pane: &View, + direction: Axis, + amount: Pixels, + bounds: &Bounds, + ) { + match &mut self.root { + Member::Pane(_) => {} + Member::Axis(axis) => { + let _ = axis.resize(pane, direction, amount, bounds); + } + }; + } + pub fn swap(&mut self, from: &View, to: &View) { match &mut self.root { Member::Pane(_) => {} @@ -445,6 +460,116 @@ impl PaneAxis { } } + fn resize( + &mut self, + pane: &View, + axis: Axis, + amount: Pixels, + bounds: &Bounds, + ) -> Option { + let container_size = self + .bounding_boxes + .lock() + .iter() + .filter_map(|e| *e) + .reduce(|acc, e| acc.union(&e)) + .unwrap_or(*bounds) + .size; + + let found_pane = self + .members + .iter() + .any(|member| matches!(member, Member::Pane(p) if p == pane)); + + if found_pane && self.axis != axis { + return Some(false); // pane found but this is not the correct axis direction + } + let mut found_axis_index: Option = None; + if !found_pane { + for (i, pa) in self.members.iter_mut().enumerate() { + if let Member::Axis(pa) = pa { + if let Some(done) = pa.resize(pane, axis, amount, bounds) { + if done { + return Some(true); // pane found and operations already done + } else if self.axis != axis { + return Some(false); // pane found but this is not the correct axis direction + } else { + found_axis_index = Some(i); // pane found and this is correct direction + } + } + } + } + found_axis_index?; // no pane found + } + + let min_size = match axis { + Axis::Horizontal => px(HORIZONTAL_MIN_SIZE), + Axis::Vertical => px(VERTICAL_MIN_SIZE), + }; + let mut flexes = self.flexes.lock(); + + let ix = if found_pane { + self.members.iter().position(|m| { + if let Member::Pane(p) = m { + p == pane + } else { + false + } + }) + } else { + found_axis_index + }; + + if ix.is_none() { + return Some(true); + } + + let ix = ix.unwrap_or(0); + + let size = move |ix, flexes: &[f32]| { + container_size.along(axis) * (flexes[ix] / flexes.len() as f32) + }; + + // Don't allow resizing to less than the minimum size, if elements are already too small + if min_size - px(1.) > size(ix, flexes.as_slice()) { + return Some(true); + } + + let flex_changes = |pixel_dx, target_ix, next: isize, flexes: &[f32]| { + let flex_change = flexes.len() as f32 * pixel_dx / container_size.along(axis); + let current_target_flex = flexes[target_ix] + flex_change; + let next_target_flex = flexes[(target_ix as isize + next) as usize] - flex_change; + (current_target_flex, next_target_flex) + }; + + let apply_changes = + |current_ix: usize, proposed_current_pixel_change: Pixels, flexes: &mut [f32]| { + let next_target_size = Pixels::max( + size(current_ix + 1, flexes) - proposed_current_pixel_change, + min_size, + ); + let current_target_size = Pixels::max( + size(current_ix, flexes) + size(current_ix + 1, flexes) - next_target_size, + min_size, + ); + + let current_pixel_change = current_target_size - size(current_ix, flexes); + + let (current_target_flex, next_target_flex) = + flex_changes(current_pixel_change, current_ix, 1, flexes); + + flexes[current_ix] = current_target_flex; + flexes[current_ix + 1] = next_target_flex; + }; + + if ix + 1 == flexes.len() { + apply_changes(ix - 1, -1.0 * amount, flexes.as_mut_slice()); + } else { + apply_changes(ix, amount, flexes.as_mut_slice()); + } + Some(true) + } + fn swap(&mut self, from: &View, to: &View) { for member in self.members.iter_mut() { match member { @@ -625,6 +750,14 @@ impl SplitDirection { } } +#[derive(Clone, Copy, Debug, Deserialize, PartialEq)] +pub enum ResizeIntent { + Lengthen, + Shorten, + Widen, + Narrow, +} + mod element { use std::mem; diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 42db3183bd..b2be324b5a 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2988,6 +2988,12 @@ impl Workspace { } } + pub fn resize_pane(&mut self, axis: gpui::Axis, amount: Pixels, cx: &mut ViewContext) { + self.center + .resize(&self.active_pane.clone(), axis, amount, &self.bounds); + cx.notify(); + } + fn handle_pane_focused(&mut self, pane: View, cx: &mut ViewContext) { // This is explicitly hoisted out of the following check for pane identity as // terminal panel panes are not registered as a center panes. From 4e720be41c46d96f127ff1de070dcb5f2a071651 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 26 Nov 2024 16:45:38 -0800 Subject: [PATCH 164/886] Add ctrl-w _ and ctrl-w = (#21227) Closes #ISSUE Release Notes: - vim: Add support for `ctrl-w _` and `ctrl-w =` --- assets/keymaps/vim.json | 2 ++ crates/vim/src/vim.rs | 29 ++++++++++++++++++++++++++--- crates/workspace/src/pane_group.rs | 19 ++++++++++++++++++- crates/workspace/src/workspace.rs | 9 +++++++++ 4 files changed, 55 insertions(+), 4 deletions(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 858a1b8d31..a69e97401d 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -561,6 +561,8 @@ "ctrl-w <": ["vim::ResizePane", "Narrow"], "ctrl-w -": ["vim::ResizePane", "Shorten"], "ctrl-w +": ["vim::ResizePane", "Lengthen"], + "ctrl-w _": "vim::MaximizePane", + "ctrl-w =": "vim::ResetPaneSizes", "ctrl-w g t": "pane::ActivateNextItem", "ctrl-w ctrl-g t": "pane::ActivateNextItem", "ctrl-w g shift-t": "pane::ActivatePrevItem", diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 0f206a88cc..a1820eafbb 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -41,7 +41,7 @@ use state::{Mode, Operator, RecordedSelection, SearchState, VimGlobals}; use std::{mem, ops::Range, sync::Arc}; use surrounds::SurroundsType; use theme::ThemeSettings; -use ui::{IntoElement, VisualContext}; +use ui::{px, IntoElement, VisualContext}; use vim_mode_setting::VimModeSetting; use workspace::{self, Pane, ResizeIntent, Workspace}; @@ -79,7 +79,9 @@ actions!( InnerObject, FindForward, FindBackward, - OpenDefaultKeymap + OpenDefaultKeymap, + MaximizePane, + ResetPaneSizes, ] ); @@ -117,8 +119,29 @@ pub fn init(cx: &mut AppContext) { }); }); + workspace.register_action(|workspace, _: &ResetPaneSizes, cx| { + workspace.reset_pane_sizes(cx); + }); + + workspace.register_action(|workspace, _: &MaximizePane, cx| { + let pane = workspace.active_pane(); + let Some(size) = workspace.bounding_box_for_pane(&pane) else { + return; + }; + + let theme = ThemeSettings::get_global(cx); + let height = theme.buffer_font_size(cx) * theme.buffer_line_height.value(); + + let desired_size = if let Some(count) = Vim::take_count(cx) { + height * count + } else { + px(10000.) + }; + workspace.resize_pane(Axis::Vertical, desired_size - size.size.height, cx) + }); + workspace.register_action(|workspace, action: &ResizePane, cx| { - let count = Vim::take_count(cx.window_context()).unwrap_or(1) as f32; + let count = Vim::take_count(cx).unwrap_or(1) as f32; let theme = ThemeSettings::get_global(cx); let Ok(font_id) = cx.text_system().font_id(&theme.buffer_font) else { return; diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 46975eb8f3..6f7d1a66b9 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -105,6 +105,15 @@ impl PaneGroup { }; } + pub fn reset_pane_sizes(&mut self) { + match &mut self.root { + Member::Pane(_) => {} + Member::Axis(axis) => { + let _ = axis.reset_pane_sizes(); + } + }; + } + pub fn swap(&mut self, from: &View, to: &View) { match &mut self.root { Member::Pane(_) => {} @@ -460,6 +469,15 @@ impl PaneAxis { } } + fn reset_pane_sizes(&self) { + *self.flexes.lock() = vec![1.; self.members.len()]; + for member in self.members.iter() { + if let Member::Axis(axis) = member { + axis.reset_pane_sizes(); + } + } + } + fn resize( &mut self, pane: &View, @@ -759,7 +777,6 @@ pub enum ResizeIntent { } mod element { - use std::mem; use std::{cell::RefCell, iter, rc::Rc, sync::Arc}; diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index b2be324b5a..28fd730e60 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2946,6 +2946,10 @@ impl Workspace { } } + pub fn bounding_box_for_pane(&self, pane: &View) -> Option> { + self.center.bounding_box_for_pane(pane) + } + pub fn find_pane_in_direction( &mut self, direction: SplitDirection, @@ -2994,6 +2998,11 @@ impl Workspace { cx.notify(); } + pub fn reset_pane_sizes(&mut self, cx: &mut ViewContext) { + self.center.reset_pane_sizes(); + cx.notify(); + } + fn handle_pane_focused(&mut self, pane: View, cx: &mut ViewContext) { // This is explicitly hoisted out of the following check for pane identity as // terminal panel panes are not registered as a center panes. From e865b6c52459ca322bff0e6caabca07c724cb6f4 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Wed, 27 Nov 2024 00:56:51 +0000 Subject: [PATCH 165/886] Fix cmd-shift-e (reveal in project panel) to match vscode (#21228) Release Notes: - Fixed cmd-shift-e / ctrl-shift-e (`pane::RevealInProjectPanel` / `project_panel::ToggleFocus`) to better my VSCode behavior --- assets/keymaps/default-linux.json | 3 ++- assets/keymaps/default-macos.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 2eedc1c839..2b792f353f 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -405,7 +405,7 @@ "ctrl-shift-p": "command_palette::Toggle", "f1": "command_palette::Toggle", "ctrl-shift-m": "diagnostics::Deploy", - "ctrl-shift-e": "project_panel::ToggleFocus", + "ctrl-shift-e": "pane::RevealInProjectPanel", "ctrl-shift-b": "outline_panel::ToggleFocus", "ctrl-?": "assistant::ToggleFocus", "ctrl-alt-s": "workspace::SaveAll", @@ -594,6 +594,7 @@ "ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }], "alt-ctrl-r": "project_panel::RevealInFileManager", "ctrl-shift-enter": "project_panel::OpenWithSystem", + "ctrl-shift-e": "project_panel::ToggleFocus", "ctrl-shift-f": "project_panel::NewSearchInDirectory", "shift-down": "menu::SelectNext", "shift-up": "menu::SelectPrev", diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index ddbbdd3faf..514604ef98 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -446,7 +446,7 @@ "ctrl-shift-tab": ["tab_switcher::Toggle", { "select_last": true }], "cmd-shift-p": "command_palette::Toggle", "cmd-shift-m": "diagnostics::Deploy", - "cmd-shift-e": "project_panel::ToggleFocus", + "cmd-shift-e": "pane::RevealInProjectPanel", "cmd-shift-b": "outline_panel::ToggleFocus", "cmd-?": "assistant::ToggleFocus", "cmd-alt-s": "workspace::SaveAll", @@ -616,6 +616,7 @@ "cmd-delete": ["project_panel::Delete", { "skip_prompt": false }], "alt-cmd-r": "project_panel::RevealInFileManager", "ctrl-shift-enter": "project_panel::OpenWithSystem", + "cmd-shift-e": "project_panel::ToggleFocus", "cmd-alt-backspace": ["project_panel::Delete", { "skip_prompt": false }], "cmd-shift-f": "project_panel::NewSearchInDirectory", "shift-down": "menu::SelectNext", From ce6782f4c8d3fe540110da0f1a49058c95192915 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 27 Nov 2024 12:02:39 +0200 Subject: [PATCH 166/886] Use eslint from the fork (#21233) Part of https://github.com/zed-industries/zed/issues/21220 Microsoft had decided to switch over to a different releasing strategy, autogenerating all releases and not publishing tarballs anymore. But it was not enough, and they had also removed old tarballs, including a relatively old `2.4.4` version release's tarballs, which broke Zed downloads. See https://github.com/microsoft/vscode-eslint/issues/1954 This PR uses https://github.com/zed-industries/vscode-eslint/releases/tag/2.4.4 from Zed's fork, manually released for the same tag. This approach is merely a stub before more sustainable solution is found, and I think we need to pivot into downloading *.vsix from https://open-vsx.org/extension/dbaeumer/vscode-eslint but this is quite a change so not done right now. Release Notes: - Fixed eslint 404 downloads --- crates/languages/src/typescript.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/languages/src/typescript.rs b/crates/languages/src/typescript.rs index c580575a1e..076d8d3374 100644 --- a/crates/languages/src/typescript.rs +++ b/crates/languages/src/typescript.rs @@ -412,7 +412,7 @@ impl LspAdapter for EsLintLspAdapter { _delegate: &dyn LspAdapterDelegate, ) -> Result> { let url = build_asset_url( - "microsoft/vscode-eslint", + "zed-industries/vscode-eslint", Self::CURRENT_VERSION_TAG_NAME, Self::GITHUB_ASSET_KIND, )?; From 6736806924d1ebadcb0c47e350c370af2cf8abc9 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Wed, 27 Nov 2024 14:25:43 +0000 Subject: [PATCH 167/886] docs: Move install rustup callup to top of developing-extensions.md (#21239) --- docs/src/extensions/developing-extensions.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/src/extensions/developing-extensions.md b/docs/src/extensions/developing-extensions.md index bdfab5fcde..c404d260a0 100644 --- a/docs/src/extensions/developing-extensions.md +++ b/docs/src/extensions/developing-extensions.md @@ -9,6 +9,16 @@ Extensions can add the following capabilities to Zed: - [Slash Commands](./slash-commands.md) - [Context Servers](./context-servers.md) +## Developing an Extension Locally + +Before starting to develop an extension for Zed, be sure to [install Rust via rustup](https://www.rust-lang.org/tools/install). + +When developing an extension, you can use it in Zed without needing to publish it by installing it as a _dev extension_. + +From the extensions page, click the `Install Dev Extension` button and select the directory containing your extension. + +If you already have a published extension with the same name installed, your dev extension will override it. + ## Directory Structure of a Zed Extension A Zed extension is a Git repository that contains an `extension.toml`. This file must contain some @@ -75,16 +85,6 @@ impl zed::Extension for MyExtension { zed::register_extension!(MyExtension); ``` -## Developing an Extension Locally - -Before starting to develop an extension for Zed, be sure to [install Rust via rustup](https://www.rust-lang.org/tools/install). - -When developing an extension, you can use it in Zed without needing to publish it by installing it as a _dev extension_. - -From the extensions page, click the `Install Dev Extension` button and select the directory containing your extension. - -If you already have a published extension with the same name installed, your dev extension will override it. - ## Publishing your extension To publish an extension, open a PR to [the `zed-industries/extensions` repo](https://github.com/zed-industries/extensions). From c021ee60d67cfbecb800f33f5d644a201a6bb567 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Wed, 27 Nov 2024 09:48:40 -0500 Subject: [PATCH 168/886] v0.165.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 41532b9773..d9da330daa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15614,7 +15614,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.164.0" +version = "0.165.0" dependencies = [ "activity_indicator", "anyhow", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 5003ca1b81..24fc0dec8b 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.164.0" +version = "0.165.0" publish = false license = "GPL-3.0-or-later" authors = ["Zed Team "] From 4564da28757d744364ff12dd3c7b43155d75f84a Mon Sep 17 00:00:00 2001 From: Stanislav Alekseev <43210583+WeetHet@users.noreply.github.com> Date: Wed, 27 Nov 2024 20:22:17 +0200 Subject: [PATCH 169/886] Improve Nix package and shell (#21075) With an addition of useFetchCargoVendor, crane becomes less necessary for our use. This reuses the package from nixpkgs as well as creating a better devshell that both work on macOS. I use Xcode's SDKROOT and DEVELOPER_DIR to point the swift in the livekit client crate to a correct sdk when using a devshell. Devshell should work without that once apple releases sources for the 15.1 SDK but for now this is an easy fix This also replaces fenix with rust-overlay because of issues with the out-of-sandbox access I've noticed fenix installed toolchains have Release Notes: - N/A --- .envrc | 2 + flake.lock | 70 +++--------- flake.nix | 81 ++++++------- nix/build.nix | 307 ++++++++++++++++++++++++++++++++++---------------- nix/shell.nix | 104 +++++++++-------- 5 files changed, 326 insertions(+), 238 deletions(-) create mode 100644 .envrc diff --git a/.envrc b/.envrc new file mode 100644 index 0000000000..082c01feeb --- /dev/null +++ b/.envrc @@ -0,0 +1,2 @@ +watch_file nix/shell.nix +use flake diff --git a/flake.lock b/flake.lock index 5666e73569..4011b38c4b 100644 --- a/flake.lock +++ b/flake.lock @@ -1,41 +1,5 @@ { "nodes": { - "crane": { - "locked": { - "lastModified": 1727060013, - "narHash": "sha256-/fC5YlJy4IoAW9GhkJiwyzk0K/gQd9Qi4rRcoweyG9E=", - "owner": "ipetkov", - "repo": "crane", - "rev": "6b40cc876c929bfe1e3a24bf538ce3b5622646ba", - "type": "github" - }, - "original": { - "owner": "ipetkov", - "repo": "crane", - "type": "github" - } - }, - "fenix": { - "inputs": { - "nixpkgs": [ - "nixpkgs" - ], - "rust-analyzer-src": "rust-analyzer-src" - }, - "locked": { - "lastModified": 1727073227, - "narHash": "sha256-1kmkEQmFfGVuPBasqSZrNThqyMDV1SzTalQdRZxtDRs=", - "owner": "nix-community", - "repo": "fenix", - "rev": "88cc292eb3c689073c784d6aecc0edbd47e12881", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "fenix", - "type": "github" - } - }, "flake-compat": { "locked": { "lastModified": 1696426674, @@ -53,11 +17,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1726937504, - "narHash": "sha256-bvGoiQBvponpZh8ClUcmJ6QnsNKw0EMrCQJARK3bI1c=", + "lastModified": 1732014248, + "narHash": "sha256-y/MEyuJ5oBWrWAic/14LaIr/u5E0wRVzyYsouYY3W6w=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "9357f4f23713673f310988025d9dc261c20e70c6", + "rev": "23e89b7da85c3640bbc2173fe04f4bd114342367", "type": "github" }, "original": { @@ -69,26 +33,28 @@ }, "root": { "inputs": { - "crane": "crane", - "fenix": "fenix", "flake-compat": "flake-compat", - "nixpkgs": "nixpkgs" + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" } }, - "rust-analyzer-src": { - "flake": false, + "rust-overlay": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, "locked": { - "lastModified": 1726443025, - "narHash": "sha256-nCmG4NJpwI0IoIlYlwtDwVA49yuspA2E6OhfCOmiArQ=", - "owner": "rust-lang", - "repo": "rust-analyzer", - "rev": "94b526fc86eaa0e90fb4d54a5ba6313aa1e9b269", + "lastModified": 1732242723, + "narHash": "sha256-NWI8csIK0ujFlFuEXKnoc+7hWoCiEtINK9r48LUUMeU=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "a229311fcb45b88a95fdfa5cecd8349c809a272a", "type": "github" }, "original": { - "owner": "rust-lang", - "ref": "nightly", - "repo": "rust-analyzer", + "owner": "oxalica", + "repo": "rust-overlay", "type": "github" } } diff --git a/flake.nix b/flake.nix index 2ee86c4466..3258522eb4 100644 --- a/flake.nix +++ b/flake.nix @@ -3,60 +3,61 @@ inputs = { nixpkgs.url = "github:NixOS/nixpkgs?ref=nixos-unstable"; - fenix = { - url = "github:nix-community/fenix"; + rust-overlay = { + url = "github:oxalica/rust-overlay"; inputs.nixpkgs.follows = "nixpkgs"; }; - crane.url = "github:ipetkov/crane"; flake-compat.url = "github:edolstra/flake-compat"; }; - outputs = { - nixpkgs, - crane, - fenix, - ... - }: let - systems = ["x86_64-linux" "aarch64-linux"]; + outputs = + { nixpkgs, rust-overlay, ... }: + let + systems = [ + "x86_64-linux" + "x86_64-darwin" + "aarch64-linux" + "aarch64-darwin" + ]; - overlays = { - fenix = fenix.overlays.default; - rust-toolchain = final: prev: { - rustToolchain = final.fenix.stable.toolchain; - }; - zed-editor = final: prev: { - zed-editor = final.callPackage ./nix/build.nix { - craneLib = (crane.mkLib final).overrideToolchain final.rustToolchain; - rustPlatform = final.makeRustPlatform { - inherit (final.rustToolchain) cargo rustc; + overlays = { + rust-overlay = rust-overlay.overlays.default; + rust-toolchain = final: prev: { + rustToolchain = final.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml; + }; + zed-editor = final: prev: { + zed-editor = final.callPackage ./nix/build.nix { + rustPlatform = final.makeRustPlatform { + cargo = final.rustToolchain; + rustc = final.rustToolchain; + }; }; }; }; - }; - mkPkgs = system: - import nixpkgs { - inherit system; - overlays = builtins.attrValues overlays; - }; + mkPkgs = + system: + import nixpkgs { + inherit system; + overlays = builtins.attrValues overlays; + }; - forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f (mkPkgs system)); - in { - packages = forAllSystems (pkgs: { - zed-editor = pkgs.zed-editor; - default = pkgs.zed-editor; - }); + forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f (mkPkgs system)); + in + { + packages = forAllSystems (pkgs: { + zed-editor = pkgs.zed-editor; + default = pkgs.zed-editor; + }); - devShells = forAllSystems (pkgs: { - default = import ./nix/shell.nix {inherit pkgs;}; - }); + devShells = forAllSystems (pkgs: { + default = import ./nix/shell.nix { inherit pkgs; }; + }); - formatter = forAllSystems (pkgs: pkgs.alejandra); + formatter = forAllSystems (pkgs: pkgs.nixfmt-rfc-style); - overlays = - overlays - // { + overlays = overlays // { default = nixpkgs.lib.composeManyExtensions (builtins.attrValues overlays); }; - }; + }; } diff --git a/nix/build.nix b/nix/build.nix index 4782c9a56f..903f9790c7 100644 --- a/nix/build.nix +++ b/nix/build.nix @@ -1,10 +1,9 @@ { lib, - craneLib, rustPlatform, + fetchpatch, clang, - llvmPackages_18, - mold-wrapped, + cmake, copyDesktopItems, curl, perl, @@ -22,122 +21,236 @@ wayland, libglvnd, xorg, + stdenv, makeFontsConf, vulkan-loader, envsubst, - stdenvAdapters, + cargo-about, + versionCheckHook, + cargo-bundle, + git, + apple-sdk_15, + darwinMinVersionHook, + makeWrapper, + nodejs_22, nix-gitignore, + withGLES ? false, - cmake, -}: let - includeFilter = path: type: let - baseName = baseNameOf (toString path); - parentDir = dirOf path; - inRootDir = type == "directory" && parentDir == ../.; - in - !(inRootDir && (baseName == "docs" || baseName == ".github" || baseName == "script" || baseName == ".git" || baseName == "target")); +}: + +assert withGLES -> stdenv.hostPlatform.isLinux; + +let + includeFilter = + path: type: + let + baseName = baseNameOf (toString path); + parentDir = dirOf path; + inRootDir = type == "directory" && parentDir == ../.; + in + !( + inRootDir + && ( + baseName == "docs" + || baseName == ".github" + || baseName == "script" + || baseName == ".git" + || baseName == "target" + ) + ); +in +rustPlatform.buildRustPackage rec { + pname = "zed-editor"; + version = "nightly"; src = lib.cleanSourceWith { - src = nix-gitignore.gitignoreSource [] ../.; + src = nix-gitignore.gitignoreSource [ ] ../.; filter = includeFilter; name = "source"; }; - stdenv = stdenvAdapters.useMoldLinker llvmPackages_18.stdenv; + patches = + [ + # Zed uses cargo-install to install cargo-about during the script execution. + # We provide cargo-about ourselves and can skip this step. + # Until https://github.com/zed-industries/zed/issues/19971 is fixed, + # we also skip any crate for which the license cannot be determined. + (fetchpatch { + url = "https://raw.githubusercontent.com/NixOS/nixpkgs/1fd02d90c6c097f91349df35da62d36c19359ba7/pkgs/by-name/ze/zed-editor/0001-generate-licenses.patch"; + hash = "sha256-cLgqLDXW1JtQ2OQFLd5UolAjfy7bMoTw40lEx2jA2pk="; + }) + ] + ++ lib.optionals stdenv.hostPlatform.isDarwin [ + # Livekit requires Swift 6 + # We need this until livekit-rust sdk is used + (fetchpatch { + url = "https://raw.githubusercontent.com/NixOS/nixpkgs/1fd02d90c6c097f91349df35da62d36c19359ba7/pkgs/by-name/ze/zed-editor/0002-disable-livekit-darwin.patch"; + hash = "sha256-whZ7RaXv8hrVzWAveU3qiBnZSrvGNEHTuyNhxgMIo5w="; + }) + ]; - commonArgs = - craneLib.crateNameFromCargoToml {cargoToml = ../crates/zed/Cargo.toml;} - // { - inherit src stdenv; + useFetchCargoVendor = true; + cargoHash = "sha256-xL/EBe3+rlaPwU2zZyQtsZNHGBjzAD8ZCWrQXCQVxm8="; - nativeBuildInputs = [ - clang - copyDesktopItems - curl - mold-wrapped - perl - pkg-config - protobuf - rustPlatform.bindgenHook - cmake + nativeBuildInputs = + [ + clang + cmake + copyDesktopItems + curl + perl + pkg-config + protobuf + rustPlatform.bindgenHook + cargo-about + ] + ++ lib.optionals stdenv.hostPlatform.isLinux [ makeWrapper ] + ++ lib.optionals stdenv.hostPlatform.isDarwin [ cargo-bundle ]; + + dontUseCmakeConfigure = true; + + buildInputs = + [ + curl + fontconfig + freetype + libgit2 + openssl + sqlite + zlib + zstd + ] + ++ lib.optionals stdenv.hostPlatform.isLinux [ + alsa-lib + libxkbcommon + wayland + xorg.libxcb + ] + ++ lib.optionals stdenv.hostPlatform.isDarwin [ + apple-sdk_15 + (darwinMinVersionHook "10.15") + ]; + + cargoBuildFlags = [ + "--package=zed" + "--package=cli" + ]; + + buildFeatures = lib.optionals stdenv.hostPlatform.isDarwin [ "gpui/runtime_shaders" ]; + + env = { + ZSTD_SYS_USE_PKG_CONFIG = true; + FONTCONFIG_FILE = makeFontsConf { + fontDirectories = [ + "${src}/assets/fonts/plex-mono" + "${src}/assets/fonts/plex-sans" ]; - - buildInputs = [ - curl - fontconfig - freetype - libgit2 - openssl - sqlite - zlib - zstd - - alsa-lib - libxkbcommon - wayland - xorg.libxcb - ]; - - ZSTD_SYS_USE_PKG_CONFIG = true; - FONTCONFIG_FILE = makeFontsConf { - fontDirectories = [ - "../assets/fonts/zed-mono" - "../assets/fonts/zed-sans" - ]; - }; - ZED_UPDATE_EXPLANATION = "zed has been installed using nix. Auto-updates have thus been disabled."; }; + ZED_UPDATE_EXPLANATION = "Zed has been installed using Nix. Auto-updates have thus been disabled."; + RELEASE_VERSION = version; + }; - cargoArtifacts = craneLib.buildDepsOnly commonArgs; + RUSTFLAGS = if withGLES then "--cfg gles" else ""; + gpu-lib = if withGLES then libglvnd else vulkan-loader; - gpu-lib = - if withGLES - then libglvnd - else vulkan-loader; + preBuild = '' + bash script/generate-licenses + ''; - zed = craneLib.buildPackage (commonArgs - // { - inherit cargoArtifacts; - cargoExtraArgs = "--package=zed --package=cli"; - buildFeatures = ["gpui/runtime_shaders"]; - doCheck = false; + postFixup = lib.optionalString stdenv.hostPlatform.isLinux '' + patchelf --add-rpath ${gpu-lib}/lib $out/libexec/* + patchelf --add-rpath ${wayland}/lib $out/libexec/* + wrapProgram $out/libexec/zed-editor --suffix PATH : ${lib.makeBinPath [ nodejs_22 ]} + ''; - RUSTFLAGS = - if withGLES - then "--cfg gles" - else ""; + preCheck = '' + export HOME=$(mktemp -d); + ''; - postFixup = '' - patchelf --add-rpath ${gpu-lib}/lib $out/libexec/* - patchelf --add-rpath ${wayland}/lib $out/libexec/* - ''; + checkFlags = + [ + # Flaky: unreliably fails on certain hosts (including Hydra) + "--skip=zed::tests::test_window_edit_state_restoring_enabled" + ] + ++ lib.optionals stdenv.hostPlatform.isLinux [ + # Fails on certain hosts (including Hydra) for unclear reason + "--skip=test_open_paths_action" + ]; + + installPhase = + if stdenv.hostPlatform.isDarwin then + '' + runHook preInstall + + # cargo-bundle expects the binary in target/release + mv target/${stdenv.hostPlatform.rust.cargoShortTarget}/release/zed target/release/zed + + pushd crates/zed + + # Note that this is GNU sed, while Zed's bundle-mac uses BSD sed + sed -i "s/package.metadata.bundle-stable/package.metadata.bundle/" Cargo.toml + export CARGO_BUNDLE_SKIP_BUILD=true + app_path=$(cargo bundle --release | xargs) + + # We're not using the fork of cargo-bundle, so we must manually append plist extensions + # Remove closing tags from Info.plist (last two lines) + head -n -2 $app_path/Contents/Info.plist > Info.plist + # Append extensions + cat resources/info/*.plist >> Info.plist + # Add closing tags + printf "\n\n" >> Info.plist + mv Info.plist $app_path/Contents/Info.plist + + popd + + mkdir -p $out/Applications $out/bin + # Zed expects git next to its own binary + ln -s ${git}/bin/git $app_path/Contents/MacOS/git + mv target/${stdenv.hostPlatform.rust.cargoShortTarget}/release/cli $app_path/Contents/MacOS/cli + mv $app_path $out/Applications/ + + # Physical location of the CLI must be inside the app bundle as this is used + # to determine which app to start + ln -s $out/Applications/Zed.app/Contents/MacOS/cli $out/bin/zed + + runHook postInstall + '' + else + '' + runHook preInstall - postInstall = '' mkdir -p $out/bin $out/libexec - mv $out/bin/zed $out/libexec/zed-editor - mv $out/bin/cli $out/bin/zed + cp target/${stdenv.hostPlatform.rust.cargoShortTarget}/release/zed $out/libexec/zed-editor + cp target/${stdenv.hostPlatform.rust.cargoShortTarget}/release/cli $out/bin/zed - install -D crates/zed/resources/app-icon@2x.png $out/share/icons/hicolor/1024x1024@2x/apps/zed.png - install -D crates/zed/resources/app-icon.png $out/share/icons/hicolor/512x512/apps/zed.png + install -D ${src}/crates/zed/resources/app-icon@2x.png $out/share/icons/hicolor/1024x1024@2x/apps/zed.png + install -D ${src}/crates/zed/resources/app-icon.png $out/share/icons/hicolor/512x512/apps/zed.png - export DO_STARTUP_NOTIFY="true" - export APP_CLI="zed" - export APP_ICON="zed" - export APP_NAME="Zed" - export APP_ARGS="%U" - mkdir -p "$out/share/applications" - ${lib.getExe envsubst} < "crates/zed/resources/zed.desktop.in" > "$out/share/applications/dev.zed.Zed.desktop" + # extracted from https://github.com/zed-industries/zed/blob/v0.141.2/script/bundle-linux (envsubst) + # and https://github.com/zed-industries/zed/blob/v0.141.2/script/install.sh (final desktop file name) + ( + export DO_STARTUP_NOTIFY="true" + export APP_CLI="zed" + export APP_ICON="zed" + export APP_NAME="Zed" + export APP_ARGS="%U" + mkdir -p "$out/share/applications" + ${lib.getExe envsubst} < "crates/zed/resources/zed.desktop.in" > "$out/share/applications/dev.zed.Zed.desktop" + ) + + runHook postInstall ''; - }); -in - zed - // { - meta = with lib; { - description = "High-performance, multiplayer code editor from the creators of Atom and Tree-sitter"; - homepage = "https://zed.dev"; - changelog = "https://zed.dev/releases/preview"; - license = licenses.gpl3Only; - mainProgram = "zed"; - platforms = platforms.linux; - }; - } + + nativeInstallCheckInputs = [ + versionCheckHook + ]; + + meta = { + description = "High-performance, multiplayer code editor from the creators of Atom and Tree-sitter"; + homepage = "https://zed.dev"; + changelog = "https://zed.dev/releases/preview"; + license = lib.licenses.gpl3Only; + mainProgram = "zed"; + platforms = lib.platforms.linux ++ lib.platforms.darwin; + }; +} diff --git a/nix/shell.nix b/nix/shell.nix index e0b4018778..75ceb0d8e3 100644 --- a/nix/shell.nix +++ b/nix/shell.nix @@ -1,51 +1,57 @@ -{pkgs ? import {}}: let - stdenv = pkgs.stdenvAdapters.useMoldLinker pkgs.llvmPackages_18.stdenv; +{ + pkgs ? import { }, +}: +let + inherit (pkgs) lib; in - if pkgs.stdenv.isDarwin - then - # See https://github.com/NixOS/nixpkgs/issues/320084 - throw "zed: nix dev-shell isn't supported on darwin yet." - else let - buildInputs = with pkgs; [ - curl - fontconfig - freetype - libgit2 - openssl - sqlite - zlib - zstd - alsa-lib - libxkbcommon - wayland - xorg.libxcb - vulkan-loader - rustToolchain +pkgs.mkShell rec { + packages = [ + pkgs.clang + pkgs.curl + pkgs.cmake + pkgs.perl + pkgs.pkg-config + pkgs.protobuf + pkgs.rustPlatform.bindgenHook + pkgs.rust-analyzer + ]; + + buildInputs = + [ + pkgs.curl + pkgs.fontconfig + pkgs.freetype + pkgs.libgit2 + pkgs.openssl + pkgs.sqlite + pkgs.zlib + pkgs.zstd + pkgs.rustToolchain + ] + ++ lib.optionals pkgs.stdenv.hostPlatform.isLinux [ + pkgs.alsa-lib + pkgs.libxkbcommon + ] + ++ lib.optional pkgs.stdenv.hostPlatform.isDarwin pkgs.apple-sdk_15; + + # We set SDKROOT and DEVELOPER_DIR to the Xcode ones instead of the nixpkgs ones, + # because we need Swift 6.0 and nixpkgs doesn't have it. + # Xcode is required for development anyways + shellHook = + '' + export LD_LIBRARY_PATH="${lib.makeLibraryPath buildInputs}:$LD_LIBRARY_PATH" + export PROTOC="${pkgs.protobuf}/bin/protoc" + '' + + lib.optionalString pkgs.stdenv.hostPlatform.isDarwin '' + export SDKROOT="/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk"; + export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"; + ''; + + FONTCONFIG_FILE = pkgs.makeFontsConf { + fontDirectories = [ + "./assets/fonts/zed-mono" + "./assets/fonts/zed-sans" ]; - in - pkgs.mkShell.override {inherit stdenv;} { - nativeBuildInputs = with pkgs; [ - clang - curl - cmake - perl - pkg-config - protobuf - rustPlatform.bindgenHook - ]; - - inherit buildInputs; - - shellHook = '' - export LD_LIBRARY_PATH="${pkgs.lib.makeLibraryPath buildInputs}:$LD_LIBRARY_PATH" - export PROTOC="${pkgs.protobuf}/bin/protoc" - ''; - - FONTCONFIG_FILE = pkgs.makeFontsConf { - fontDirectories = [ - "./assets/fonts/zed-mono" - "./assets/fonts/zed-sans" - ]; - }; - ZSTD_SYS_USE_PKG_CONFIG = true; - } + }; + ZSTD_SYS_USE_PKG_CONFIG = true; +} From d0bafce86bf94c3ddafae865896ae31cf89711e9 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 27 Nov 2024 20:22:39 +0200 Subject: [PATCH 170/886] Allow splitting the terminal panel (#21238) Closes https://github.com/zed-industries/zed/issues/4351 ![it_splits](https://github.com/user-attachments/assets/40de03c9-2173-4441-ba96-8e91537956e0) Applies the same splitting mechanism, as Zed's central pane has, to the terminal panel. Similar navigation, splitting and (de)serialization capabilities are supported. Notable caveats: * zooming keeps the terminal splits' ratio, rather expanding the terminal pane * on macOs, central panel is split with `cmd-k up/down/etc.` but `cmd-k` is a "standard" terminal clearing keybinding on macOS, so terminal panel splitting is done via `ctrl-k up/down/etc.` * task terminals are "split" into regular terminals, and also not persisted (same as currently in the terminal) Seems ok for the initial version, we can revisit and polish things later. Release Notes: - Added the ability to split the terminal panel --- Cargo.lock | 1 + assets/keymaps/default-macos.json | 6 +- crates/assistant/src/assistant_panel.rs | 1 - crates/editor/src/items.rs | 14 +- crates/gpui/src/app.rs | 2 +- crates/gpui/src/elements/div.rs | 39 +- crates/gpui/src/text_system/line_layout.rs | 26 +- crates/gpui/src/window.rs | 20 +- crates/image_viewer/src/image_viewer.rs | 6 +- crates/terminal_view/Cargo.toml | 1 + crates/terminal_view/src/persistence.rs | 345 +++++++++- crates/terminal_view/src/terminal_panel.rs | 696 +++++++++++++-------- crates/terminal_view/src/terminal_view.rs | 10 +- crates/workspace/src/item.rs | 7 +- crates/workspace/src/pane.rs | 32 +- crates/workspace/src/pane_group.rs | 50 +- crates/workspace/src/workspace.rs | 45 +- 17 files changed, 953 insertions(+), 348 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d9da330daa..9e1354c40d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12418,6 +12418,7 @@ name = "terminal_view" version = "0.1.0" dependencies = [ "anyhow", + "async-recursion 1.1.1", "breadcrumbs", "client", "collections", diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 514604ef98..f3990cecee 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -732,7 +732,11 @@ "cmd-end": "terminal::ScrollToBottom", "shift-home": "terminal::ScrollToTop", "shift-end": "terminal::ScrollToBottom", - "ctrl-shift-space": "terminal::ToggleViMode" + "ctrl-shift-space": "terminal::ToggleViMode", + "ctrl-k up": "pane::SplitUp", + "ctrl-k down": "pane::SplitDown", + "ctrl-k left": "pane::SplitLeft", + "ctrl-k right": "pane::SplitRight" } } ] diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 7467d5dfd4..79e026cb51 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -416,7 +416,6 @@ impl AssistantPanel { ControlFlow::Break(()) }); - pane.set_can_split(false, cx); pane.set_can_navigate(true, cx); pane.display_nav_history_buttons(None); pane.set_should_display_tab_bar(|_| true); diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 51ad9b9dec..813b212761 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -47,7 +47,7 @@ use workspace::item::{BreadcrumbText, FollowEvent}; use workspace::{ item::{FollowableItem, Item, ItemEvent, ProjectItem}, searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle}, - ItemId, ItemNavHistory, Pane, ToolbarItemLocation, ViewId, Workspace, WorkspaceId, + ItemId, ItemNavHistory, ToolbarItemLocation, ViewId, Workspace, WorkspaceId, }; pub const MAX_TAB_TITLE_LEN: usize = 24; @@ -954,7 +954,7 @@ impl SerializableItem for Editor { workspace: WeakView, workspace_id: workspace::WorkspaceId, item_id: ItemId, - cx: &mut ViewContext, + cx: &mut WindowContext, ) -> Task>> { let serialized_editor = match DB .get_serialized_editor(item_id, workspace_id) @@ -989,7 +989,7 @@ impl SerializableItem for Editor { contents: Some(contents), language, .. - } => cx.spawn(|pane, mut cx| { + } => cx.spawn(|mut cx| { let project = project.clone(); async move { let language = if let Some(language_name) = language { @@ -1019,7 +1019,7 @@ impl SerializableItem for Editor { buffer.set_text(contents, cx); })?; - pane.update(&mut cx, |_, cx| { + cx.update(|cx| { cx.new_view(|cx| { let mut editor = Editor::for_buffer(buffer, Some(project), cx); @@ -1046,7 +1046,7 @@ impl SerializableItem for Editor { match project_item { Some(project_item) => { - cx.spawn(|pane, mut cx| async move { + cx.spawn(|mut cx| async move { let (_, project_item) = project_item.await?; let buffer = project_item.downcast::().map_err(|_| { anyhow!("Project item at stored path was not a buffer") @@ -1073,7 +1073,7 @@ impl SerializableItem for Editor { })?; } - pane.update(&mut cx, |_, cx| { + cx.update(|cx| { cx.new_view(|cx| { let mut editor = Editor::for_buffer(buffer, Some(project), cx); @@ -1087,7 +1087,7 @@ impl SerializableItem for Editor { let open_by_abs_path = workspace.update(cx, |workspace, cx| { workspace.open_abs_path(abs_path.clone(), false, cx) }); - cx.spawn(|_, mut cx| async move { + cx.spawn(|mut cx| async move { let editor = open_by_abs_path?.await?.downcast::().with_context(|| format!("Failed to downcast to Editor after opening abs path {abs_path:?}"))?; editor.update(&mut cx, |editor, cx| { editor.read_scroll_position_from_db(item_id, workspace_id, cx); diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 0776e5c72e..87ee3942dd 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1578,7 +1578,7 @@ pub struct AnyDrag { pub view: AnyView, /// The value of the dragged item, to be dropped - pub value: Box, + pub value: Arc, /// This is used to render the dragged item in the same place /// on the original element that the drag was initiated diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index 6928ca74ee..909af004a5 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -35,6 +35,7 @@ use std::{ mem, ops::DerefMut, rc::Rc, + sync::Arc, time::Duration, }; use taffy::style::Overflow; @@ -61,6 +62,7 @@ pub struct DragMoveEvent { /// The bounds of this element. pub bounds: Bounds, drag: PhantomData, + dragged_item: Arc, } impl DragMoveEvent { @@ -71,6 +73,11 @@ impl DragMoveEvent { .and_then(|drag| drag.value.downcast_ref::()) .expect("DragMoveEvent is only valid when the stored active drag is of the same type.") } + + /// An item that is about to be dropped. + pub fn dragged_item(&self) -> &dyn Any { + self.dragged_item.as_ref() + } } impl Interactivity { @@ -243,20 +250,20 @@ impl Interactivity { { self.mouse_move_listeners .push(Box::new(move |event, phase, hitbox, cx| { - if phase == DispatchPhase::Capture - && cx - .active_drag - .as_ref() - .is_some_and(|drag| drag.value.as_ref().type_id() == TypeId::of::()) - { - (listener)( - &DragMoveEvent { - event: event.clone(), - bounds: hitbox.bounds, - drag: PhantomData, - }, - cx, - ); + if phase == DispatchPhase::Capture { + if let Some(drag) = &cx.active_drag { + if drag.value.as_ref().type_id() == TypeId::of::() { + (listener)( + &DragMoveEvent { + event: event.clone(), + bounds: hitbox.bounds, + drag: PhantomData, + dragged_item: Arc::clone(&drag.value), + }, + cx, + ); + } + } } })); } @@ -454,7 +461,7 @@ impl Interactivity { "calling on_drag more than once on the same element is not supported" ); self.drag_listener = Some(( - Box::new(value), + Arc::new(value), Box::new(move |value, offset, cx| { constructor(value.downcast_ref().unwrap(), offset, cx).into() }), @@ -1292,7 +1299,7 @@ pub struct Interactivity { pub(crate) drop_listeners: Vec<(TypeId, DropListener)>, pub(crate) can_drop_predicate: Option, pub(crate) click_listeners: Vec, - pub(crate) drag_listener: Option<(Box, DragListener)>, + pub(crate) drag_listener: Option<(Arc, DragListener)>, pub(crate) hover_listener: Option>, pub(crate) tooltip_builder: Option, pub(crate) occlude_mouse: bool, diff --git a/crates/gpui/src/text_system/line_layout.rs b/crates/gpui/src/text_system/line_layout.rs index 66eb914a30..13a7896a3f 100644 --- a/crates/gpui/src/text_system/line_layout.rs +++ b/crates/gpui/src/text_system/line_layout.rs @@ -385,20 +385,28 @@ impl LineLayoutCache { let mut previous_frame = &mut *self.previous_frame.lock(); let mut current_frame = &mut *self.current_frame.write(); - for key in &previous_frame.used_lines[range.start.lines_index..range.end.lines_index] { - if let Some((key, line)) = previous_frame.lines.remove_entry(key) { - current_frame.lines.insert(key, line); + if let Some(cached_keys) = previous_frame + .used_lines + .get(range.start.lines_index..range.end.lines_index) + { + for key in cached_keys { + if let Some((key, line)) = previous_frame.lines.remove_entry(key) { + current_frame.lines.insert(key, line); + } + current_frame.used_lines.push(key.clone()); } - current_frame.used_lines.push(key.clone()); } - for key in &previous_frame.used_wrapped_lines - [range.start.wrapped_lines_index..range.end.wrapped_lines_index] + if let Some(cached_keys) = previous_frame + .used_wrapped_lines + .get(range.start.wrapped_lines_index..range.end.wrapped_lines_index) { - if let Some((key, line)) = previous_frame.wrapped_lines.remove_entry(key) { - current_frame.wrapped_lines.insert(key, line); + for key in cached_keys { + if let Some((key, line)) = previous_frame.wrapped_lines.remove_entry(key) { + current_frame.wrapped_lines.insert(key, line); + } + current_frame.used_wrapped_lines.push(key.clone()); } - current_frame.used_wrapped_lines.push(key.clone()); } } diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index c1c14edba2..902c699cb7 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -1752,12 +1752,18 @@ impl<'a> WindowContext<'a> { .iter_mut() .map(|listener| listener.take()), ); - window.next_frame.accessed_element_states.extend( - window.rendered_frame.accessed_element_states[range.start.accessed_element_states_index - ..range.end.accessed_element_states_index] - .iter() - .map(|(id, type_id)| (GlobalElementId(id.0.clone()), *type_id)), - ); + if let Some(element_states) = window + .rendered_frame + .accessed_element_states + .get(range.start.accessed_element_states_index..range.end.accessed_element_states_index) + { + window.next_frame.accessed_element_states.extend( + element_states + .iter() + .map(|(id, type_id)| (GlobalElementId(id.0.clone()), *type_id)), + ); + } + window .text_system .reuse_layouts(range.start.line_layout_index..range.end.line_layout_index); @@ -3126,7 +3132,7 @@ impl<'a> WindowContext<'a> { self.window.mouse_position = position; if self.active_drag.is_none() { self.active_drag = Some(AnyDrag { - value: Box::new(paths.clone()), + value: Arc::new(paths.clone()), view: self.new_view(|_| paths).into(), cursor_offset: position, }); diff --git a/crates/image_viewer/src/image_viewer.rs b/crates/image_viewer/src/image_viewer.rs index 1d03e77e76..ed87562e64 100644 --- a/crates/image_viewer/src/image_viewer.rs +++ b/crates/image_viewer/src/image_viewer.rs @@ -16,7 +16,7 @@ use settings::Settings; use util::paths::PathExt; use workspace::{ item::{BreadcrumbText, Item, ProjectItem, SerializableItem, TabContentParams}, - ItemId, ItemSettings, Pane, ToolbarItemLocation, Workspace, WorkspaceId, + ItemId, ItemSettings, ToolbarItemLocation, Workspace, WorkspaceId, }; const IMAGE_VIEWER_KIND: &str = "ImageView"; @@ -172,9 +172,9 @@ impl SerializableItem for ImageView { _workspace: WeakView, workspace_id: WorkspaceId, item_id: ItemId, - cx: &mut ViewContext, + cx: &mut WindowContext, ) -> Task>> { - cx.spawn(|_pane, mut cx| async move { + cx.spawn(|mut cx| async move { let image_path = IMAGE_VIEWER .get_image_path(item_id, workspace_id)? .ok_or_else(|| anyhow::anyhow!("No image path found"))?; diff --git a/crates/terminal_view/Cargo.toml b/crates/terminal_view/Cargo.toml index e57d9d1fc6..7e4a4fe76f 100644 --- a/crates/terminal_view/Cargo.toml +++ b/crates/terminal_view/Cargo.toml @@ -14,6 +14,7 @@ doctest = false [dependencies] anyhow.workspace = true +async-recursion.workspace = true breadcrumbs.workspace = true collections.workspace = true db.workspace = true diff --git a/crates/terminal_view/src/persistence.rs b/crates/terminal_view/src/persistence.rs index b8c31e05b0..dd430963d2 100644 --- a/crates/terminal_view/src/persistence.rs +++ b/crates/terminal_view/src/persistence.rs @@ -1,8 +1,351 @@ use anyhow::Result; +use async_recursion::async_recursion; +use collections::HashSet; +use futures::{stream::FuturesUnordered, StreamExt as _}; +use gpui::{AsyncWindowContext, Axis, Model, Task, View, WeakView}; +use project::{terminals::TerminalKind, Project}; +use serde::{Deserialize, Serialize}; use std::path::PathBuf; +use ui::{Pixels, ViewContext, VisualContext as _, WindowContext}; +use util::ResultExt as _; use db::{define_connection, query, sqlez::statement::Statement, sqlez_macros::sql}; -use workspace::{ItemId, WorkspaceDb, WorkspaceId}; +use workspace::{ + ItemHandle, ItemId, Member, Pane, PaneAxis, PaneGroup, SerializableItem as _, Workspace, + WorkspaceDb, WorkspaceId, +}; + +use crate::{ + default_working_directory, + terminal_panel::{new_terminal_pane, TerminalPanel}, + TerminalView, +}; + +pub(crate) fn serialize_pane_group( + pane_group: &PaneGroup, + active_pane: &View, + cx: &WindowContext, +) -> SerializedPaneGroup { + build_serialized_pane_group(&pane_group.root, active_pane, cx) +} + +fn build_serialized_pane_group( + pane_group: &Member, + active_pane: &View, + cx: &WindowContext, +) -> SerializedPaneGroup { + match pane_group { + Member::Axis(PaneAxis { + axis, + members, + flexes, + bounding_boxes: _, + }) => SerializedPaneGroup::Group { + axis: SerializedAxis(*axis), + children: members + .iter() + .map(|member| build_serialized_pane_group(member, active_pane, cx)) + .collect::>(), + flexes: Some(flexes.lock().clone()), + }, + Member::Pane(pane_handle) => { + SerializedPaneGroup::Pane(serialize_pane(pane_handle, pane_handle == active_pane, cx)) + } + } +} + +fn serialize_pane(pane: &View, active: bool, cx: &WindowContext) -> SerializedPane { + let mut items_to_serialize = HashSet::default(); + let pane = pane.read(cx); + let children = pane + .items() + .filter_map(|item| { + let terminal_view = item.act_as::(cx)?; + if terminal_view.read(cx).terminal().read(cx).task().is_some() { + None + } else { + let id = item.item_id().as_u64(); + items_to_serialize.insert(id); + Some(id) + } + }) + .collect::>(); + let active_item = pane + .active_item() + .map(|item| item.item_id().as_u64()) + .filter(|active_id| items_to_serialize.contains(active_id)); + + SerializedPane { + active, + children, + active_item, + } +} + +pub(crate) fn deserialize_terminal_panel( + workspace: WeakView, + project: Model, + database_id: WorkspaceId, + serialized_panel: SerializedTerminalPanel, + cx: &mut WindowContext, +) -> Task>> { + cx.spawn(move |mut cx| async move { + let terminal_panel = workspace.update(&mut cx, |workspace, cx| { + cx.new_view(|cx| { + let mut panel = TerminalPanel::new(workspace, cx); + panel.height = serialized_panel.height.map(|h| h.round()); + panel.width = serialized_panel.width.map(|w| w.round()); + panel + }) + })?; + match &serialized_panel.items { + SerializedItems::NoSplits(item_ids) => { + let items = deserialize_terminal_views( + database_id, + project, + workspace, + item_ids.as_slice(), + &mut cx, + ) + .await; + let active_item = serialized_panel.active_item_id; + terminal_panel.update(&mut cx, |terminal_panel, cx| { + terminal_panel.active_pane.update(cx, |pane, cx| { + populate_pane_items(pane, items, active_item, cx); + }); + })?; + } + SerializedItems::WithSplits(serialized_pane_group) => { + let center_pane = deserialize_pane_group( + workspace, + project, + terminal_panel.clone(), + database_id, + serialized_pane_group, + &mut cx, + ) + .await; + if let Some((center_group, active_pane)) = center_pane { + terminal_panel.update(&mut cx, |terminal_panel, _| { + terminal_panel.center = PaneGroup::with_root(center_group); + terminal_panel.active_pane = + active_pane.unwrap_or_else(|| terminal_panel.center.first_pane()); + })?; + } + } + } + + Ok(terminal_panel) + }) +} + +fn populate_pane_items( + pane: &mut Pane, + items: Vec>, + active_item: Option, + cx: &mut ViewContext<'_, Pane>, +) { + let mut item_index = pane.items_len(); + for item in items { + let activate_item = Some(item.item_id().as_u64()) == active_item; + pane.add_item(Box::new(item), false, false, None, cx); + item_index += 1; + if activate_item { + pane.activate_item(item_index, false, false, cx); + } + } +} + +#[async_recursion(?Send)] +async fn deserialize_pane_group( + workspace: WeakView, + project: Model, + panel: View, + workspace_id: WorkspaceId, + serialized: &SerializedPaneGroup, + cx: &mut AsyncWindowContext, +) -> Option<(Member, Option>)> { + match serialized { + SerializedPaneGroup::Group { + axis, + flexes, + children, + } => { + let mut current_active_pane = None; + let mut members = Vec::new(); + for child in children { + if let Some((new_member, active_pane)) = deserialize_pane_group( + workspace.clone(), + project.clone(), + panel.clone(), + workspace_id, + child, + cx, + ) + .await + { + members.push(new_member); + current_active_pane = current_active_pane.or(active_pane); + } + } + + if members.is_empty() { + return None; + } + + if members.len() == 1 { + return Some((members.remove(0), current_active_pane)); + } + + Some(( + Member::Axis(PaneAxis::load(axis.0, members, flexes.clone())), + current_active_pane, + )) + } + SerializedPaneGroup::Pane(serialized_pane) => { + let active = serialized_pane.active; + let new_items = deserialize_terminal_views( + workspace_id, + project.clone(), + workspace.clone(), + serialized_pane.children.as_slice(), + cx, + ) + .await; + + let pane = panel + .update(cx, |_, cx| { + new_terminal_pane(workspace.clone(), project.clone(), cx) + }) + .log_err()?; + let active_item = serialized_pane.active_item; + pane.update(cx, |pane, cx| { + populate_pane_items(pane, new_items, active_item, cx); + // Avoid blank panes in splits + if pane.items_len() == 0 { + let working_directory = workspace + .update(cx, |workspace, cx| default_working_directory(workspace, cx)) + .ok() + .flatten(); + let kind = TerminalKind::Shell(working_directory); + let window = cx.window_handle(); + let terminal = project + .update(cx, |project, cx| project.create_terminal(kind, window, cx)) + .log_err()?; + let terminal_view = Box::new(cx.new_view(|cx| { + TerminalView::new( + terminal.clone(), + workspace.clone(), + Some(workspace_id), + cx, + ) + })); + pane.add_item(terminal_view, true, false, None, cx); + } + Some(()) + }) + .ok() + .flatten()?; + Some((Member::Pane(pane.clone()), active.then_some(pane))) + } + } +} + +async fn deserialize_terminal_views( + workspace_id: WorkspaceId, + project: Model, + workspace: WeakView, + item_ids: &[u64], + cx: &mut AsyncWindowContext, +) -> Vec> { + let mut items = Vec::with_capacity(item_ids.len()); + let mut deserialized_items = item_ids + .iter() + .map(|item_id| { + cx.update(|cx| { + TerminalView::deserialize( + project.clone(), + workspace.clone(), + workspace_id, + *item_id, + cx, + ) + }) + .unwrap_or_else(|e| Task::ready(Err(e.context("no window present")))) + }) + .collect::>(); + while let Some(item) = deserialized_items.next().await { + if let Some(item) = item.log_err() { + items.push(item); + } + } + items +} + +#[derive(Debug, Serialize, Deserialize)] +pub(crate) struct SerializedTerminalPanel { + pub items: SerializedItems, + // A deprecated field, kept for backwards compatibility for the code before terminal splits were introduced. + pub active_item_id: Option, + pub width: Option, + pub height: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(untagged)] +pub(crate) enum SerializedItems { + // The data stored before terminal splits were introduced. + NoSplits(Vec), + WithSplits(SerializedPaneGroup), +} + +#[derive(Debug, Serialize, Deserialize)] +pub(crate) enum SerializedPaneGroup { + Pane(SerializedPane), + Group { + axis: SerializedAxis, + flexes: Option>, + children: Vec, + }, +} + +#[derive(Debug, Serialize, Deserialize)] +pub(crate) struct SerializedPane { + pub active: bool, + pub children: Vec, + pub active_item: Option, +} + +#[derive(Debug)] +pub(crate) struct SerializedAxis(pub Axis); + +impl Serialize for SerializedAxis { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self.0 { + Axis::Horizontal => serializer.serialize_str("horizontal"), + Axis::Vertical => serializer.serialize_str("vertical"), + } + } +} + +impl<'de> Deserialize<'de> for SerializedAxis { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + match s.as_str() { + "horizontal" => Ok(SerializedAxis(Axis::Horizontal)), + "vertical" => Ok(SerializedAxis(Axis::Vertical)), + invalid => Err(serde::de::Error::custom(format!( + "Invalid axis value: '{invalid}'" + ))), + } + } +} define_connection! { pub static ref TERMINAL_DB: TerminalDb = diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index ee10e924f4..38b2eda676 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -1,19 +1,24 @@ -use std::{ops::ControlFlow, path::PathBuf, sync::Arc}; +use std::{cmp, ops::ControlFlow, path::PathBuf, sync::Arc, time::Duration}; -use crate::{default_working_directory, TerminalView}; +use crate::{ + default_working_directory, + persistence::{ + deserialize_terminal_panel, serialize_pane_group, SerializedItems, SerializedTerminalPanel, + }, + TerminalView, +}; use breadcrumbs::Breadcrumbs; -use collections::{HashMap, HashSet}; +use collections::HashMap; use db::kvp::KEY_VALUE_STORE; use futures::future::join_all; use gpui::{ - actions, Action, AnchorCorner, AnyView, AppContext, AsyncWindowContext, Entity, EventEmitter, + actions, Action, AnchorCorner, AnyView, AppContext, AsyncWindowContext, EventEmitter, ExternalPaths, FocusHandle, FocusableView, IntoElement, Model, ParentElement, Pixels, Render, - Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowContext, + Styled, Task, View, ViewContext, VisualContext, WeakView, WindowContext, }; use itertools::Itertools; -use project::{terminals::TerminalKind, Fs, ProjectEntryId}; +use project::{terminals::TerminalKind, Fs, Project, ProjectEntryId}; use search::{buffer_search::DivRegistrar, BufferSearchBar}; -use serde::{Deserialize, Serialize}; use settings::Settings; use task::{RevealStrategy, Shell, SpawnInTerminal, TaskId}; use terminal::{ @@ -21,16 +26,18 @@ use terminal::{ Terminal, }; use ui::{ - h_flex, ButtonCommon, Clickable, ContextMenu, IconButton, IconSize, PopoverMenu, Selectable, - Tooltip, + div, h_flex, ButtonCommon, Clickable, ContextMenu, IconButton, IconSize, InteractiveElement, + PopoverMenu, Selectable, Tooltip, }; use util::{ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, item::SerializableItem, - pane, + move_item, pane, ui::IconName, - DraggedTab, ItemId, NewTerminal, Pane, ToggleZoom, Workspace, + ActivateNextPane, ActivatePane, ActivatePaneInDirection, ActivatePreviousPane, DraggedTab, + ItemId, NewTerminal, Pane, PaneGroup, SplitDirection, SwapPaneInDirection, ToggleZoom, + Workspace, }; use anyhow::Result; @@ -60,14 +67,14 @@ pub fn init(cx: &mut AppContext) { } pub struct TerminalPanel { - pane: View, + pub(crate) active_pane: View, + pub(crate) center: PaneGroup, fs: Arc, workspace: WeakView, - width: Option, - height: Option, + pub(crate) width: Option, + pub(crate) height: Option, pending_serialization: Task>, pending_terminals_to_add: usize, - _subscriptions: Vec, deferred_tasks: HashMap>, enabled: bool, assistant_enabled: bool, @@ -75,85 +82,14 @@ pub struct TerminalPanel { } impl TerminalPanel { - fn new(workspace: &Workspace, cx: &mut ViewContext) -> Self { - let pane = cx.new_view(|cx| { - let mut pane = Pane::new( - workspace.weak_handle(), - workspace.project().clone(), - Default::default(), - None, - NewTerminal.boxed_clone(), - cx, - ); - pane.set_can_split(false, cx); - pane.set_can_navigate(false, cx); - pane.display_nav_history_buttons(None); - pane.set_should_display_tab_bar(|_| true); - - let is_local = workspace.project().read(cx).is_local(); - let workspace = workspace.weak_handle(); - pane.set_custom_drop_handle(cx, move |pane, dropped_item, cx| { - if let Some(tab) = dropped_item.downcast_ref::() { - let item = if &tab.pane == cx.view() { - pane.item_for_index(tab.ix) - } else { - tab.pane.read(cx).item_for_index(tab.ix) - }; - if let Some(item) = item { - if item.downcast::().is_some() { - return ControlFlow::Continue(()); - } else if let Some(project_path) = item.project_path(cx) { - if let Some(entry_path) = workspace - .update(cx, |workspace, cx| { - workspace - .project() - .read(cx) - .absolute_path(&project_path, cx) - }) - .log_err() - .flatten() - { - add_paths_to_terminal(pane, &[entry_path], cx); - } - } - } - } else if let Some(&entry_id) = dropped_item.downcast_ref::() { - if let Some(entry_path) = workspace - .update(cx, |workspace, cx| { - let project = workspace.project().read(cx); - project - .path_for_entry(entry_id, cx) - .and_then(|project_path| project.absolute_path(&project_path, cx)) - }) - .log_err() - .flatten() - { - add_paths_to_terminal(pane, &[entry_path], cx); - } - } else if is_local { - if let Some(paths) = dropped_item.downcast_ref::() { - add_paths_to_terminal(pane, paths.paths(), cx); - } - } - - ControlFlow::Break(()) - }); - let buffer_search_bar = cx.new_view(search::BufferSearchBar::new); - 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![ - cx.observe(&pane, |_, _, cx| cx.notify()), - cx.subscribe(&pane, Self::handle_pane_event), - ]; - let project = workspace.project().read(cx); - let enabled = project.supports_terminal(cx); - let this = Self { - pane, + pub fn new(workspace: &Workspace, cx: &mut ViewContext) -> Self { + let project = workspace.project(); + let pane = new_terminal_pane(workspace.weak_handle(), project.clone(), cx); + let center = PaneGroup::new(pane.clone()); + let enabled = project.read(cx).supports_terminal(cx); + let terminal_panel = Self { + center, + active_pane: pane, fs: workspace.app_state().fs.clone(), workspace: workspace.weak_handle(), pending_serialization: Task::ready(None), @@ -161,20 +97,19 @@ impl TerminalPanel { height: None, pending_terminals_to_add: 0, deferred_tasks: HashMap::default(), - _subscriptions: subscriptions, enabled, assistant_enabled: false, assistant_tab_bar_button: None, }; - this.apply_tab_bar_buttons(cx); - this + terminal_panel.apply_tab_bar_buttons(&terminal_panel.active_pane, cx); + terminal_panel } pub fn asssistant_enabled(&mut self, enabled: bool, cx: &mut ViewContext) { self.assistant_enabled = enabled; if enabled { let focus_handle = self - .pane + .active_pane .read(cx) .active_item() .map(|item| item.focus_handle(cx)) @@ -186,12 +121,14 @@ impl TerminalPanel { } else { self.assistant_tab_bar_button = None; } - self.apply_tab_bar_buttons(cx); + for pane in self.center.panes() { + self.apply_tab_bar_buttons(pane, cx); + } } - fn apply_tab_bar_buttons(&self, cx: &mut ViewContext) { + fn apply_tab_bar_buttons(&self, terminal_pane: &View, cx: &mut ViewContext) { let assistant_tab_bar_button = self.assistant_tab_bar_button.clone(); - self.pane.update(cx, |pane, cx| { + terminal_pane.update(cx, |pane, cx| { pane.set_render_tab_bar_buttons(cx, move |pane, cx| { if !pane.has_focus(cx) && !pane.context_menu_focused(cx) { return (None, None); @@ -268,80 +205,45 @@ impl TerminalPanel { .log_err() .flatten(); - let (panel, pane, items) = workspace.update(&mut cx, |workspace, cx| { - let panel = cx.new_view(|cx| TerminalPanel::new(workspace, cx)); - let items = if let Some((serialized_panel, database_id)) = - serialized_panel.as_ref().zip(workspace.database_id()) - { - panel.update(cx, |panel, cx| { - cx.notify(); - panel.height = serialized_panel.height.map(|h| h.round()); - panel.width = serialized_panel.width.map(|w| w.round()); - panel.pane.update(cx, |_, cx| { - serialized_panel - .items - .iter() - .map(|item_id| { - TerminalView::deserialize( - workspace.project().clone(), - workspace.weak_handle(), - database_id, - *item_id, - cx, - ) - }) - .collect::>() - }) - }) - } else { - Vec::new() - }; - let pane = panel.read(cx).pane.clone(); - (panel, pane, items) - })?; + let terminal_panel = workspace + .update(&mut cx, |workspace, cx| { + match serialized_panel.zip(workspace.database_id()) { + Some((serialized_panel, database_id)) => deserialize_terminal_panel( + workspace.weak_handle(), + workspace.project().clone(), + database_id, + serialized_panel, + cx, + ), + None => Task::ready(Ok(cx.new_view(|cx| TerminalPanel::new(workspace, cx)))), + } + })? + .await?; if let Some(workspace) = workspace.upgrade() { - panel - .update(&mut cx, |panel, cx| { - panel._subscriptions.push(cx.subscribe( - &workspace, - |terminal_panel, _, e, cx| { - if let workspace::Event::SpawnTask(spawn_in_terminal) = e { - terminal_panel.spawn_task(spawn_in_terminal, cx); - }; - }, - )) + terminal_panel + .update(&mut cx, |_, cx| { + cx.subscribe(&workspace, |terminal_panel, _, e, cx| { + if let workspace::Event::SpawnTask(spawn_in_terminal) = e { + terminal_panel.spawn_task(spawn_in_terminal, cx); + }; + }) + .detach(); }) .ok(); } - let pane = pane.downgrade(); - let items = futures::future::join_all(items).await; - let mut alive_item_ids = Vec::new(); - pane.update(&mut cx, |pane, cx| { - let active_item_id = serialized_panel - .as_ref() - .and_then(|panel| panel.active_item_id); - let mut active_ix = None; - for item in items { - if let Some(item) = item.log_err() { - let item_id = item.entity_id().as_u64(); - pane.add_item(Box::new(item), false, false, None, cx); - alive_item_ids.push(item_id as ItemId); - if Some(item_id) == active_item_id { - active_ix = Some(pane.items_len() - 1); - } - } - } - - if let Some(active_ix) = active_ix { - pane.activate_item(active_ix, false, false, cx) - } - })?; - // Since panels/docks are loaded outside from the workspace, we cleanup here, instead of through the workspace. if let Some(workspace) = workspace.upgrade() { let cleanup_task = workspace.update(&mut cx, |workspace, cx| { + let alive_item_ids = terminal_panel + .read(cx) + .center + .panes() + .into_iter() + .flat_map(|pane| pane.read(cx).items()) + .map(|item| item.item_id().as_u64() as ItemId) + .collect(); workspace .database_id() .map(|workspace_id| TerminalView::cleanup(workspace_id, alive_item_ids, cx)) @@ -351,33 +253,92 @@ impl TerminalPanel { } } - Ok(panel) + Ok(terminal_panel) } fn handle_pane_event( &mut self, - _pane: View, + pane: View, event: &pane::Event, cx: &mut ViewContext, ) { match event { pane::Event::ActivateItem { .. } => self.serialize(cx), pane::Event::RemovedItem { .. } => self.serialize(cx), - pane::Event::Remove { .. } => cx.emit(PanelEvent::Close), + pane::Event::Remove { focus_on_pane } => { + let pane_count_before_removal = self.center.panes().len(); + let _removal_result = self.center.remove(&pane); + if pane_count_before_removal == 1 { + cx.emit(PanelEvent::Close); + } else { + if let Some(focus_on_pane) = + focus_on_pane.as_ref().or_else(|| self.center.panes().pop()) + { + focus_on_pane.focus_handle(cx).focus(cx); + } + } + } pane::Event::ZoomIn => cx.emit(PanelEvent::ZoomIn), pane::Event::ZoomOut => cx.emit(PanelEvent::ZoomOut), - pane::Event::AddItem { item } => { if let Some(workspace) = self.workspace.upgrade() { - let pane = self.pane.clone(); - workspace.update(cx, |workspace, cx| item.added_to_pane(workspace, pane, cx)) + workspace.update(cx, |workspace, cx| { + item.added_to_pane(workspace, pane.clone(), cx) + }) } } + pane::Event::Split(direction) => { + let Some(new_pane) = self.new_pane_with_cloned_active_terminal(cx) else { + return; + }; + self.center.split(&pane, &new_pane, *direction).log_err(); + } + pane::Event::Focus => { + self.active_pane = pane.clone(); + } _ => {} } } + fn new_pane_with_cloned_active_terminal( + &mut self, + cx: &mut ViewContext, + ) -> Option> { + let workspace = self.workspace.clone().upgrade()?; + let project = workspace.read(cx).project().clone(); + let working_directory = self + .active_pane + .read(cx) + .active_item() + .and_then(|item| item.downcast::()) + .and_then(|terminal_view| { + terminal_view + .read(cx) + .terminal() + .read(cx) + .working_directory() + }) + .or_else(|| default_working_directory(workspace.read(cx), cx)); + let kind = TerminalKind::Shell(working_directory); + let window = cx.window_handle(); + let terminal = project + .update(cx, |project, cx| project.create_terminal(kind, window, cx)) + .log_err()?; + let database_id = workspace.read(cx).database_id(); + let terminal_view = Box::new(cx.new_view(|cx| { + TerminalView::new(terminal.clone(), self.workspace.clone(), database_id, cx) + })); + let pane = new_terminal_pane(self.workspace.clone(), project, cx); + self.apply_tab_bar_buttons(&pane, cx); + pane.update(cx, |pane, cx| { + pane.add_item(terminal_view, true, true, None, cx); + }); + cx.focus_view(&pane); + + Some(pane) + } + pub fn open_terminal( workspace: &mut Workspace, action: &workspace::OpenTerminal, @@ -494,7 +455,7 @@ impl TerminalPanel { .detach_and_log_err(cx); return; } - let (existing_item_index, existing_terminal) = terminals_for_task + let (existing_item_index, task_pane, existing_terminal) = terminals_for_task .last() .expect("covered no terminals case above") .clone(); @@ -503,7 +464,13 @@ impl TerminalPanel { !use_new_terminal, "Should have handled 'allow_concurrent_runs && use_new_terminal' case above" ); - self.replace_terminal(spawn_task, existing_item_index, existing_terminal, cx); + self.replace_terminal( + spawn_task, + task_pane, + existing_item_index, + existing_terminal, + cx, + ); } else { self.deferred_tasks.insert( spawn_in_terminal.id.clone(), @@ -518,6 +485,7 @@ impl TerminalPanel { } else { terminal_panel.replace_terminal( spawn_task, + task_pane, existing_item_index, existing_terminal, cx, @@ -562,25 +530,36 @@ impl TerminalPanel { &self, label: &str, cx: &mut AppContext, - ) -> Vec<(usize, View)> { - self.pane - .read(cx) - .items() - .enumerate() - .filter_map(|(index, item)| Some((index, item.act_as::(cx)?))) - .filter_map(|(index, terminal_view)| { - let task_state = terminal_view.read(cx).terminal().read(cx).task()?; - if &task_state.full_label == label { - Some((index, terminal_view)) - } else { - None - } + ) -> Vec<(usize, View, View)> { + self.center + .panes() + .into_iter() + .flat_map(|pane| { + pane.read(cx) + .items() + .enumerate() + .filter_map(|(index, item)| Some((index, item.act_as::(cx)?))) + .filter_map(|(index, terminal_view)| { + let task_state = terminal_view.read(cx).terminal().read(cx).task()?; + if &task_state.full_label == label { + Some((index, terminal_view)) + } else { + None + } + }) + .map(|(index, terminal_view)| (index, pane.clone(), terminal_view)) }) .collect() } - fn activate_terminal_view(&self, item_index: usize, focus: bool, cx: &mut WindowContext) { - self.pane.update(cx, |pane, cx| { + fn activate_terminal_view( + &self, + pane: &View, + item_index: usize, + focus: bool, + cx: &mut WindowContext, + ) { + pane.update(cx, |pane, cx| { pane.activate_item(item_index, true, focus, cx) }) } @@ -601,7 +580,7 @@ impl TerminalPanel { self.pending_terminals_to_add += 1; cx.spawn(|terminal_panel, mut cx| async move { - let pane = terminal_panel.update(&mut cx, |this, _| this.pane.clone())?; + let pane = terminal_panel.update(&mut cx, |this, _| this.active_pane.clone())?; let result = workspace.update(&mut cx, |workspace, cx| { let window = cx.window_handle(); let terminal = workspace @@ -640,52 +619,49 @@ impl TerminalPanel { } fn serialize(&mut self, cx: &mut ViewContext) { - let mut items_to_serialize = HashSet::default(); - let items = self - .pane - .read(cx) - .items() - .filter_map(|item| { - let terminal_view = item.act_as::(cx)?; - if terminal_view.read(cx).terminal().read(cx).task().is_some() { - None - } else { - let id = item.item_id().as_u64(); - items_to_serialize.insert(id); - Some(id) - } - }) - .collect::>(); - let active_item_id = self - .pane - .read(cx) - .active_item() - .map(|item| item.item_id().as_u64()) - .filter(|active_id| items_to_serialize.contains(active_id)); let height = self.height; let width = self.width; - self.pending_serialization = cx.background_executor().spawn( - async move { - KEY_VALUE_STORE - .write_kvp( - TERMINAL_PANEL_KEY.into(), - serde_json::to_string(&SerializedTerminalPanel { - items, - active_item_id, - height, - width, - })?, - ) - .await?; - anyhow::Ok(()) - } - .log_err(), - ); + self.pending_serialization = cx.spawn(|terminal_panel, mut cx| async move { + cx.background_executor() + .timer(Duration::from_millis(50)) + .await; + let terminal_panel = terminal_panel.upgrade()?; + let items = terminal_panel + .update(&mut cx, |terminal_panel, cx| { + SerializedItems::WithSplits(serialize_pane_group( + &terminal_panel.center, + &terminal_panel.active_pane, + cx, + )) + }) + .ok()?; + cx.background_executor() + .spawn( + async move { + KEY_VALUE_STORE + .write_kvp( + TERMINAL_PANEL_KEY.into(), + serde_json::to_string(&SerializedTerminalPanel { + items, + active_item_id: None, + height, + width, + })?, + ) + .await?; + anyhow::Ok(()) + } + .log_err(), + ) + .await; + Some(()) + }); } fn replace_terminal( &self, spawn_task: SpawnInTerminal, + task_pane: View, terminal_item_index: usize, terminal_to_replace: View, cx: &mut ViewContext<'_, Self>, @@ -708,7 +684,7 @@ impl TerminalPanel { match reveal { RevealStrategy::Always => { - self.activate_terminal_view(terminal_item_index, true, cx); + self.activate_terminal_view(&task_pane, terminal_item_index, true, cx); let task_workspace = self.workspace.clone(); cx.spawn(|_, mut cx| async move { task_workspace @@ -718,7 +694,7 @@ impl TerminalPanel { .detach(); } RevealStrategy::NoFocus => { - self.activate_terminal_view(terminal_item_index, false, cx); + self.activate_terminal_view(&task_pane, terminal_item_index, false, cx); let task_workspace = self.workspace.clone(); cx.spawn(|_, mut cx| async move { task_workspace @@ -734,7 +710,7 @@ impl TerminalPanel { } fn has_no_terminals(&self, cx: &WindowContext) -> bool { - self.pane.read(cx).items_len() == 0 && self.pending_terminals_to_add == 0 + self.active_pane.read(cx).items_len() == 0 && self.pending_terminals_to_add == 0 } pub fn assistant_enabled(&self) -> bool { @@ -742,11 +718,149 @@ impl TerminalPanel { } } +pub fn new_terminal_pane( + workspace: WeakView, + project: Model, + cx: &mut ViewContext, +) -> View { + let is_local = project.read(cx).is_local(); + let terminal_panel = cx.view().clone(); + let pane = cx.new_view(|cx| { + let mut pane = Pane::new( + workspace.clone(), + project.clone(), + Default::default(), + None, + NewTerminal.boxed_clone(), + cx, + ); + pane.set_can_navigate(false, cx); + pane.display_nav_history_buttons(None); + pane.set_should_display_tab_bar(|_| true); + + let terminal_panel_for_split_check = terminal_panel.clone(); + pane.set_can_split(Some(Arc::new(move |pane, dragged_item, cx| { + if let Some(tab) = dragged_item.downcast_ref::() { + let current_pane = cx.view().clone(); + let can_drag_away = + terminal_panel_for_split_check.update(cx, |terminal_panel, _| { + let current_panes = terminal_panel.center.panes(); + !current_panes.contains(&&tab.pane) + || current_panes.len() > 1 + || (tab.pane != current_pane || pane.items_len() > 1) + }); + if can_drag_away { + let item = if tab.pane == current_pane { + pane.item_for_index(tab.ix) + } else { + tab.pane.read(cx).item_for_index(tab.ix) + }; + if let Some(item) = item { + return item.downcast::().is_some(); + } + } + } + false + }))); + + let buffer_search_bar = cx.new_view(search::BufferSearchBar::new); + 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.set_custom_drop_handle(cx, move |pane, dropped_item, cx| { + if let Some(tab) = dropped_item.downcast_ref::() { + let this_pane = cx.view().clone(); + let belongs_to_this_pane = tab.pane == this_pane; + let item = if belongs_to_this_pane { + pane.item_for_index(tab.ix) + } else { + tab.pane.read(cx).item_for_index(tab.ix) + }; + if let Some(item) = item { + if item.downcast::().is_some() { + let source = tab.pane.clone(); + let item_id_to_move = item.item_id(); + + let new_pane = pane.drag_split_direction().and_then(|split_direction| { + terminal_panel.update(cx, |terminal_panel, cx| { + let new_pane = + new_terminal_pane(workspace.clone(), project.clone(), cx); + terminal_panel.apply_tab_bar_buttons(&new_pane, cx); + terminal_panel + .center + .split(&this_pane, &new_pane, split_direction) + .log_err()?; + Some(new_pane) + }) + }); + + let destination; + let destination_index; + if let Some(new_pane) = new_pane { + destination_index = new_pane.read(cx).active_item_index(); + destination = new_pane; + } else if belongs_to_this_pane { + return ControlFlow::Break(()); + } else { + destination = cx.view().clone(); + destination_index = pane.active_item_index(); + } + // Destination pane may be the one currently updated, so defer the move. + cx.spawn(|_, mut cx| async move { + cx.update(|cx| { + move_item( + &source, + &destination, + item_id_to_move, + destination_index, + cx, + ); + }) + .ok(); + }) + .detach(); + } else if let Some(project_path) = item.project_path(cx) { + if let Some(entry_path) = project.read(cx).absolute_path(&project_path, cx) + { + add_paths_to_terminal(pane, &[entry_path], cx); + } + } + } + } else if let Some(&entry_id) = dropped_item.downcast_ref::() { + if let Some(entry_path) = project + .read(cx) + .path_for_entry(entry_id, cx) + .and_then(|project_path| project.read(cx).absolute_path(&project_path, cx)) + { + add_paths_to_terminal(pane, &[entry_path], cx); + } + } else if is_local { + if let Some(paths) = dropped_item.downcast_ref::() { + add_paths_to_terminal(pane, paths.paths(), cx); + } + } + + ControlFlow::Break(()) + }); + + pane + }); + + cx.subscribe(&pane, TerminalPanel::handle_pane_event) + .detach(); + cx.observe(&pane, |_, _, cx| cx.notify()).detach(); + + pane +} + async fn wait_for_terminals_tasks( - terminals_for_task: Vec<(usize, View)>, + terminals_for_task: Vec<(usize, View, View)>, cx: &mut AsyncWindowContext, ) { - let pending_tasks = terminals_for_task.iter().filter_map(|(_, terminal)| { + let pending_tasks = terminals_for_task.iter().filter_map(|(_, _, terminal)| { terminal .update(cx, |terminal_view, cx| { terminal_view @@ -781,7 +895,7 @@ impl Render for TerminalPanel { let mut registrar = DivRegistrar::new( |panel, cx| { panel - .pane + .active_pane .read(cx) .toolbar() .read(cx) @@ -790,13 +904,99 @@ impl Render for TerminalPanel { cx, ); BufferSearchBar::register(&mut registrar); - registrar.into_div().size_full().child(self.pane.clone()) + let registrar = registrar.into_div(); + self.workspace + .update(cx, |workspace, cx| { + registrar.size_full().child(self.center.render( + workspace.project(), + &HashMap::default(), + None, + &self.active_pane, + workspace.zoomed_item(), + workspace.app_state(), + cx, + )) + }) + .ok() + .map(|div| { + div.on_action({ + cx.listener(|terminal_panel, action: &ActivatePaneInDirection, cx| { + if let Some(pane) = terminal_panel.center.find_pane_in_direction( + &terminal_panel.active_pane, + action.0, + cx, + ) { + cx.focus_view(&pane); + } + }) + }) + .on_action( + cx.listener(|terminal_panel, _action: &ActivateNextPane, cx| { + let panes = terminal_panel.center.panes(); + if let Some(ix) = panes + .iter() + .position(|pane| **pane == terminal_panel.active_pane) + { + let next_ix = (ix + 1) % panes.len(); + let next_pane = panes[next_ix].clone(); + cx.focus_view(&next_pane); + } + }), + ) + .on_action( + cx.listener(|terminal_panel, _action: &ActivatePreviousPane, cx| { + let panes = terminal_panel.center.panes(); + if let Some(ix) = panes + .iter() + .position(|pane| **pane == terminal_panel.active_pane) + { + let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1); + let prev_pane = panes[prev_ix].clone(); + cx.focus_view(&prev_pane); + } + }), + ) + .on_action(cx.listener(|terminal_panel, action: &ActivatePane, cx| { + let panes = terminal_panel.center.panes(); + if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) { + cx.focus_view(&pane); + } else { + if let Some(new_pane) = + terminal_panel.new_pane_with_cloned_active_terminal(cx) + { + terminal_panel + .center + .split( + &terminal_panel.active_pane, + &new_pane, + SplitDirection::Right, + ) + .log_err(); + } + } + })) + .on_action(cx.listener( + |terminal_panel, action: &SwapPaneInDirection, cx| { + if let Some(to) = terminal_panel + .center + .find_pane_in_direction(&terminal_panel.active_pane, action.0, cx) + .cloned() + { + terminal_panel + .center + .swap(&terminal_panel.active_pane.clone(), &to); + cx.notify(); + } + }, + )) + }) + .unwrap_or_else(|| div()) } } impl FocusableView for TerminalPanel { fn focus_handle(&self, cx: &AppContext) -> FocusHandle { - self.pane.focus_handle(cx) + self.active_pane.focus_handle(cx) } } @@ -848,11 +1048,12 @@ impl Panel for TerminalPanel { } fn is_zoomed(&self, cx: &WindowContext) -> bool { - self.pane.read(cx).is_zoomed() + self.active_pane.read(cx).is_zoomed() } fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext) { - self.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx)); + self.active_pane + .update(cx, |pane, cx| pane.set_zoomed(zoomed, cx)); } fn set_active(&mut self, active: bool, cx: &mut ViewContext) { @@ -872,7 +1073,12 @@ impl Panel for TerminalPanel { } fn icon_label(&self, cx: &WindowContext) -> Option { - let count = self.pane.read(cx).items_len(); + let count = self + .center + .panes() + .into_iter() + .map(|pane| pane.read(cx).items_len()) + .sum::(); if count == 0 { None } else { @@ -901,7 +1107,7 @@ impl Panel for TerminalPanel { } fn pane(&self) -> Option> { - Some(self.pane.clone()) + Some(self.active_pane.clone()) } } @@ -923,14 +1129,6 @@ impl Render for InlineAssistTabBarButton { } } -#[derive(Serialize, Deserialize)] -struct SerializedTerminalPanel { - items: Vec, - active_item_id: Option, - width: Option, - height: Option, -} - fn retrieve_system_shell() -> Option { #[cfg(not(target_os = "windows"))] { diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index ad0c7f520d..35ad35a0e1 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -33,8 +33,8 @@ use workspace::{ notifications::NotifyResultExt, register_serializable_item, searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle}, - CloseActiveItem, NewCenterTerminal, NewTerminal, OpenVisible, Pane, ToolbarItemLocation, - Workspace, WorkspaceId, + CloseActiveItem, NewCenterTerminal, NewTerminal, OpenVisible, ToolbarItemLocation, Workspace, + WorkspaceId, }; use anyhow::Context; @@ -1222,10 +1222,10 @@ impl SerializableItem for TerminalView { workspace: WeakView, workspace_id: workspace::WorkspaceId, item_id: workspace::ItemId, - cx: &mut ViewContext, + cx: &mut WindowContext, ) -> Task>> { let window = cx.window_handle(); - cx.spawn(|pane, mut cx| async move { + cx.spawn(|mut cx| async move { let cwd = cx .update(|cx| { let from_db = TERMINAL_DB @@ -1249,7 +1249,7 @@ impl SerializableItem for TerminalView { let terminal = project.update(&mut cx, |project, cx| { project.create_terminal(TerminalKind::Shell(cwd), window, cx) })??; - pane.update(&mut cx, |_, cx| { + cx.update(|cx| { cx.new_view(|cx| TerminalView::new(terminal, workspace, Some(workspace_id), cx)) }) }) diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index a7bf90dd17..20437145cb 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -315,7 +315,7 @@ pub trait SerializableItem: Item { _workspace: WeakView, _workspace_id: WorkspaceId, _item_id: ItemId, - _cx: &mut ViewContext, + _cx: &mut WindowContext, ) -> Task>>; fn serialize( @@ -1032,7 +1032,7 @@ impl WeakFollowableItemHandle for WeakView { #[cfg(any(test, feature = "test-support"))] pub mod test { use super::{Item, ItemEvent, SerializableItem, TabContentParams}; - use crate::{ItemId, ItemNavHistory, Pane, Workspace, WorkspaceId}; + use crate::{ItemId, ItemNavHistory, Workspace, WorkspaceId}; use gpui::{ AnyElement, AppContext, Context as _, EntityId, EventEmitter, FocusableView, InteractiveElement, IntoElement, Model, Render, SharedString, Task, View, ViewContext, @@ -1040,6 +1040,7 @@ pub mod test { }; use project::{Project, ProjectEntryId, ProjectPath, WorktreeId}; use std::{any::Any, cell::Cell, path::Path}; + use ui::WindowContext; pub struct TestProjectItem { pub entry_id: Option, @@ -1339,7 +1340,7 @@ pub mod test { _workspace: WeakView, workspace_id: WorkspaceId, _item_id: ItemId, - cx: &mut ViewContext, + cx: &mut WindowContext, ) -> Task>> { let view = cx.new_view(|cx| Self::new_deserialized(workspace_id, cx)); Task::ready(Ok(view)) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 4eec2f18d1..69485846e9 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -291,7 +291,7 @@ pub struct Pane { can_drop_predicate: Option bool>>, custom_drop_handle: Option) -> ControlFlow<(), ()>>>, - can_split: bool, + can_split_predicate: Option) -> bool>>, should_display_tab_bar: Rc) -> bool>, render_tab_bar_buttons: Rc) -> (Option, Option)>, @@ -411,7 +411,7 @@ impl Pane { project, can_drop_predicate, custom_drop_handle: None, - can_split: true, + can_split_predicate: None, should_display_tab_bar: Rc::new(|cx| TabBarSettings::get_global(cx).show), render_tab_bar_buttons: Rc::new(move |pane, cx| { if !pane.has_focus(cx) && !pane.context_menu_focused(cx) { @@ -623,9 +623,13 @@ impl Pane { self.should_display_tab_bar = Rc::new(should_display_tab_bar); } - pub fn set_can_split(&mut self, can_split: bool, cx: &mut ViewContext) { - self.can_split = can_split; - cx.notify(); + pub fn set_can_split( + &mut self, + can_split_predicate: Option< + Arc) -> bool + 'static>, + >, + ) { + self.can_split_predicate = can_split_predicate; } pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut ViewContext) { @@ -2384,8 +2388,18 @@ impl Pane { self.zoomed } - fn handle_drag_move(&mut self, event: &DragMoveEvent, cx: &mut ViewContext) { - if !self.can_split { + fn handle_drag_move( + &mut self, + event: &DragMoveEvent, + cx: &mut ViewContext, + ) { + let can_split_predicate = self.can_split_predicate.take(); + let can_split = match &can_split_predicate { + Some(can_split_predicate) => can_split_predicate(self, event.dragged_item(), cx), + None => false, + }; + self.can_split_predicate = can_split_predicate; + if !can_split { return; } @@ -2679,6 +2693,10 @@ impl Pane { }) .collect() } + + pub fn drag_split_direction(&self) -> Option { + self.drag_split_direction + } } impl FocusableView for Pane { diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 6f7d1a66b9..4461e58925 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -27,11 +27,11 @@ const VERTICAL_MIN_SIZE: f32 = 100.; /// Single-pane group is a regular pane. #[derive(Clone)] pub struct PaneGroup { - pub(crate) root: Member, + pub root: Member, } impl PaneGroup { - pub(crate) fn with_root(root: Member) -> Self { + pub fn with_root(root: Member) -> Self { Self { root } } @@ -122,7 +122,7 @@ impl PaneGroup { } #[allow(clippy::too_many_arguments)] - pub(crate) fn render( + pub fn render( &self, project: &Model, follower_states: &HashMap, @@ -144,19 +144,51 @@ impl PaneGroup { ) } - pub(crate) fn panes(&self) -> Vec<&View> { + pub fn panes(&self) -> Vec<&View> { let mut panes = Vec::new(); self.root.collect_panes(&mut panes); panes } - pub(crate) fn first_pane(&self) -> View { + pub fn first_pane(&self) -> View { self.root.first_pane() } + + pub fn find_pane_in_direction( + &mut self, + active_pane: &View, + direction: SplitDirection, + cx: &WindowContext, + ) -> Option<&View> { + let bounding_box = self.bounding_box_for_pane(active_pane)?; + let cursor = active_pane.read(cx).pixel_position_of_cursor(cx); + let center = match cursor { + Some(cursor) if bounding_box.contains(&cursor) => cursor, + _ => bounding_box.center(), + }; + + let distance_to_next = crate::HANDLE_HITBOX_SIZE; + + let target = match direction { + SplitDirection::Left => { + Point::new(bounding_box.left() - distance_to_next.into(), center.y) + } + SplitDirection::Right => { + Point::new(bounding_box.right() + distance_to_next.into(), center.y) + } + SplitDirection::Up => { + Point::new(center.x, bounding_box.top() - distance_to_next.into()) + } + SplitDirection::Down => { + Point::new(center.x, bounding_box.bottom() + distance_to_next.into()) + } + }; + self.pane_at_pixel_position(target) + } } -#[derive(Clone)] -pub(crate) enum Member { +#[derive(Debug, Clone)] +pub enum Member { Axis(PaneAxis), Pane(View), } @@ -359,8 +391,8 @@ impl Member { } } -#[derive(Clone)] -pub(crate) struct PaneAxis { +#[derive(Debug, Clone)] +pub struct PaneAxis { pub axis: Axis, pub members: Vec, pub flexes: Arc>>, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 28fd730e60..4687b1decd 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -777,7 +777,7 @@ pub struct ViewId { pub id: u64, } -struct FollowerState { +pub struct FollowerState { center_pane: View, dock_pane: Option>, active_view_id: Option, @@ -887,14 +887,16 @@ impl Workspace { let pane_history_timestamp = Arc::new(AtomicUsize::new(0)); let center_pane = cx.new_view(|cx| { - Pane::new( + let mut center_pane = Pane::new( weak_handle.clone(), project.clone(), pane_history_timestamp.clone(), None, NewFile.boxed_clone(), cx, - ) + ); + center_pane.set_can_split(Some(Arc::new(|_, _, _| true))); + center_pane }); cx.subscribe(¢er_pane, Self::handle_pane_event).detach(); @@ -2464,14 +2466,16 @@ impl Workspace { fn add_pane(&mut self, cx: &mut ViewContext) -> View { let pane = cx.new_view(|cx| { - Pane::new( + let mut pane = Pane::new( self.weak_handle(), self.project.clone(), self.pane_history_timestamp.clone(), None, NewFile.boxed_clone(), cx, - ) + ); + pane.set_can_split(Some(Arc::new(|_, _, _| true))); + pane }); cx.subscribe(&pane, Self::handle_pane_event).detach(); self.panes.push(pane.clone()); @@ -2955,30 +2959,9 @@ impl Workspace { direction: SplitDirection, cx: &WindowContext, ) -> Option> { - let bounding_box = self.center.bounding_box_for_pane(&self.active_pane)?; - let cursor = self.active_pane.read(cx).pixel_position_of_cursor(cx); - let center = match cursor { - Some(cursor) if bounding_box.contains(&cursor) => cursor, - _ => bounding_box.center(), - }; - - let distance_to_next = pane_group::HANDLE_HITBOX_SIZE; - - let target = match direction { - SplitDirection::Left => { - Point::new(bounding_box.left() - distance_to_next.into(), center.y) - } - SplitDirection::Right => { - Point::new(bounding_box.right() + distance_to_next.into(), center.y) - } - SplitDirection::Up => { - Point::new(center.x, bounding_box.top() - distance_to_next.into()) - } - SplitDirection::Down => { - Point::new(center.x, bounding_box.bottom() + distance_to_next.into()) - } - }; - self.center.pane_at_pixel_position(target).cloned() + self.center + .find_pane_in_direction(&self.active_pane, direction, cx) + .cloned() } pub fn swap_pane_in_direction( @@ -4591,6 +4574,10 @@ impl Workspace { let window = cx.window_handle().downcast::()?; cx.read_window(&window, |workspace, _| workspace).ok() } + + pub fn zoomed_item(&self) -> Option<&AnyWeakView> { + self.zoomed.as_ref() + } } fn leader_border_for_pane( From cff9ae0bbcc7f05c075d8aa226954c0ac290ece9 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, 28 Nov 2024 02:22:58 +0800 Subject: [PATCH 171/886] Better absolute path handling (#19727) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #19866 This PR supersedes #19228, as #19228 encountered too many merge conflicts. After some exploration, I found that for paths with the `\\?\` prefix, we can safely remove it and consistently use the clean paths in all cases. Previously, in #19228, I thought we would still need the `\\?\` prefix for IO operations to handle long paths better. However, this turns out to be unnecessary because Rust automatically manages this for us when calling IO-related APIs. For details, refer to Rust's internal function [`get_long_path`](https://github.com/rust-lang/rust/blob/017ae1b21f7be6dcdcfc95631e54bde806653a8a/library/std/src/sys/path/windows.rs#L225-L233). Therefore, we can always store and use paths without the `\\?\` prefix. This PR introduces a `SanitizedPath` structure, which represents a path stripped of the `\\?\` prefix. To prevent untrimmed paths from being mistakenly passed into `Worktree`, the type of `Worktree`’s `abs_path` member variable has been changed to `SanitizedPath`. Additionally, this PR reverts the changes of #15856 and #18726. After testing, it appears that the issues those PRs addressed can be resolved by this PR. ### Existing Issue To keep the scope of modifications manageable, `Worktree::abs_path` has retained its current signature as `fn abs_path(&self) -> Arc`, rather than returning a `SanitizedPath`. Updating the method to return `SanitizedPath`—which may better resolve path inconsistencies—would likely introduce extensive changes similar to those in #19228. Currently, the limitation is as follows: ```rust let abs_path: &Arc = snapshot.abs_path(); let some_non_trimmed_path = Path::new("\\\\?\\C:\\Users\\user\\Desktop\\project"); // The caller performs some actions here: some_non_trimmed_path.strip_prefix(abs_path); // This fails some_non_trimmed_path.starts_with(abs_path); // This fails too ``` The final two lines will fail because `snapshot.abs_path()` returns a clean path without the `\\?\` prefix. I have identified two relevant instances that may face this issue: - [lsp_store.rs#L3578](https://github.com/zed-industries/zed/blob/0173479d18e2526c1f9c8b25ac94ec66b992a2b2/crates/project/src/lsp_store.rs#L3578) - [worktree.rs#L4338](https://github.com/zed-industries/zed/blob/0173479d18e2526c1f9c8b25ac94ec66b992a2b2/crates/worktree/src/worktree.rs#L4338) Switching `Worktree::abs_path` to return `SanitizedPath` would resolve these issues but would also lead to many code changes. Any suggestions or feedback on this approach are very welcome. cc @SomeoneToIgnore Release Notes: - N/A --- Cargo.lock | 7 ++ Cargo.toml | 12 +- crates/fs/src/fs.rs | 20 ++-- crates/gpui/src/platform/windows/platform.rs | 14 +-- crates/project/src/lsp_store.rs | 3 +- crates/project/src/worktree_store.rs | 42 ++++--- crates/terminal_view/src/terminal_view.rs | 15 --- crates/util/Cargo.toml | 1 + crates/util/src/paths.rs | 60 +++++++++- crates/workspace/src/workspace.rs | 6 +- crates/worktree/src/worktree.rs | 117 ++++++++++++------- crates/zed/src/main.rs | 5 +- 12 files changed, 189 insertions(+), 113 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9e1354c40d..f5c45f8d4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3752,6 +3752,12 @@ dependencies = [ "phf", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "dwrote" version = "0.11.2" @@ -13689,6 +13695,7 @@ dependencies = [ "async-fs 1.6.0", "collections", "dirs 4.0.0", + "dunce", "futures 0.3.31", "futures-lite 1.13.0", "git2", diff --git a/Cargo.toml b/Cargo.toml index 7c141a1b6c..71701dd8f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -228,7 +228,9 @@ git = { path = "crates/git" } git_hosting_providers = { path = "crates/git_hosting_providers" } go_to_line = { path = "crates/go_to_line" } google_ai = { path = "crates/google_ai" } -gpui = { path = "crates/gpui", default-features = false, features = ["http_client"]} +gpui = { path = "crates/gpui", default-features = false, features = [ + "http_client", +] } gpui_macros = { path = "crates/gpui_macros" } html_to_markdown = { path = "crates/html_to_markdown" } http_client = { path = "crates/http_client" } @@ -403,10 +405,10 @@ parking_lot = "0.12.1" pathdiff = "0.2" pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" } pet-fs = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" } -pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" } -pet-core = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" } -pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" } -pet-reporter = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" } +pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" } +pet-core = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" } +pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" } +pet-reporter = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" } postage = { version = "0.5", features = ["futures-traits"] } pretty_assertions = { version = "1.3.0", features = ["unstable"] } profiling = "1" diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index fc0fae3fe8..37525db7d9 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -452,18 +452,16 @@ impl Fs for RealFs { #[cfg(target_os = "windows")] async fn trash_file(&self, path: &Path, _options: RemoveOptions) -> Result<()> { + use util::paths::SanitizedPath; use windows::{ core::HSTRING, Storage::{StorageDeleteOption, StorageFile}, }; // todo(windows) // When new version of `windows-rs` release, make this operation `async` - let path = path.canonicalize()?.to_string_lossy().to_string(); - let path_str = path.trim_start_matches("\\\\?\\"); - if path_str.is_empty() { - anyhow::bail!("File path is empty!"); - } - let file = StorageFile::GetFileFromPathAsync(&HSTRING::from(path_str))?.get()?; + let path = SanitizedPath::from(path.canonicalize()?); + let path_string = path.to_string(); + let file = StorageFile::GetFileFromPathAsync(&HSTRING::from(path_string))?.get()?; file.DeleteAsync(StorageDeleteOption::Default)?.get()?; Ok(()) } @@ -480,19 +478,17 @@ impl Fs for RealFs { #[cfg(target_os = "windows")] async fn trash_dir(&self, path: &Path, _options: RemoveOptions) -> Result<()> { + use util::paths::SanitizedPath; use windows::{ core::HSTRING, Storage::{StorageDeleteOption, StorageFolder}, }; - let path = path.canonicalize()?.to_string_lossy().to_string(); - let path_str = path.trim_start_matches("\\\\?\\"); - if path_str.is_empty() { - anyhow::bail!("Folder path is empty!"); - } // todo(windows) // When new version of `windows-rs` release, make this operation `async` - let folder = StorageFolder::GetFolderFromPathAsync(&HSTRING::from(path_str))?.get()?; + let path = SanitizedPath::from(path.canonicalize()?); + let path_string = path.to_string(); + let folder = StorageFolder::GetFolderFromPathAsync(&HSTRING::from(path_string))?.get()?; folder.DeleteAsync(StorageDeleteOption::Default)?.get()?; Ok(()) } diff --git a/crates/gpui/src/platform/windows/platform.rs b/crates/gpui/src/platform/windows/platform.rs index 91e9816106..389b90765d 100644 --- a/crates/gpui/src/platform/windows/platform.rs +++ b/crates/gpui/src/platform/windows/platform.rs @@ -6,7 +6,7 @@ use std::{ sync::Arc, }; -use ::util::ResultExt; +use ::util::{paths::SanitizedPath, ResultExt}; use anyhow::{anyhow, Context, Result}; use async_task::Runnable; use futures::channel::oneshot::{self, Receiver}; @@ -645,13 +645,11 @@ fn file_save_dialog(directory: PathBuf) -> Result> { let dialog: IFileSaveDialog = unsafe { CoCreateInstance(&FileSaveDialog, None, CLSCTX_ALL)? }; if !directory.to_string_lossy().is_empty() { if let Some(full_path) = directory.canonicalize().log_err() { - let full_path = full_path.to_string_lossy(); - let full_path_str = full_path.trim_start_matches("\\\\?\\"); - if !full_path_str.is_empty() { - let path_item: IShellItem = - unsafe { SHCreateItemFromParsingName(&HSTRING::from(full_path_str), None)? }; - unsafe { dialog.SetFolder(&path_item).log_err() }; - } + let full_path = SanitizedPath::from(full_path); + let full_path_string = full_path.to_string(); + let path_item: IShellItem = + unsafe { SHCreateItemFromParsingName(&HSTRING::from(full_path_string), None)? }; + unsafe { dialog.SetFolder(&path_item).log_err() }; } } unsafe { diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 29a0afcfe5..6f4d23fa76 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -5577,7 +5577,7 @@ impl LspStore { let worktree = worktree_handle.read(cx); let worktree_id = worktree.id(); - let worktree_path = worktree.abs_path(); + let root_path = worktree.abs_path(); let key = (worktree_id, adapter.name.clone()); if self.language_server_ids.contains_key(&key) { @@ -5599,7 +5599,6 @@ impl LspStore { as Arc; let server_id = self.languages.next_language_server_id(); - let root_path = worktree_path.clone(); log::info!( "attempting to start language server {:?}, path: {root_path:?}, id: {server_id}", adapter.name.0 diff --git a/crates/project/src/worktree_store.rs b/crates/project/src/worktree_store.rs index db5ae67ba7..1e48cc052e 100644 --- a/crates/project/src/worktree_store.rs +++ b/crates/project/src/worktree_store.rs @@ -23,7 +23,7 @@ use smol::{ stream::StreamExt, }; use text::ReplicaId; -use util::ResultExt; +use util::{paths::SanitizedPath, ResultExt}; use worktree::{Entry, ProjectEntryId, Worktree, WorktreeId, WorktreeSettings}; use crate::{search::SearchQuery, ProjectPath}; @@ -52,7 +52,7 @@ pub struct WorktreeStore { worktrees_reordered: bool, #[allow(clippy::type_complexity)] loading_worktrees: - HashMap, Shared, Arc>>>>, + HashMap, Arc>>>>, state: WorktreeStoreState, } @@ -147,11 +147,12 @@ impl WorktreeStore { pub fn find_worktree( &self, - abs_path: &Path, + abs_path: impl Into, cx: &AppContext, ) -> Option<(Model, PathBuf)> { + let abs_path: SanitizedPath = abs_path.into(); for tree in self.worktrees() { - if let Ok(relative_path) = abs_path.strip_prefix(tree.read(cx).abs_path()) { + if let Ok(relative_path) = abs_path.as_path().strip_prefix(tree.read(cx).abs_path()) { return Some((tree.clone(), relative_path.into())); } } @@ -192,12 +193,12 @@ impl WorktreeStore { pub fn create_worktree( &mut self, - abs_path: impl AsRef, + abs_path: impl Into, visible: bool, cx: &mut ModelContext, ) -> Task>> { - let path: Arc = abs_path.as_ref().into(); - if !self.loading_worktrees.contains_key(&path) { + let abs_path: SanitizedPath = abs_path.into(); + if !self.loading_worktrees.contains_key(&abs_path) { let task = match &self.state { WorktreeStoreState::Remote { upstream_client, .. @@ -205,20 +206,26 @@ impl WorktreeStore { if upstream_client.is_via_collab() { Task::ready(Err(Arc::new(anyhow!("cannot create worktrees via collab")))) } else { - self.create_ssh_worktree(upstream_client.clone(), abs_path, visible, cx) + self.create_ssh_worktree( + upstream_client.clone(), + abs_path.clone(), + visible, + cx, + ) } } WorktreeStoreState::Local { fs } => { - self.create_local_worktree(fs.clone(), abs_path, visible, cx) + self.create_local_worktree(fs.clone(), abs_path.clone(), visible, cx) } }; - self.loading_worktrees.insert(path.clone(), task.shared()); + self.loading_worktrees + .insert(abs_path.clone(), task.shared()); } - let task = self.loading_worktrees.get(&path).unwrap().clone(); + let task = self.loading_worktrees.get(&abs_path).unwrap().clone(); cx.spawn(|this, mut cx| async move { let result = task.await; - this.update(&mut cx, |this, _| this.loading_worktrees.remove(&path)) + this.update(&mut cx, |this, _| this.loading_worktrees.remove(&abs_path)) .ok(); match result { Ok(worktree) => Ok(worktree), @@ -230,12 +237,11 @@ impl WorktreeStore { fn create_ssh_worktree( &mut self, client: AnyProtoClient, - abs_path: impl AsRef, + abs_path: impl Into, visible: bool, cx: &mut ModelContext, ) -> Task, Arc>> { - let path_key: Arc = abs_path.as_ref().into(); - let mut abs_path = path_key.clone().to_string_lossy().to_string(); + let mut abs_path = Into::::into(abs_path).to_string(); // If we start with `/~` that means the ssh path was something like `ssh://user@host/~/home-dir-folder/` // in which case want to strip the leading the `/`. // On the host-side, the `~` will get expanded. @@ -293,12 +299,12 @@ impl WorktreeStore { fn create_local_worktree( &mut self, fs: Arc, - abs_path: impl AsRef, + abs_path: impl Into, visible: bool, cx: &mut ModelContext, ) -> Task, Arc>> { let next_entry_id = self.next_entry_id.clone(); - let path: Arc = abs_path.as_ref().into(); + let path: SanitizedPath = abs_path.into(); cx.spawn(move |this, mut cx| async move { let worktree = Worktree::local(path.clone(), visible, fs, next_entry_id, &mut cx).await; @@ -308,7 +314,7 @@ impl WorktreeStore { if visible { cx.update(|cx| { - cx.add_recent_document(&path); + cx.add_recent_document(path.as_path()); }) .log_err(); } diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 35ad35a0e1..44e97122b8 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -798,7 +798,6 @@ fn possible_open_paths_metadata( cx.background_executor().spawn(async move { let mut paths_with_metadata = Vec::with_capacity(potential_paths.len()); - #[cfg(not(target_os = "windows"))] let mut fetch_metadata_tasks = potential_paths .into_iter() .map(|potential_path| async { @@ -814,20 +813,6 @@ fn possible_open_paths_metadata( }) .collect::>(); - #[cfg(target_os = "windows")] - let mut fetch_metadata_tasks = potential_paths - .iter() - .map(|potential_path| async { - let metadata = fs.metadata(potential_path).await.ok().flatten(); - let path = PathBuf::from( - potential_path - .to_string_lossy() - .trim_start_matches("\\\\?\\"), - ); - (PathWithPosition { path, row, column }, metadata) - }) - .collect::>(); - while let Some((path, metadata)) = fetch_metadata_tasks.next().await { if let Some(metadata) = metadata { paths_with_metadata.push((path, metadata)); diff --git a/crates/util/Cargo.toml b/crates/util/Cargo.toml index 94d580e643..2f84114409 100644 --- a/crates/util/Cargo.toml +++ b/crates/util/Cargo.toml @@ -37,6 +37,7 @@ unicase.workspace = true [target.'cfg(windows)'.dependencies] tendril = "0.4.3" +dunce = "1.0" [dev-dependencies] git2.workspace = true diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index f4e494f66e..e3b0af1fdb 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -1,5 +1,5 @@ use std::cmp; -use std::sync::OnceLock; +use std::sync::{Arc, OnceLock}; use std::{ ffi::OsStr, path::{Path, PathBuf}, @@ -95,6 +95,46 @@ impl> PathExt for T { } } +/// Due to the issue of UNC paths on Windows, which can cause bugs in various parts of Zed, introducing this `SanitizedPath` +/// leverages Rust's type system to ensure that all paths entering Zed are always "sanitized" by removing the `\\\\?\\` prefix. +/// On non-Windows operating systems, this struct is effectively a no-op. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct SanitizedPath(Arc); + +impl SanitizedPath { + pub fn starts_with(&self, prefix: &SanitizedPath) -> bool { + self.0.starts_with(&prefix.0) + } + + pub fn as_path(&self) -> &Arc { + &self.0 + } + + pub fn to_string(&self) -> String { + self.0.to_string_lossy().to_string() + } +} + +impl From for Arc { + fn from(sanitized_path: SanitizedPath) -> Self { + sanitized_path.0 + } +} + +impl> From for SanitizedPath { + #[cfg(not(target_os = "windows"))] + fn from(path: T) -> Self { + let path = path.as_ref(); + SanitizedPath(path.into()) + } + + #[cfg(target_os = "windows")] + fn from(path: T) -> Self { + let path = path.as_ref(); + SanitizedPath(dunce::simplified(path).into()) + } +} + /// A delimiter to use in `path_query:row_number:column_number` strings parsing. pub const FILE_ROW_COLUMN_DELIMITER: char = ':'; @@ -805,4 +845,22 @@ mod tests { "Path matcher should match {path:?}" ); } + + #[test] + #[cfg(target_os = "windows")] + fn test_sanitized_path() { + let path = Path::new("C:\\Users\\someone\\test_file.rs"); + let sanitized_path = SanitizedPath::from(path); + assert_eq!( + sanitized_path.to_string(), + "C:\\Users\\someone\\test_file.rs" + ); + + let path = Path::new("\\\\?\\C:\\Users\\someone\\test_file.rs"); + let sanitized_path = SanitizedPath::from(path); + assert_eq!( + sanitized_path.to_string(), + "C:\\Users\\someone\\test_file.rs" + ); + } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 4687b1decd..ed5aaa6e49 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -97,7 +97,7 @@ use ui::{ IntoElement, ParentElement as _, Pixels, SharedString, Styled as _, ViewContext, VisualContext as _, WindowContext, }; -use util::{ResultExt, TryFutureExt}; +use util::{paths::SanitizedPath, ResultExt, TryFutureExt}; use uuid::Uuid; pub use workspace_settings::{ AutosaveSetting, RestoreOnStartupBehavior, TabBarSettings, WorkspaceSettings, @@ -2024,7 +2024,7 @@ impl Workspace { }; let this = this.clone(); - let abs_path = abs_path.clone(); + let abs_path: Arc = SanitizedPath::from(abs_path.clone()).into(); let fs = fs.clone(); let pane = pane.clone(); let task = cx.spawn(move |mut cx| async move { @@ -2033,7 +2033,7 @@ impl Workspace { this.update(&mut cx, |workspace, cx| { let worktree = worktree.read(cx); let worktree_abs_path = worktree.abs_path(); - let entry_id = if abs_path == worktree_abs_path.as_ref() { + let entry_id = if abs_path.as_ref() == worktree_abs_path.as_ref() { worktree.root_entry() } else { abs_path diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index b7ee4466c7..e856bbf7de 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -66,7 +66,7 @@ use std::{ use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet}; use text::{LineEnding, Rope}; use util::{ - paths::{home_dir, PathMatcher}, + paths::{home_dir, PathMatcher, SanitizedPath}, ResultExt, }; pub use worktree_settings::WorktreeSettings; @@ -149,7 +149,7 @@ pub struct RemoteWorktree { #[derive(Clone)] pub struct Snapshot { id: WorktreeId, - abs_path: Arc, + abs_path: SanitizedPath, root_name: String, root_char_bag: CharBag, entries_by_path: SumTree, @@ -356,7 +356,7 @@ enum ScanState { scanning: bool, }, RootUpdated { - new_path: Option>, + new_path: Option, }, } @@ -654,8 +654,8 @@ impl Worktree { pub fn abs_path(&self) -> Arc { match self { - Worktree::Local(worktree) => worktree.abs_path.clone(), - Worktree::Remote(worktree) => worktree.abs_path.clone(), + Worktree::Local(worktree) => worktree.abs_path.clone().into(), + Worktree::Remote(worktree) => worktree.abs_path.clone().into(), } } @@ -1026,6 +1026,7 @@ impl LocalWorktree { } pub fn contains_abs_path(&self, path: &Path) -> bool { + let path = SanitizedPath::from(path); path.starts_with(&self.abs_path) } @@ -1066,13 +1067,13 @@ impl LocalWorktree { let (scan_states_tx, mut scan_states_rx) = mpsc::unbounded(); let background_scanner = cx.background_executor().spawn({ let abs_path = &snapshot.abs_path; - let abs_path = if cfg!(target_os = "windows") { - abs_path - .canonicalize() - .unwrap_or_else(|_| abs_path.to_path_buf()) - } else { - abs_path.to_path_buf() - }; + #[cfg(target_os = "windows")] + let abs_path = abs_path + .as_path() + .canonicalize() + .unwrap_or_else(|_| abs_path.as_path().to_path_buf()); + #[cfg(not(target_os = "windows"))] + let abs_path = abs_path.as_path().to_path_buf(); let background = cx.background_executor().clone(); async move { let (events, watcher) = fs.watch(&abs_path, FS_WATCH_LATENCY).await; @@ -1135,6 +1136,7 @@ impl LocalWorktree { this.snapshot.git_repositories = Default::default(); this.snapshot.ignores_by_parent_abs_path = Default::default(); let root_name = new_path + .as_path() .file_name() .map_or(String::new(), |f| f.to_string_lossy().to_string()); this.snapshot.update_abs_path(new_path, root_name); @@ -2075,7 +2077,7 @@ impl Snapshot { pub fn new(id: u64, root_name: String, abs_path: Arc) -> Self { Snapshot { id: WorktreeId::from_usize(id as usize), - abs_path, + abs_path: abs_path.into(), root_char_bag: root_name.chars().map(|c| c.to_ascii_lowercase()).collect(), root_name, always_included_entries: Default::default(), @@ -2091,8 +2093,20 @@ impl Snapshot { self.id } + // TODO: + // Consider the following: + // + // ```rust + // let abs_path: Arc = snapshot.abs_path(); // e.g. "C:\Users\user\Desktop\project" + // let some_non_trimmed_path = Path::new("\\\\?\\C:\\Users\\user\\Desktop\\project\\main.rs"); + // // The caller perform some actions here: + // some_non_trimmed_path.strip_prefix(abs_path); // This fails + // some_non_trimmed_path.starts_with(abs_path); // This fails too + // ``` + // + // This is definitely a bug, but it's not clear if we should handle it here or not. pub fn abs_path(&self) -> &Arc { - &self.abs_path + self.abs_path.as_path() } fn build_initial_update(&self, project_id: u64, worktree_id: u64) -> proto::UpdateWorktree { @@ -2132,9 +2146,9 @@ impl Snapshot { return Err(anyhow!("invalid path")); } if path.file_name().is_some() { - Ok(self.abs_path.join(path)) + Ok(self.abs_path.as_path().join(path)) } else { - Ok(self.abs_path.to_path_buf()) + Ok(self.abs_path.as_path().to_path_buf()) } } @@ -2193,7 +2207,7 @@ impl Snapshot { .and_then(|entry| entry.git_status) } - fn update_abs_path(&mut self, abs_path: Arc, root_name: String) { + fn update_abs_path(&mut self, abs_path: SanitizedPath, root_name: String) { self.abs_path = abs_path; if root_name != self.root_name { self.root_char_bag = root_name.chars().map(|c| c.to_ascii_lowercase()).collect(); @@ -2212,7 +2226,7 @@ impl Snapshot { update.removed_entries.len() ); self.update_abs_path( - Arc::from(PathBuf::from(update.abs_path).as_path()), + SanitizedPath::from(PathBuf::from(update.abs_path)), update.root_name, ); @@ -2632,7 +2646,7 @@ impl LocalSnapshot { fn insert_entry(&mut self, mut entry: Entry, fs: &dyn Fs) -> Entry { if entry.is_file() && entry.path.file_name() == Some(&GITIGNORE) { - let abs_path = self.abs_path.join(&entry.path); + let abs_path = self.abs_path.as_path().join(&entry.path); match smol::block_on(build_gitignore(&abs_path, fs)) { Ok(ignore) => { self.ignores_by_parent_abs_path @@ -2786,8 +2800,9 @@ impl LocalSnapshot { if git_state { for ignore_parent_abs_path in self.ignores_by_parent_abs_path.keys() { - let ignore_parent_path = - ignore_parent_abs_path.strip_prefix(&self.abs_path).unwrap(); + let ignore_parent_path = ignore_parent_abs_path + .strip_prefix(self.abs_path.as_path()) + .unwrap(); assert!(self.entry_for_path(ignore_parent_path).is_some()); assert!(self .entry_for_path(ignore_parent_path.join(*GITIGNORE)) @@ -2941,7 +2956,7 @@ impl BackgroundScannerState { } if let Some(ignore) = ignore { - let abs_parent_path = self.snapshot.abs_path.join(parent_path).into(); + let abs_parent_path = self.snapshot.abs_path.as_path().join(parent_path).into(); self.snapshot .ignores_by_parent_abs_path .insert(abs_parent_path, (ignore, false)); @@ -3004,7 +3019,11 @@ impl BackgroundScannerState { } if entry.path.file_name() == Some(&GITIGNORE) { - let abs_parent_path = self.snapshot.abs_path.join(entry.path.parent().unwrap()); + let abs_parent_path = self + .snapshot + .abs_path + .as_path() + .join(entry.path.parent().unwrap()); if let Some((_, needs_update)) = self .snapshot .ignores_by_parent_abs_path @@ -3085,7 +3104,7 @@ impl BackgroundScannerState { return None; } - let dot_git_abs_path = self.snapshot.abs_path.join(&dot_git_path); + let dot_git_abs_path = self.snapshot.abs_path.as_path().join(&dot_git_path); let t0 = Instant::now(); let repository = fs.open_repo(&dot_git_abs_path)?; @@ -3299,9 +3318,9 @@ impl language::LocalFile for File { fn abs_path(&self, cx: &AppContext) -> PathBuf { let worktree_path = &self.worktree.read(cx).as_local().unwrap().abs_path; if self.path.as_ref() == Path::new("") { - worktree_path.to_path_buf() + worktree_path.as_path().to_path_buf() } else { - worktree_path.join(&self.path) + worktree_path.as_path().join(&self.path) } } @@ -3712,7 +3731,7 @@ impl BackgroundScanner { // the git repository in an ancestor directory. Find any gitignore files // in ancestor directories. let root_abs_path = self.state.lock().snapshot.abs_path.clone(); - for (index, ancestor) in root_abs_path.ancestors().enumerate() { + for (index, ancestor) in root_abs_path.as_path().ancestors().enumerate() { if index != 0 { if let Ok(ignore) = build_gitignore(&ancestor.join(*GITIGNORE), self.fs.as_ref()).await @@ -3744,7 +3763,13 @@ impl BackgroundScanner { self.state.lock().insert_git_repository_for_path( Path::new("").into(), ancestor_dot_git.into(), - Some(root_abs_path.strip_prefix(ancestor).unwrap().into()), + Some( + root_abs_path + .as_path() + .strip_prefix(ancestor) + .unwrap() + .into(), + ), self.fs.as_ref(), self.watcher.as_ref(), ); @@ -3763,12 +3788,12 @@ impl BackgroundScanner { if let Some(mut root_entry) = state.snapshot.root_entry().cloned() { let ignore_stack = state .snapshot - .ignore_stack_for_abs_path(&root_abs_path, true); - if ignore_stack.is_abs_path_ignored(&root_abs_path, true) { + .ignore_stack_for_abs_path(root_abs_path.as_path(), true); + if ignore_stack.is_abs_path_ignored(root_abs_path.as_path(), true) { root_entry.is_ignored = true; state.insert_entry(root_entry.clone(), self.fs.as_ref(), self.watcher.as_ref()); } - state.enqueue_scan_dir(root_abs_path, &root_entry, &scan_job_tx); + state.enqueue_scan_dir(root_abs_path.into(), &root_entry, &scan_job_tx); } }; @@ -3818,7 +3843,7 @@ impl BackgroundScanner { { let mut state = self.state.lock(); state.path_prefixes_to_scan.insert(path_prefix.clone()); - state.snapshot.abs_path.join(&path_prefix) + state.snapshot.abs_path.as_path().join(&path_prefix) }; if let Some(abs_path) = self.fs.canonicalize(&abs_path).await.log_err() { @@ -3845,7 +3870,7 @@ impl BackgroundScanner { self.forcibly_load_paths(&request.relative_paths).await; let root_path = self.state.lock().snapshot.abs_path.clone(); - let root_canonical_path = match self.fs.canonicalize(&root_path).await { + let root_canonical_path = match self.fs.canonicalize(root_path.as_path()).await { Ok(path) => path, Err(err) => { log::error!("failed to canonicalize root path: {}", err); @@ -3874,7 +3899,7 @@ impl BackgroundScanner { } self.reload_entries_for_paths( - root_path, + root_path.into(), root_canonical_path, &request.relative_paths, abs_paths, @@ -3887,7 +3912,7 @@ impl BackgroundScanner { async fn process_events(&self, mut abs_paths: Vec) { let root_path = self.state.lock().snapshot.abs_path.clone(); - let root_canonical_path = match self.fs.canonicalize(&root_path).await { + let root_canonical_path = match self.fs.canonicalize(root_path.as_path()).await { Ok(path) => path, Err(err) => { let new_path = self @@ -3897,21 +3922,20 @@ impl BackgroundScanner { .root_file_handle .clone() .and_then(|handle| handle.current_path(&self.fs).log_err()) - .filter(|new_path| **new_path != *root_path); + .map(SanitizedPath::from) + .filter(|new_path| *new_path != root_path); if let Some(new_path) = new_path.as_ref() { log::info!( "root renamed from {} to {}", - root_path.display(), - new_path.display() + root_path.as_path().display(), + new_path.as_path().display() ) } else { log::warn!("root path could not be canonicalized: {}", err); } self.status_updates_tx - .unbounded_send(ScanState::RootUpdated { - new_path: new_path.map(|p| p.into()), - }) + .unbounded_send(ScanState::RootUpdated { new_path }) .ok(); return; } @@ -4006,7 +4030,7 @@ impl BackgroundScanner { let (scan_job_tx, scan_job_rx) = channel::unbounded(); log::debug!("received fs events {:?}", relative_paths); self.reload_entries_for_paths( - root_path, + root_path.into(), root_canonical_path, &relative_paths, abs_paths, @@ -4044,7 +4068,7 @@ impl BackgroundScanner { for ancestor in path.ancestors() { if let Some(entry) = state.snapshot.entry_for_path(ancestor) { if entry.kind == EntryKind::UnloadedDir { - let abs_path = root_path.join(ancestor); + let abs_path = root_path.as_path().join(ancestor); state.enqueue_scan_dir(abs_path.into(), entry, &scan_job_tx); state.paths_to_scan.insert(path.clone()); break; @@ -4548,7 +4572,7 @@ impl BackgroundScanner { snapshot .ignores_by_parent_abs_path .retain(|parent_abs_path, (_, needs_update)| { - if let Ok(parent_path) = parent_abs_path.strip_prefix(&abs_path) { + if let Ok(parent_path) = parent_abs_path.strip_prefix(abs_path.as_path()) { if *needs_update { *needs_update = false; if snapshot.snapshot.entry_for_path(parent_path).is_some() { @@ -4627,7 +4651,10 @@ impl BackgroundScanner { let mut entries_by_id_edits = Vec::new(); let mut entries_by_path_edits = Vec::new(); - let path = job.abs_path.strip_prefix(&snapshot.abs_path).unwrap(); + let path = job + .abs_path + .strip_prefix(snapshot.abs_path.as_path()) + .unwrap(); let repo = snapshot.repo_for_path(path); for mut entry in snapshot.child_entries(path).cloned() { let was_ignored = entry.is_ignored; diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index cfc11ade3f..c598054356 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -1124,10 +1124,7 @@ impl ToString for IdType { fn parse_url_arg(arg: &str, cx: &AppContext) -> Result { match std::fs::canonicalize(Path::new(&arg)) { - Ok(path) => Ok(format!( - "file://{}", - path.to_string_lossy().trim_start_matches(r#"\\?\"#) - )), + Ok(path) => Ok(format!("file://{}", path.display())), Err(error) => { if arg.starts_with("file://") || arg.starts_with("zed-cli://") From 0c8e5550e7dc2c343e9a387eb1af9dd92d1b720b Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 27 Nov 2024 10:47:23 -0800 Subject: [PATCH 172/886] Make Markdown images layout vertically instead of horizontally (#21247) Release Notes: - Fixed a bug in the Markdown preview where images in the same paragraph would be rendered next to each other --- crates/markdown_preview/src/markdown_renderer.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/markdown_preview/src/markdown_renderer.rs b/crates/markdown_preview/src/markdown_renderer.rs index 6140372e0b..39bcd546df 100644 --- a/crates/markdown_preview/src/markdown_renderer.rs +++ b/crates/markdown_preview/src/markdown_renderer.rs @@ -417,6 +417,7 @@ fn render_markdown_paragraph(parsed: &MarkdownParagraph, cx: &mut RenderContext) cx.with_common_p(div()) .children(render_markdown_text(parsed, cx)) .flex() + .flex_col() .into_any_element() } From 34ed48e14bcf48a2dea2bc9b237bc669601e0a88 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 27 Nov 2024 23:17:44 +0200 Subject: [PATCH 173/886] Add a split button to terminal panes (#21251) Follow-up of https://github.com/zed-industries/zed/pull/21238 image Release Notes: - N/A --- crates/terminal_view/src/terminal_panel.rs | 23 ++++++++++++++++++++-- crates/workspace/src/pane.rs | 2 +- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 38b2eda676..4d8d197aea 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -36,8 +36,8 @@ use workspace::{ move_item, pane, ui::IconName, ActivateNextPane, ActivatePane, ActivatePaneInDirection, ActivatePreviousPane, DraggedTab, - ItemId, NewTerminal, Pane, PaneGroup, SplitDirection, SwapPaneInDirection, ToggleZoom, - Workspace, + ItemId, NewTerminal, Pane, PaneGroup, SplitDirection, SplitDown, SplitLeft, SplitRight, + SplitUp, SwapPaneInDirection, ToggleZoom, Workspace, }; use anyhow::Result; @@ -166,6 +166,25 @@ impl TerminalPanel { Some(menu) }), ) + .child( + PopoverMenu::new("terminal-pane-tab-bar-split") + .trigger( + IconButton::new("terminal-pane-split", IconName::Split) + .icon_size(IconSize::Small) + .tooltip(|cx| Tooltip::text("Split Pane", cx)), + ) + .anchor(AnchorCorner::TopRight) + .with_handle(pane.split_item_context_menu_handle.clone()) + .menu(move |cx| { + ContextMenu::build(cx, |menu, _| { + menu.action("Split Right", SplitRight.boxed_clone()) + .action("Split Left", SplitLeft.boxed_clone()) + .action("Split Up", SplitUp.boxed_clone()) + .action("Split Down", SplitDown.boxed_clone()) + }) + .into() + }), + ) .child({ let zoomed = pane.is_zoomed(); IconButton::new("toggle_zoom", IconName::Maximize) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 69485846e9..292f59eba8 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -303,7 +303,7 @@ pub struct Pane { double_click_dispatch_action: Box, save_modals_spawned: HashSet, pub new_item_context_menu_handle: PopoverMenuHandle, - split_item_context_menu_handle: PopoverMenuHandle, + pub split_item_context_menu_handle: PopoverMenuHandle, pinned_tab_count: usize, } From e803815b1645b551b096fc77f16a3d7485c6fdd7 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 28 Nov 2024 00:06:23 +0200 Subject: [PATCH 174/886] Use proper context to show terminal split menu bindings (#21253) Follow-up of https://github.com/zed-industries/zed/pull/21251 Show proper keybindings on the terminal split button: image Release Notes: - N/A --- crates/terminal_view/src/terminal_panel.rs | 25 ++++++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 4d8d197aea..1bc8a9e19b 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -26,8 +26,8 @@ use terminal::{ Terminal, }; use ui::{ - div, h_flex, ButtonCommon, Clickable, ContextMenu, IconButton, IconSize, InteractiveElement, - PopoverMenu, Selectable, Tooltip, + div, h_flex, ButtonCommon, Clickable, ContextMenu, FluentBuilder, IconButton, IconSize, + InteractiveElement, PopoverMenu, Selectable, Tooltip, }; use util::{ResultExt, TryFutureExt}; use workspace::{ @@ -130,6 +130,10 @@ impl TerminalPanel { let assistant_tab_bar_button = self.assistant_tab_bar_button.clone(); terminal_pane.update(cx, |pane, cx| { pane.set_render_tab_bar_buttons(cx, move |pane, cx| { + let split_context = pane + .items() + .find_map(|item| item.downcast::()) + .map(|terminal_view| terminal_view.read(cx).focus_handle.clone()); if !pane.has_focus(cx) && !pane.context_menu_focused(cx) { return (None, None); } @@ -175,14 +179,21 @@ impl TerminalPanel { ) .anchor(AnchorCorner::TopRight) .with_handle(pane.split_item_context_menu_handle.clone()) - .menu(move |cx| { - ContextMenu::build(cx, |menu, _| { - menu.action("Split Right", SplitRight.boxed_clone()) + .menu({ + let split_context = split_context.clone(); + move |cx| { + ContextMenu::build(cx, |menu, _| { + menu.when_some( + split_context.clone(), + |menu, split_context| menu.context(split_context), + ) + .action("Split Right", SplitRight.boxed_clone()) .action("Split Left", SplitLeft.boxed_clone()) .action("Split Up", SplitUp.boxed_clone()) .action("Split Down", SplitDown.boxed_clone()) - }) - .into() + }) + .into() + } }), ) .child({ From 66ba9d5b4b27bc26571e6cb98b08cb46b7a0ae41 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 28 Nov 2024 00:30:33 +0200 Subject: [PATCH 175/886] Use item context for pane tab context menu (#21254) This allows to show proper override values for terminal tabs in Linux and Windows. Release Notes: - Fixed incorrect "close tab" keybinding shown in context menu of the terminal panel tabs on Linux and Windows --- crates/workspace/src/pane.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 292f59eba8..dc7b92a13b 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -2075,8 +2075,10 @@ impl Pane { let is_pinned = self.is_tab_pinned(ix); let pane = cx.view().downgrade(); + let menu_context = item.focus_handle(cx); right_click_menu(ix).trigger(tab).menu(move |cx| { let pane = pane.clone(); + let menu_context = menu_context.clone(); ContextMenu::build(cx, move |mut menu, cx| { if let Some(pane) = pane.upgrade() { menu = menu @@ -2255,7 +2257,7 @@ impl Pane { } } - menu + menu.context(menu_context) }) }) } From 04ff9f060cf9eeb3b848c858f80d6c882bb5cc20 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Thu, 28 Nov 2024 00:54:01 +0100 Subject: [PATCH 176/886] Improve runnable detection for JavaScript files (#21246) Closes #21242 ![Screenshot 2024-11-27 at 18 52 51](https://github.com/user-attachments/assets/d096197c-33d2-41b9-963d-3e1a9bbdc035) ![Screenshot 2024-11-27 at 18 53 08](https://github.com/user-attachments/assets/b3202b00-3f68-4d9d-acc2-1b86c081fc34) Release Notes: - Improved runnable detection for JavaScript/Typescript files. --- crates/languages/src/javascript/outline.scm | 20 +++++++++++++------ crates/languages/src/javascript/runnables.scm | 15 ++++++++++---- crates/languages/src/tsx/outline.scm | 20 +++++++++++++------ crates/languages/src/tsx/runnables.scm | 19 ++++++++++++------ crates/languages/src/typescript/outline.scm | 20 +++++++++++++------ crates/languages/src/typescript/runnables.scm | 19 ++++++++++++------ 6 files changed, 79 insertions(+), 34 deletions(-) diff --git a/crates/languages/src/javascript/outline.scm b/crates/languages/src/javascript/outline.scm index c5ec3d36dd..da6a1e0d31 100644 --- a/crates/languages/src/javascript/outline.scm +++ b/crates/languages/src/javascript/outline.scm @@ -62,12 +62,20 @@ name: (_) @name) @item ; Add support for (node:test, bun:test and Jest) runnable -(call_expression - function: (_) @context - (#any-of? @context "it" "test" "describe") - arguments: ( - arguments . (string - (string_fragment) @name +( + (call_expression + function: [ + (identifier) @_name + (member_expression + object: [ + (identifier) @_name + (member_expression object: (identifier) @_name) + ]* + ) + ]* @context + (#any-of? @_name "it" "test" "describe") + arguments: ( + arguments . (string (string_fragment) @name) ) ) ) @item diff --git a/crates/languages/src/javascript/runnables.scm b/crates/languages/src/javascript/runnables.scm index 37f48e1df8..615bd2f51a 100644 --- a/crates/languages/src/javascript/runnables.scm +++ b/crates/languages/src/javascript/runnables.scm @@ -2,13 +2,20 @@ ; Function expression that has `it`, `test` or `describe` as the function name ( (call_expression - function: (_) @_name + function: [ + (identifier) @_name + (member_expression + object: [ + (identifier) @_name + (member_expression object: (identifier) @_name) + ]* + ) + ] (#any-of? @_name "it" "test" "describe") arguments: ( - arguments . (string - (string_fragment) @run - ) + arguments . (string (string_fragment) @run) ) ) @_js-test + (#set! tag js-test) ) diff --git a/crates/languages/src/tsx/outline.scm b/crates/languages/src/tsx/outline.scm index 0c3589071d..14dbf1cc0a 100644 --- a/crates/languages/src/tsx/outline.scm +++ b/crates/languages/src/tsx/outline.scm @@ -70,12 +70,20 @@ name: (_) @name) @item ; Add support for (node:test, bun:test and Jest) runnable -(call_expression - function: (_) @context - (#any-of? @context "it" "test" "describe") - arguments: ( - arguments . (string - (string_fragment) @name +( + (call_expression + function: [ + (identifier) @_name + (member_expression + object: [ + (identifier) @_name + (member_expression object: (identifier) @_name) + ]* + ) + ]* @context + (#any-of? @_name "it" "test" "describe") + arguments: ( + arguments . (string (string_fragment) @name) ) ) ) @item diff --git a/crates/languages/src/tsx/runnables.scm b/crates/languages/src/tsx/runnables.scm index 68c81d04c7..615bd2f51a 100644 --- a/crates/languages/src/tsx/runnables.scm +++ b/crates/languages/src/tsx/runnables.scm @@ -2,13 +2,20 @@ ; Function expression that has `it`, `test` or `describe` as the function name ( (call_expression - function: (_) @_name + function: [ + (identifier) @_name + (member_expression + object: [ + (identifier) @_name + (member_expression object: (identifier) @_name) + ]* + ) + ] (#any-of? @_name "it" "test" "describe") arguments: ( - arguments . (string - (string_fragment) @run - ) + arguments . (string (string_fragment) @run) ) - ) @_tsx-test - (#set! tag tsx-test) + ) @_js-test + + (#set! tag js-test) ) diff --git a/crates/languages/src/typescript/outline.scm b/crates/languages/src/typescript/outline.scm index 0c3589071d..14dbf1cc0a 100644 --- a/crates/languages/src/typescript/outline.scm +++ b/crates/languages/src/typescript/outline.scm @@ -70,12 +70,20 @@ name: (_) @name) @item ; Add support for (node:test, bun:test and Jest) runnable -(call_expression - function: (_) @context - (#any-of? @context "it" "test" "describe") - arguments: ( - arguments . (string - (string_fragment) @name +( + (call_expression + function: [ + (identifier) @_name + (member_expression + object: [ + (identifier) @_name + (member_expression object: (identifier) @_name) + ]* + ) + ]* @context + (#any-of? @_name "it" "test" "describe") + arguments: ( + arguments . (string (string_fragment) @name) ) ) ) @item diff --git a/crates/languages/src/typescript/runnables.scm b/crates/languages/src/typescript/runnables.scm index 21a965fd31..615bd2f51a 100644 --- a/crates/languages/src/typescript/runnables.scm +++ b/crates/languages/src/typescript/runnables.scm @@ -2,13 +2,20 @@ ; Function expression that has `it`, `test` or `describe` as the function name ( (call_expression - function: (_) @_name + function: [ + (identifier) @_name + (member_expression + object: [ + (identifier) @_name + (member_expression object: (identifier) @_name) + ]* + ) + ] (#any-of? @_name "it" "test" "describe") arguments: ( - arguments . (string - (string_fragment) @run - ) + arguments . (string (string_fragment) @run) ) - ) @_ts-test - (#set! tag ts-test) + ) @_js-test + + (#set! tag js-test) ) From 461ab24a0618484644b4e8732060e70bc2b5c0c6 Mon Sep 17 00:00:00 2001 From: Jared Ramirez Date: Wed, 27 Nov 2024 22:04:11 -0800 Subject: [PATCH 177/886] Update nix cargo hash (#21257) Closes https://github.com/zed-industries/zed/issues/21256 Release Notes: - N/A --- nix/build.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/build.nix b/nix/build.nix index 903f9790c7..d3d3d1aab1 100644 --- a/nix/build.nix +++ b/nix/build.nix @@ -90,7 +90,7 @@ rustPlatform.buildRustPackage rec { ]; useFetchCargoVendor = true; - cargoHash = "sha256-xL/EBe3+rlaPwU2zZyQtsZNHGBjzAD8ZCWrQXCQVxm8="; + cargoHash = "sha256-KURM1W9UP65BU9gbvEBgQj3jwSYfQT7X18gcSmOMguI="; nativeBuildInputs = [ From e9e260776bba12a0427e875d0cd29914ab8220cc Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Thu, 28 Nov 2024 16:08:07 +0800 Subject: [PATCH 178/886] gpui: Fix default colors blue, red, green to match in CSS default colors (#20851) Release Notes: - N/A --- This change to let the default colors to 100% match with CSS default colors. And update the methods to as `const`. Here is an example: image https://codepen.io/huacnlee/pen/ZEgNXJZ But the before version for example blue: `h: 0.6 * 360 = 216`, but we expected `240`, `240 / 360 = 0.666666666`, so the before version are lose the precision. (Here is a test tool: https://hslpicker.com/#0000FF) ## After Update ```bash cargo run -p gpui --example hello_world ``` image --- crates/gpui/examples/hello_world.rs | 19 ++++++++++++--- crates/gpui/src/color.rs | 36 ++++++++++++++--------------- 2 files changed, 34 insertions(+), 21 deletions(-) diff --git a/crates/gpui/examples/hello_world.rs b/crates/gpui/examples/hello_world.rs index 961212fa62..57312c06bb 100644 --- a/crates/gpui/examples/hello_world.rs +++ b/crates/gpui/examples/hello_world.rs @@ -8,8 +8,10 @@ impl Render for HelloWorld { fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { div() .flex() - .bg(rgb(0x2e7d32)) - .size(Length::Definite(Pixels(300.0).into())) + .flex_col() + .gap_3() + .bg(rgb(0x505050)) + .size(Length::Definite(Pixels(500.0).into())) .justify_center() .items_center() .shadow_lg() @@ -18,12 +20,23 @@ impl Render for HelloWorld { .text_xl() .text_color(rgb(0xffffff)) .child(format!("Hello, {}!", &self.text)) + .child( + div() + .flex() + .gap_2() + .child(div().size_8().bg(gpui::red())) + .child(div().size_8().bg(gpui::green())) + .child(div().size_8().bg(gpui::blue())) + .child(div().size_8().bg(gpui::yellow())) + .child(div().size_8().bg(gpui::black())) + .child(div().size_8().bg(gpui::white())), + ) } } fn main() { App::new().run(|cx: &mut AppContext| { - let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx); + let bounds = Bounds::centered(None, size(px(500.), px(500.0)), cx); cx.open_window( WindowOptions { window_bounds: Some(WindowBounds::Windowed(bounds)), diff --git a/crates/gpui/src/color.rs b/crates/gpui/src/color.rs index 9c831d0875..04a35e6886 100644 --- a/crates/gpui/src/color.rs +++ b/crates/gpui/src/color.rs @@ -314,7 +314,7 @@ pub fn hsla(h: f32, s: f32, l: f32, a: f32) -> Hsla { } /// Pure black in [`Hsla`] -pub fn black() -> Hsla { +pub const fn black() -> Hsla { Hsla { h: 0., s: 0., @@ -324,7 +324,7 @@ pub fn black() -> Hsla { } /// Transparent black in [`Hsla`] -pub fn transparent_black() -> Hsla { +pub const fn transparent_black() -> Hsla { Hsla { h: 0., s: 0., @@ -334,7 +334,7 @@ pub fn transparent_black() -> Hsla { } /// Transparent black in [`Hsla`] -pub fn transparent_white() -> Hsla { +pub const fn transparent_white() -> Hsla { Hsla { h: 0., s: 0., @@ -354,7 +354,7 @@ pub fn opaque_grey(lightness: f32, opacity: f32) -> Hsla { } /// Pure white in [`Hsla`] -pub fn white() -> Hsla { +pub const fn white() -> Hsla { Hsla { h: 0., s: 0., @@ -364,7 +364,7 @@ pub fn white() -> Hsla { } /// The color red in [`Hsla`] -pub fn red() -> Hsla { +pub const fn red() -> Hsla { Hsla { h: 0., s: 1., @@ -374,9 +374,9 @@ pub fn red() -> Hsla { } /// The color blue in [`Hsla`] -pub fn blue() -> Hsla { +pub const fn blue() -> Hsla { Hsla { - h: 0.6, + h: 0.6666666667, s: 1., l: 0.5, a: 1., @@ -384,19 +384,19 @@ pub fn blue() -> Hsla { } /// The color green in [`Hsla`] -pub fn green() -> Hsla { +pub const fn green() -> Hsla { Hsla { - h: 0.33, + h: 0.3333333333, s: 1., - l: 0.5, + l: 0.25, a: 1., } } /// The color yellow in [`Hsla`] -pub fn yellow() -> Hsla { +pub const fn yellow() -> Hsla { Hsla { - h: 0.16, + h: 0.1666666667, s: 1., l: 0.5, a: 1., @@ -410,32 +410,32 @@ impl Hsla { } /// The color red - pub fn red() -> Self { + pub const fn red() -> Self { red() } /// The color green - pub fn green() -> Self { + pub const fn green() -> Self { green() } /// The color blue - pub fn blue() -> Self { + pub const fn blue() -> Self { blue() } /// The color black - pub fn black() -> Self { + pub const fn black() -> Self { black() } /// The color white - pub fn white() -> Self { + pub const fn white() -> Self { white() } /// The color transparent black - pub fn transparent_black() -> Self { + pub const fn transparent_black() -> Self { transparent_black() } From a4584c9d13876d6cb2b3fb6d4fd7f881ca359808 Mon Sep 17 00:00:00 2001 From: Stanislav Alekseev <43210583+WeetHet@users.noreply.github.com> Date: Thu, 28 Nov 2024 10:31:12 +0200 Subject: [PATCH 179/886] Add an uninstall script (#21213) Closes #14306 This looks at what #16660 did and install.sh script as a base for the uninstall.sh script. The script is bundled with the cli by default unless the cli/no-bundled-uninstall feature is selected which is done, so package managers could build zed without bundling a useless feature and increasing binary size. I don't have capabilities to test this right now, so any help with that is appreciated. Release Notes: - Added an uninstall script for Zed installations done via zed.dev. To uninstall zed, run `zed --uninstall` via the CLI binary. --- crates/cli/Cargo.toml | 4 ++ crates/cli/build.rs | 5 ++ crates/cli/src/main.rs | 30 ++++++++ script/uninstall.sh | 158 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 197 insertions(+) create mode 100644 crates/cli/build.rs create mode 100644 script/uninstall.sh diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 5dd53b5a09..18f49a5691 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -16,6 +16,10 @@ doctest = false name = "cli" path = "src/main.rs" +[features] +no-bundled-uninstall = [] +default = [] + [dependencies] anyhow.workspace = true clap.workspace = true diff --git a/crates/cli/build.rs b/crates/cli/build.rs new file mode 100644 index 0000000000..399755fa28 --- /dev/null +++ b/crates/cli/build.rs @@ -0,0 +1,5 @@ +fn main() { + if std::env::var("ZED_UPDATE_EXPLANATION").is_ok() { + println!(r#"cargo:rustc-cfg=feature="no-bundled-uninstall""#); + } +} diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 002b0c0173..c8e1c8d3ed 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -59,6 +59,13 @@ struct Args { /// Run zed in dev-server mode #[arg(long)] dev_server_token: Option, + /// Uninstall Zed from user system + #[cfg(all( + any(target_os = "linux", target_os = "macos"), + not(feature = "no-bundled-uninstall") + ))] + #[arg(long)] + uninstall: bool, } fn parse_path_with_position(argument_str: &str) -> anyhow::Result { @@ -119,6 +126,29 @@ fn main() -> Result<()> { return Ok(()); } + #[cfg(all( + any(target_os = "linux", target_os = "macos"), + not(feature = "no-bundled-uninstall") + ))] + if args.uninstall { + static UNINSTALL_SCRIPT: &[u8] = include_bytes!("../../../script/uninstall.sh"); + + let tmp_dir = tempfile::tempdir()?; + let script_path = tmp_dir.path().join("uninstall.sh"); + fs::write(&script_path, UNINSTALL_SCRIPT)?; + + use std::os::unix::fs::PermissionsExt as _; + fs::set_permissions(&script_path, fs::Permissions::from_mode(0o755))?; + + let status = std::process::Command::new("sh") + .arg(&script_path) + .env("ZED_CHANNEL", &*release_channel::RELEASE_CHANNEL_NAME) + .status() + .context("Failed to execute uninstall script")?; + + std::process::exit(status.code().unwrap_or(1)); + } + let (server, server_name) = IpcOneShotServer::::new().context("Handshake before Zed spawn")?; let url = format!("zed-cli://{server_name}"); diff --git a/script/uninstall.sh b/script/uninstall.sh new file mode 100644 index 0000000000..3e460b8186 --- /dev/null +++ b/script/uninstall.sh @@ -0,0 +1,158 @@ +#!/usr/bin/env sh +set -eu + +# Uninstalls Zed that was installed using the install.sh script + +check_remaining_installations() { + platform="$(uname -s)" + if [ "$platform" = "Darwin" ]; then + # Check for any Zed variants in /Applications + remaining=$(ls -d /Applications/Zed*.app 2>/dev/null | wc -l) + [ "$remaining" -eq 0 ] + else + # Check for any Zed variants in ~/.local + remaining=$(ls -d "$HOME/.local/zed"*.app 2>/dev/null | wc -l) + [ "$remaining" -eq 0 ] + fi +} + +prompt_remove_preferences() { + printf "Do you want to keep your Zed preferences? [Y/n] " + read -r response + case "$response" in + [nN]|[nN][oO]) + rm -rf "$HOME/.config/zed" + echo "Preferences removed." + ;; + *) + echo "Preferences kept." + ;; + esac +} + +main() { + platform="$(uname -s)" + channel="${ZED_CHANNEL:-stable}" + + if [ "$platform" = "Darwin" ]; then + platform="macos" + elif [ "$platform" = "Linux" ]; then + platform="linux" + else + echo "Unsupported platform $platform" + exit 1 + fi + + "$platform" + + echo "Zed has been uninstalled" +} + +linux() { + suffix="" + if [ "$channel" != "stable" ]; then + suffix="-$channel" + fi + + appid="" + db_suffix="stable" + case "$channel" in + stable) + appid="dev.zed.Zed" + db_suffix="stable" + ;; + nightly) + appid="dev.zed.Zed-Nightly" + db_suffix="nightly" + ;; + preview) + appid="dev.zed.Zed-Preview" + db_suffix="preview" + ;; + dev) + appid="dev.zed.Zed-Dev" + db_suffix="dev" + ;; + *) + echo "Unknown release channel: ${channel}. Using stable app ID." + appid="dev.zed.Zed" + db_suffix="stable" + ;; + esac + + # Remove the app directory + rm -rf "$HOME/.local/zed$suffix.app" + + # Remove the binary symlink + rm -f "$HOME/.local/bin/zed" + + # Remove the .desktop file + rm -f "$HOME/.local/share/applications/${appid}.desktop" + + # Remove the database directory for this channel + rm -rf "$HOME/.local/share/zed/db/0-$db_suffix" + + # Remove socket file + rm -f "$HOME/.local/share/zed/zed-$db_suffix.sock" + + # Remove the entire Zed directory if no installations remain + if check_remaining_installations; then + rm -rf "$HOME/.local/share/zed" + prompt_remove_preferences + fi + + rm -rf $HOME/.zed_server +} + +macos() { + app="Zed.app" + db_suffix="stable" + app_id="dev.zed.Zed" + case "$channel" in + nightly) + app="Zed Nightly.app" + db_suffix="nightly" + app_id="dev.zed.Zed-Nightly" + ;; + preview) + app="Zed Preview.app" + db_suffix="preview" + app_id="dev.zed.Zed-Preview" + ;; + dev) + app="Zed Dev.app" + db_suffix="dev" + app_id="dev.zed.Zed-Dev" + ;; + esac + + # Remove the app bundle + if [ -d "/Applications/$app" ]; then + rm -rf "/Applications/$app" + fi + + # Remove the binary symlink + rm -f "$HOME/.local/bin/zed" + + # Remove the database directory for this channel + rm -rf "$HOME/Library/Application Support/Zed/db/0-$db_suffix" + + # Remove app-specific files and directories + rm -rf "$HOME/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments/$app_id.sfl"* + rm -rf "$HOME/Library/Caches/$app_id" + rm -rf "$HOME/Library/HTTPStorages/$app_id" + rm -rf "$HOME/Library/Preferences/$app_id.plist" + rm -rf "$HOME/Library/Saved Application State/$app_id.savedState" + + # Remove the entire Zed directory if no installations remain + if check_remaining_installations; then + rm -rf "$HOME/Library/Application Support/Zed" + rm -rf "$HOME/Library/Logs/Zed" + + prompt_remove_preferences + fi + + rm -rf $HOME/.zed_server +} + +main "$@" From c2c968f2de46018891b5958e0cfec82098e06257 Mon Sep 17 00:00:00 2001 From: feeiyu <158308373+feeiyu@users.noreply.github.com> Date: Thu, 28 Nov 2024 16:43:25 +0800 Subject: [PATCH 180/886] Enable clangd's dot-to-arrow feature (#21142) Closes #20815 ![dot2arrow1127](https://github.com/user-attachments/assets/d825f9bf-52ae-47ee-b3a3-5f952b6e8979) Release Notes: - Enabled clangd's dot-to-arrow feature --- crates/language/src/language.rs | 10 +++++++++- crates/languages/src/c.rs | 25 +++++++++++++++++++++++-- crates/lsp/src/lsp.rs | 30 +++++++++++++++++++----------- crates/project/src/lsp_store.rs | 14 +++++++++++--- 4 files changed, 62 insertions(+), 17 deletions(-) diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 58be8a4dc3..2725122990 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -30,7 +30,10 @@ use gpui::{AppContext, AsyncAppContext, Model, SharedString, Task}; pub use highlight_map::HighlightMap; use http_client::HttpClient; pub use language_registry::{LanguageName, LoadedLanguage}; -use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerName}; +use lsp::{ + CodeActionKind, InitializeParams, LanguageServerBinary, LanguageServerBinaryOptions, + LanguageServerName, +}; use parking_lot::Mutex; use regex::Regex; use schemars::{ @@ -484,6 +487,11 @@ pub trait LspAdapter: 'static + Send + Sync { fn language_ids(&self) -> HashMap { Default::default() } + + /// Support custom initialize params. + fn prepare_initialize_params(&self, original: InitializeParams) -> Result { + Ok(original) + } } async fn try_fetch_server_binary( diff --git a/crates/languages/src/c.rs b/crates/languages/src/c.rs index 8d0369f0e0..c50a16b3e4 100644 --- a/crates/languages/src/c.rs +++ b/crates/languages/src/c.rs @@ -4,10 +4,11 @@ use futures::StreamExt; use gpui::AsyncAppContext; use http_client::github::{latest_github_release, GitHubLspBinaryVersion}; pub use language::*; -use lsp::{LanguageServerBinary, LanguageServerName}; +use lsp::{InitializeParams, LanguageServerBinary, LanguageServerName}; +use serde_json::json; use smol::fs::{self, File}; use std::{any::Any, env::consts, path::PathBuf, sync::Arc}; -use util::{fs::remove_matching, maybe, ResultExt}; +use util::{fs::remove_matching, maybe, merge_json_value_into, ResultExt}; pub struct CLspAdapter; @@ -257,6 +258,26 @@ impl super::LspAdapter for CLspAdapter { filter_range, }) } + + fn prepare_initialize_params( + &self, + mut original: InitializeParams, + ) -> Result { + // enable clangd's dot-to-arrow feature. + let experimental = json!({ + "textDocument": { + "completion" : { + "editsNearCursor": true + } + } + }); + if let Some(ref mut original_experimental) = original.capabilities.experimental { + merge_json_value_into(experimental, original_experimental); + } else { + original.capabilities.experimental = Some(experimental); + } + Ok(original) + } } async fn get_cached_server_binary(container_dir: PathBuf) -> Option { diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 98755583e3..8789f5f252 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -599,22 +599,14 @@ impl LanguageServer { Ok(()) } - /// Initializes a language server by sending the `Initialize` request. - /// Note that `options` is used directly to construct [`InitializeParams`], which is why it is owned. - /// - /// [LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize) - pub fn initialize( - mut self, - options: Option, - cx: &AppContext, - ) -> Task>> { + pub fn default_initialize_params(&self, cx: &AppContext) -> InitializeParams { let root_uri = Url::from_file_path(&self.working_dir).unwrap(); #[allow(deprecated)] - let params = InitializeParams { + InitializeParams { process_id: None, root_path: None, root_uri: Some(root_uri.clone()), - initialization_options: options, + initialization_options: None, capabilities: ClientCapabilities { workspace: Some(WorkspaceClientCapabilities { configuration: Some(true), @@ -779,6 +771,22 @@ impl LanguageServer { }), locale: None, ..Default::default() + } + } + + /// Initializes a language server by sending the `Initialize` request. + /// Note that `options` is used directly to construct [`InitializeParams`], which is why it is owned. + /// + /// [LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize) + pub fn initialize( + mut self, + initialize_params: Option, + cx: &AppContext, + ) -> Task>> { + let params = if let Some(params) = initialize_params { + params + } else { + self.default_initialize_params(cx) }; cx.spawn(|_| async move { diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 6f4d23fa76..7d75347cf0 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -5673,8 +5673,6 @@ impl LspStore { .initialization_options(&(delegate)) .await?; - Self::setup_lsp_messages(this.clone(), &language_server, delegate, adapter); - match (&mut initialization_options, override_options) { (Some(initialization_options), Some(override_options)) => { merge_json_value_into(override_options, initialization_options); @@ -5683,8 +5681,18 @@ impl LspStore { _ => {} } + let initialization_params = cx.update(|cx| { + let mut params = language_server.default_initialize_params(cx); + params.initialization_options = initialization_options; + adapter.adapter.prepare_initialize_params(params) + })??; + + Self::setup_lsp_messages(this.clone(), &language_server, delegate, adapter); + let language_server = cx - .update(|cx| language_server.initialize(initialization_options, cx))? + .update(|cx| { + language_server.initialize(Some(initialization_params), cx) + })? .await .inspect_err(|_| { if let Some(this) = this.upgrade() { From 28640ac0766eda9a04b767ffd0d1ac43c8d4ad7f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 28 Nov 2024 10:55:46 +0200 Subject: [PATCH 181/886] Update astral-sh/setup-uv digest to caf0cab (#20927) 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 | |---|---|---|---| | [astral-sh/setup-uv](https://redirect.github.com/astral-sh/setup-uv) | action | digest | `2e657c1` -> `caf0cab` | --- ### 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> --- .github/workflows/community_update_all_top_ranking_issues.yml | 2 +- .../workflows/community_update_weekly_top_ranking_issues.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/community_update_all_top_ranking_issues.yml b/.github/workflows/community_update_all_top_ranking_issues.yml index af69446462..9642315bb3 100644 --- a/.github/workflows/community_update_all_top_ranking_issues.yml +++ b/.github/workflows/community_update_all_top_ranking_issues.yml @@ -12,7 +12,7 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - name: Set up uv - uses: astral-sh/setup-uv@2e657c127d5b1635d5a8e3fa40e0ac50a5bf6992 # v3 + uses: astral-sh/setup-uv@caf0cab7a618c569241d31dcd442f54681755d39 # v3 with: version: "latest" enable-cache: true diff --git a/.github/workflows/community_update_weekly_top_ranking_issues.yml b/.github/workflows/community_update_weekly_top_ranking_issues.yml index 18f525ab3b..53dcfd1d87 100644 --- a/.github/workflows/community_update_weekly_top_ranking_issues.yml +++ b/.github/workflows/community_update_weekly_top_ranking_issues.yml @@ -12,7 +12,7 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - name: Set up uv - uses: astral-sh/setup-uv@2e657c127d5b1635d5a8e3fa40e0ac50a5bf6992 # v3 + uses: astral-sh/setup-uv@caf0cab7a618c569241d31dcd442f54681755d39 # v3 with: version: "latest" enable-cache: true From 4342a93d2226c3152cadc8304e6fe4540115cb84 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 28 Nov 2024 10:55:57 +0200 Subject: [PATCH 182/886] Update Rust crate tree-sitter-c to v0.23.2 (#20938) 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 | |---|---|---|---| | [tree-sitter-c](https://redirect.github.com/tree-sitter/tree-sitter-c) | workspace.dependencies | patch | `0.23.1` -> `0.23.2` | --- ### Release Notes
tree-sitter/tree-sitter-c (tree-sitter-c) ### [`v0.23.2`](https://redirect.github.com/tree-sitter/tree-sitter-c/releases/tag/v0.23.2) [Compare Source](https://redirect.github.com/tree-sitter/tree-sitter-c/compare/v0.23.1...v0.23.2) **NOTE:** Download `tree-sitter-c.tar.xz` for the *complete* source code.
--- ### 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 f5c45f8d4a..97e92f46f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13170,9 +13170,9 @@ dependencies = [ [[package]] name = "tree-sitter-c" -version = "0.23.1" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8b3fb515e498e258799a31d78e6603767cd6892770d9e2290ec00af5c3ad80b" +checksum = "db56fadd8c3c6bc880dffcf1177c9d1c54a71a5207716db8660189082e63b587" dependencies = [ "cc", "tree-sitter-language", From 6927512e345bb8c258417e58f7b0cf25b1ac8a87 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 28 Nov 2024 10:56:21 +0200 Subject: [PATCH 183/886] Update Rust crate ashpd to 0.10.0 (#20939) 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 | |---|---|---|---| | [ashpd](https://redirect.github.com/bilelmoussaoui/ashpd) | workspace.dependencies | minor | `0.9.1` -> `0.10.0` | --- ### Release Notes
bilelmoussaoui/ashpd (ashpd) ### [`v0.10.2`](https://redirect.github.com/bilelmoussaoui/ashpd/releases/tag/0.10.2) [Compare Source](https://redirect.github.com/bilelmoussaoui/ashpd/compare/0.10.1...0.10.2) - Add `backend` feature to docs.rs ### [`v0.10.1`](https://redirect.github.com/bilelmoussaoui/ashpd/releases/tag/0.10.1) [Compare Source](https://redirect.github.com/bilelmoussaoui/ashpd/compare/0.10.0...0.10.1) #### What's Changed - desktop/activation-token: Add helper for retriving the token from a `gtk::Widget` or a `WlSurface` - desktop/secret: Close the socket after done reading - desktop/input-capture: Fix barrier-id type - desktop: Use a Pid alias all over the codebase - desktop/notification: Support v2 of the interface - Introduce backend implementation support, allowing to write a portal implementation in pure Rust. Currently, we don't support Session based portals. The backend feature is considered experimental as we might possibly introduce API breaking changes in the future but it should be good enough for getting started. Examples of how a portal can be implemented can be found in [backend-demo](https://redirect.github.com/bilelmoussaoui/ashpd/tree/master/backend-demo) **Note**: The 0.10.0 release has been yanked from crates.io as it contained a build error when the `glib` feature is enabled. ### [`v0.10.0`](https://redirect.github.com/bilelmoussaoui/ashpd/compare/0.9.2...0.10.0) [Compare Source](https://redirect.github.com/bilelmoussaoui/ashpd/compare/0.9.2...0.10.0)
--- ### 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 | 127 +++++++++++++++++++++++++++++++++++++++++++++++------ Cargo.toml | 2 +- 2 files changed, 114 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 97e92f46f3..a1727c610f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -342,20 +342,19 @@ dependencies = [ [[package]] name = "ashpd" -version = "0.9.2" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d43c03d9e36dd40cab48435be0b09646da362c278223ca535493877b2c1dee9" +checksum = "e9c39d707614dbcc6bed00015539f488d8e3fe3e66ed60961efc0c90f4b380b3" dependencies = [ - "async-fs 2.1.2", - "async-net 2.0.0", "enumflags2", "futures-channel", "futures-util", "rand 0.8.5", "serde", "serde_repr", + "tokio", "url", - "zbus", + "zbus 5.1.1", ] [[package]] @@ -7988,9 +7987,9 @@ dependencies = [ "serde", "sha2", "subtle", - "zbus", + "zbus 4.4.0", "zeroize", - "zvariant", + "zvariant 4.2.0", ] [[package]] @@ -12798,6 +12797,7 @@ dependencies = [ "signal-hook-registry", "socket2 0.5.7", "tokio-macros", + "tracing", "windows-sys 0.52.0", ] @@ -15591,9 +15591,39 @@ dependencies = [ "uds_windows", "windows-sys 0.52.0", "xdg-home", - "zbus_macros", - "zbus_names", - "zvariant", + "zbus_macros 4.4.0", + "zbus_names 3.0.0", + "zvariant 4.2.0", +] + +[[package]] +name = "zbus" +version = "5.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1162094dc63b1629fcc44150bcceeaa80798cd28bcbe7fa987b65a034c258608" +dependencies = [ + "async-broadcast", + "async-recursion 1.1.1", + "async-trait", + "enumflags2", + "event-listener 5.3.1", + "futures-core", + "futures-util", + "hex", + "nix", + "ordered-stream", + "serde", + "serde_repr", + "static_assertions", + "tokio", + "tracing", + "uds_windows", + "windows-sys 0.59.0", + "winnow 0.6.20", + "xdg-home", + "zbus_macros 5.1.1", + "zbus_names 4.1.0", + "zvariant 5.1.0", ] [[package]] @@ -15606,7 +15636,22 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.87", - "zvariant_utils", + "zvariant_utils 2.1.0", +] + +[[package]] +name = "zbus_macros" +version = "5.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd2dcdce3e2727f7d74b7e33b5a89539b3cc31049562137faf7ae4eb86cd16d" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.87", + "zbus_names 4.1.0", + "zvariant 5.1.0", + "zvariant_utils 3.0.2", ] [[package]] @@ -15617,7 +15662,19 @@ checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" dependencies = [ "serde", "static_assertions", - "zvariant", + "zvariant 4.2.0", +] + +[[package]] +name = "zbus_names" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "856b7a38811f71846fd47856ceee8bccaec8399ff53fb370247e66081ace647b" +dependencies = [ + "serde", + "static_assertions", + "winnow 0.6.20", + "zvariant 5.1.0", ] [[package]] @@ -16107,13 +16164,28 @@ name = "zvariant" version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe" +dependencies = [ + "endi", + "enumflags2", + "serde", + "static_assertions", + "zvariant_derive 4.2.0", +] + +[[package]] +name = "zvariant" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1200ee6ac32f1e5a312e455a949a4794855515d34f9909f4a3e082d14e1a56f" dependencies = [ "endi", "enumflags2", "serde", "static_assertions", "url", - "zvariant_derive", + "winnow 0.6.20", + "zvariant_derive 5.1.0", + "zvariant_utils 3.0.2", ] [[package]] @@ -16126,7 +16198,20 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.87", - "zvariant_utils", + "zvariant_utils 2.1.0", +] + +[[package]] +name = "zvariant_derive" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687e3b97fae6c9104fbbd36c73d27d149abf04fb874e2efbd84838763daa8916" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.87", + "zvariant_utils 3.0.2", ] [[package]] @@ -16139,3 +16224,17 @@ dependencies = [ "quote", "syn 2.0.87", ] + +[[package]] +name = "zvariant_utils" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20d1d011a38f12360e5fcccceeff5e2c42a8eb7f27f0dcba97a0862ede05c9c6" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "static_assertions", + "syn 2.0.87", + "winnow 0.6.20", +] diff --git a/Cargo.toml b/Cargo.toml index 71701dd8f4..996d41e803 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -333,7 +333,7 @@ alacritty_terminal = { git = "https://github.com/alacritty/alacritty", rev = "91 any_vec = "0.14" anyhow = "1.0.86" arrayvec = { version = "0.7.4", features = ["serde"] } -ashpd = "0.9.1" +ashpd = "0.10.0" async-compat = "0.2.1" async-compression = { version = "0.4", features = ["gzip", "futures-io"] } async-dispatcher = "0.1" From 38900c2321fb417d3b96c529bfa56c635c7e5c2a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 28 Nov 2024 10:56:36 +0200 Subject: [PATCH 184/886] Update Rust crate bytemuck to v1.20.0 (#20947) 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 | |---|---|---|---| | [bytemuck](https://redirect.github.com/Lokathor/bytemuck) | dependencies | minor | `1.19.0` -> `1.20.0` | --- ### Release Notes
Lokathor/bytemuck (bytemuck) ### [`v1.20.0`](https://redirect.github.com/Lokathor/bytemuck/compare/v1.19.0...v1.20.0) [Compare Source](https://redirect.github.com/Lokathor/bytemuck/compare/v1.19.0...v1.20.0)
--- ### 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 a1727c610f..f24731677d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1974,9 +1974,9 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d" +checksum = "8b37c88a63ffd85d15b406896cc343916d7cf57838a847b3a6f2ca5d39a5695a" dependencies = [ "bytemuck_derive", ] From fe30a03921191c56c725f02c3edb1ded315e6a2c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 28 Nov 2024 10:58:10 +0200 Subject: [PATCH 185/886] Update Rust crate ipc-channel to 0.19 (#20951) 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 | |---|---|---|---| | [ipc-channel](https://redirect.github.com/servo/ipc-channel) | dependencies | minor | `0.18` -> `0.19` | --- ### 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 ++-- crates/cli/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f24731677d..6082b46fa0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6284,9 +6284,9 @@ dependencies = [ [[package]] name = "ipc-channel" -version = "0.18.3" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f4c80f2df4fc64fb7fc2cff69fc034af26e6e6617ea9f1313131af464b9ca0" +checksum = "6fb8251fb7bcd9ccd3725ed8deae9fe7db8e586495c9eb5b0c52e6233e5e75ea" dependencies = [ "bincode", "crossbeam-channel", diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 18f49a5691..fedd6738ed 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -24,7 +24,7 @@ default = [] anyhow.workspace = true clap.workspace = true collections.workspace = true -ipc-channel = "0.18" +ipc-channel = "0.19" once_cell.workspace = true parking_lot.workspace = true paths.workspace = true From 4aa47a90631c69457a279f17812c165ae9ac8a6b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 28 Nov 2024 10:58:36 +0200 Subject: [PATCH 186/886] Update Rust crate rodio to 0.20.0 (#20955) 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 | |---|---|---|---| | [rodio](https://redirect.github.com/RustAudio/rodio) | dependencies | minor | `0.19.0` -> `0.20.0` | --- ### Release Notes
RustAudio/rodio (rodio) ### [`v0.20.1`](https://redirect.github.com/RustAudio/rodio/blob/HEAD/CHANGELOG.md#Version-0201-2024-11-08) [Compare Source](https://redirect.github.com/RustAudio/rodio/compare/v0.20.0...v0.20.1) ##### Fixed - Builds without the `symphonia` feature did not compile ### [`v0.20.0`](https://redirect.github.com/RustAudio/rodio/blob/HEAD/CHANGELOG.md#Version-0200-2024-11-08) [Compare Source](https://redirect.github.com/RustAudio/rodio/compare/v0.19.0...v0.20.0) ##### Added - Support for *ALAC/AIFF* - Add `automatic_gain_control` source for dynamic audio level adjustment. - New test signal generator sources: - `SignalGenerator` source generates a sine, triangle, square wave or sawtooth of a given frequency and sample rate. - `Chirp` source generates a sine wave with a linearly-increasing frequency over a given frequency range and duration. - `white` and `pink` generate white or pink noise, respectively. These sources depend on the `rand` crate and are guarded with the "noise" feature. - Documentation for the "noise" feature has been added to `lib.rs`. - New Fade and Crossfade sources: - `fade_out` fades an input out using a linear gain fade. - `linear_gain_ramp` applies a linear gain change to a sound over a given duration. `fade_out` is implemented as a `linear_gain_ramp` and `fade_in` has been refactored to use the `linear_gain_ramp` implementation. ##### Fixed - `Sink.try_seek` now updates `controls.position` before returning. Calls to `Sink.get_pos` done immediately after a seek will now return the correct value. ##### Changed - `SamplesBuffer` is now `Clone`
--- ### 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 | 5 ++--- crates/audio/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6082b46fa0..68af825b9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10286,13 +10286,12 @@ dependencies = [ [[package]] name = "rodio" -version = "0.19.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6006a627c1a38d37f3d3a85c6575418cfe34a5392d60a686d0071e1c8d427acb" +checksum = "e7ceb6607dd738c99bc8cb28eff249b7cd5c8ec88b9db96c0608c1480d140fb1" dependencies = [ "cpal", "hound", - "thiserror 1.0.69", ] [[package]] diff --git a/crates/audio/Cargo.toml b/crates/audio/Cargo.toml index 9502b58f93..f3bc173764 100644 --- a/crates/audio/Cargo.toml +++ b/crates/audio/Cargo.toml @@ -18,5 +18,5 @@ collections.workspace = true derive_more.workspace = true gpui.workspace = true parking_lot.workspace = true -rodio = { version = "0.19.0", default-features = false, features = ["wav"] } +rodio = { version = "0.20.0", default-features = false, features = ["wav"] } util.workspace = true From 1739de59d4438529ba6a4bf6ba472bc350a13eea Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 28 Nov 2024 10:58:53 +0200 Subject: [PATCH 187/886] Update Rust crate proc-macro2 to v1.0.92 (#20967) 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 | |---|---|---|---| | [proc-macro2](https://redirect.github.com/dtolnay/proc-macro2) | dependencies | patch | `1.0.89` -> `1.0.92` | --- ### Release Notes
dtolnay/proc-macro2 (proc-macro2) ### [`v1.0.92`](https://redirect.github.com/dtolnay/proc-macro2/releases/tag/1.0.92) [Compare Source](https://redirect.github.com/dtolnay/proc-macro2/compare/1.0.91...1.0.92) - Improve compiler/fallback mismatch panic message ([#​487](https://redirect.github.com/dtolnay/proc-macro2/issues/487)) ### [`v1.0.91`](https://redirect.github.com/dtolnay/proc-macro2/releases/tag/1.0.91) [Compare Source](https://redirect.github.com/dtolnay/proc-macro2/compare/1.0.90...1.0.91) - Fix panic *"compiler/fallback mismatch 949"* when using TokenStream::from_str from inside a proc macro to parse a string containing doc comment ([#​484](https://redirect.github.com/dtolnay/proc-macro2/issues/484)) ### [`v1.0.90`](https://redirect.github.com/dtolnay/proc-macro2/releases/tag/1.0.90) [Compare Source](https://redirect.github.com/dtolnay/proc-macro2/compare/1.0.89...1.0.90) - Improve error recovery in TokenStream's and Literal's FromStr implementations to work around [https://github.com/rust-lang/rust/issues/58736](https://redirect.github.com/rust-lang/rust/issues/58736) such that rustc does not poison compilation on codepaths that should be recoverable errors ([#​477](https://redirect.github.com/dtolnay/proc-macro2/issues/477), [#​478](https://redirect.github.com/dtolnay/proc-macro2/issues/478), [#​479](https://redirect.github.com/dtolnay/proc-macro2/issues/479), [#​480](https://redirect.github.com/dtolnay/proc-macro2/issues/480), [#​481](https://redirect.github.com/dtolnay/proc-macro2/issues/481), [#​482](https://redirect.github.com/dtolnay/proc-macro2/issues/482))
--- ### 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 68af825b9d..fc4de8bd7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9220,9 +9220,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.89" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] From b12a508ed9ae5b7ba42b207f68c1e9f4c9f90a78 Mon Sep 17 00:00:00 2001 From: Jaagup Averin Date: Thu, 28 Nov 2024 10:59:10 +0200 Subject: [PATCH 188/886] python: Fix highlighting for forward references (#20766) [PEP484](https://peps.python.org/pep-0484/) defines "Forward references" for undefined types. This PR treats such annotations as types rather than strings. Release Notes: - Added Python syntax highlighting for forward references. --- crates/languages/src/python/highlights.scm | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/languages/src/python/highlights.scm b/crates/languages/src/python/highlights.scm index 98ed203969..3b318fe962 100644 --- a/crates/languages/src/python/highlights.scm +++ b/crates/languages/src/python/highlights.scm @@ -18,6 +18,12 @@ (tuple (identifier) @type) ) +; Forward references +(type + (string) @type +) + + ; Function calls (decorator From 3ac119ac4edd373a896e6b98976843fd0f85679a Mon Sep 17 00:00:00 2001 From: Zach Bruggeman Date: Thu, 28 Nov 2024 01:00:45 -0800 Subject: [PATCH 189/886] Fix hovered links underline not showing when using cmd_or_ctrl for multi_cursor_modifier (#20949) I use `cmd_or_ctrl` for `multi_cursor_modifier`, but noticed that if I hovered a code reference while holding alt, it wouldn't show the underline. Instead, it would only show when pressing cmd. Looking at the code, it seems like this was just a small oversight on always checking for `modifiers.secondary`, instead of reading from the `multi_cursor_modifier` setting to determine which button was invoking link handling. --- Release Notes: - Fixed underline when hovering a code link not showing when `multi_cursor_modifier` is `cmd_or_ctrl` --- crates/editor/src/hover_links.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/crates/editor/src/hover_links.rs b/crates/editor/src/hover_links.rs index 31be9e93a9..0973f59bab 100644 --- a/crates/editor/src/hover_links.rs +++ b/crates/editor/src/hover_links.rs @@ -1,8 +1,9 @@ use crate::{ + editor_settings::MultiCursorModifier, hover_popover::{self, InlayHover}, scroll::ScrollAmount, - Anchor, Editor, EditorSnapshot, FindAllReferences, GoToDefinition, GoToTypeDefinition, - GotoDefinitionKind, InlayId, Navigated, PointForPosition, SelectPhase, + Anchor, Editor, EditorSettings, EditorSnapshot, FindAllReferences, GoToDefinition, + GoToTypeDefinition, GotoDefinitionKind, InlayId, Navigated, PointForPosition, SelectPhase, }; use gpui::{px, AppContext, AsyncWindowContext, Model, Modifiers, Task, ViewContext}; use language::{Bias, ToOffset}; @@ -12,6 +13,7 @@ use project::{ HoverBlock, HoverBlockKind, InlayHintLabelPartTooltip, InlayHintTooltip, LocationLink, Project, ResolveState, ResolvedPath, }; +use settings::Settings; use std::ops::Range; use theme::ActiveTheme as _; use util::{maybe, ResultExt, TryFutureExt as _}; @@ -117,7 +119,12 @@ impl Editor { modifiers: Modifiers, cx: &mut ViewContext, ) { - if !modifiers.secondary() || self.has_pending_selection() { + let multi_cursor_setting = EditorSettings::get_global(cx).multi_cursor_modifier; + let hovered_link_modifier = match multi_cursor_setting { + MultiCursorModifier::Alt => modifiers.secondary(), + MultiCursorModifier::CmdOrCtrl => modifiers.alt, + }; + if !hovered_link_modifier || self.has_pending_selection() { self.hide_hovered_link(cx); return; } @@ -137,7 +144,7 @@ impl Editor { snapshot, point_for_position, self, - modifiers.secondary(), + hovered_link_modifier, modifiers.shift, cx, ); From cacec06db66fe29252b0f24b08e53509812f32a6 Mon Sep 17 00:00:00 2001 From: CharlesChen0823 Date: Thu, 28 Nov 2024 17:06:48 +0800 Subject: [PATCH 190/886] search: Treat non-word char as whole-char when searching (#19152) when search somethings like `clone(`, with search options `match case sensitively` and `match whole words` in zed code base, only `clone(cx)` hit match, `clone()` will not hit math. Release Notes: - Improved buffer search for queries ending with non-letter characters --- crates/project/src/search.rs | 26 ++++++++-- crates/search/src/buffer_search.rs | 80 ++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 5 deletions(-) diff --git a/crates/project/src/search.rs b/crates/project/src/search.rs index 6a2d5032e4..0708f25410 100644 --- a/crates/project/src/search.rs +++ b/crates/project/src/search.rs @@ -3,14 +3,14 @@ use anyhow::Result; use client::proto; use fancy_regex::{Captures, Regex, RegexBuilder}; use gpui::Model; -use language::{Buffer, BufferSnapshot}; +use language::{Buffer, BufferSnapshot, CharKind}; use smol::future::yield_now; use std::{ borrow::Cow, io::{BufRead, BufReader, Read}, ops::Range, path::Path, - sync::{Arc, OnceLock}, + sync::{Arc, LazyLock, OnceLock}, }; use text::Anchor; use util::paths::PathMatcher; @@ -76,6 +76,12 @@ pub enum SearchQuery { }, } +static WORD_MATCH_TEST: LazyLock = LazyLock::new(|| { + RegexBuilder::new(r"\B") + .build() + .expect("Failed to create WORD_MATCH_TEST") +}); + impl SearchQuery { pub fn text( query: impl ToString, @@ -119,9 +125,17 @@ impl SearchQuery { let initial_query = Arc::from(query.as_str()); if whole_word { let mut word_query = String::new(); - word_query.push_str("\\b"); + if let Some(first) = query.get(0..1) { + if WORD_MATCH_TEST.is_match(first).is_ok_and(|x| !x) { + word_query.push_str("\\b"); + } + } word_query.push_str(&query); - word_query.push_str("\\b"); + if let Some(last) = query.get(query.len() - 1..) { + if WORD_MATCH_TEST.is_match(last).is_ok_and(|x| !x) { + word_query.push_str("\\b"); + } + } query = word_query } @@ -313,7 +327,9 @@ impl SearchQuery { let end_kind = classifier.kind(rope.reversed_chars_at(mat.end()).next().unwrap()); let next_kind = rope.chars_at(mat.end()).next().map(|c| classifier.kind(c)); - if Some(start_kind) == prev_kind || Some(end_kind) == next_kind { + if (Some(start_kind) == prev_kind && start_kind == CharKind::Word) + || (Some(end_kind) == next_kind && end_kind == CharKind::Word) + { continue; } } diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 41e5ba28df..b8603b8649 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -1866,6 +1866,86 @@ mod tests { .unwrap(); } + #[gpui::test] + async fn test_search_query_with_match_whole_word(cx: &mut TestAppContext) { + init_globals(cx); + let buffer_text = r#" + self.buffer.update(cx, |buffer, cx| { + buffer.edit( + edits, + Some(AutoindentMode::Block { + original_indent_columns, + }), + cx, + ) + }); + + this.buffer.update(cx, |buffer, cx| { + buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx) + }); + "# + .unindent(); + let buffer = cx.new_model(|cx| Buffer::local(buffer_text, cx)); + let cx = cx.add_empty_window(); + + let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx)); + + let search_bar = cx.new_view(|cx| { + let mut search_bar = BufferSearchBar::new(cx); + search_bar.set_active_pane_item(Some(&editor), cx); + search_bar.show(cx); + search_bar + }); + + search_bar + .update(cx, |search_bar, cx| { + search_bar.search( + "edit\\(", + Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX), + cx, + ) + }) + .await + .unwrap(); + + search_bar.update(cx, |search_bar, cx| { + search_bar.select_all_matches(&SelectAllMatches, cx); + }); + search_bar.update(cx, |_, cx| { + let all_selections = + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)); + assert_eq!( + all_selections.len(), + 2, + "Should select all `edit(` in the buffer, but got: {all_selections:?}" + ); + }); + + search_bar + .update(cx, |search_bar, cx| { + search_bar.search( + "edit(", + Some(SearchOptions::WHOLE_WORD | SearchOptions::CASE_SENSITIVE), + cx, + ) + }) + .await + .unwrap(); + + search_bar.update(cx, |search_bar, cx| { + search_bar.select_all_matches(&SelectAllMatches, cx); + }); + search_bar.update(cx, |_, cx| { + let all_selections = + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)); + assert_eq!( + all_selections.len(), + 2, + "Should select all `edit(` in the buffer, but got: {all_selections:?}" + ); + }); + } + #[gpui::test] async fn test_search_query_history(cx: &mut TestAppContext) { init_globals(cx); From 6cba467a4e218e85180ce271219d47b1923402de Mon Sep 17 00:00:00 2001 From: Gowtham K <73059450+dovakin0007@users.noreply.github.com> Date: Thu, 28 Nov 2024 16:50:10 +0530 Subject: [PATCH 191/886] project-panel: Fix playback GIF images (#21274) --- crates/image_viewer/src/image_viewer.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/image_viewer/src/image_viewer.rs b/crates/image_viewer/src/image_viewer.rs index ed87562e64..f7647223e5 100644 --- a/crates/image_viewer/src/image_viewer.rs +++ b/crates/image_viewer/src/image_viewer.rs @@ -301,7 +301,8 @@ impl Render for ImageView { img(image) .object_fit(ObjectFit::ScaleDown) .max_w_full() - .max_h_full(), + .max_h_full() + .id("img"), ), ) } From f30944543e6557b2cbb7527ebef014424043311a Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 28 Nov 2024 18:16:37 +0200 Subject: [PATCH 192/886] Do less resolves when showing the completion menu (#21286) Closes https://github.com/zed-industries/zed/issues/21205 Zed does completion resolve on every menu item selection and when applying the edit, so resolving all completion menu list is excessive indeed. In addition to that, removes the documentation-centric approach of menu resolves, as we're actually resolving these for more than that, e.g. additionalTextEdits and have to do that always, even if we do not show the documentation. Potentially, we can omit the second resolve too, but that seems relatively dangerous, and many servers remove the `data` after the first resolve, so a 2nd one is not that harmful given that we used to do much more Release Notes: - Reduced the amount of `completionItem/resolve` calls done in the completion menu --- crates/editor/src/editor.rs | 108 ++++------------- crates/editor/src/editor_tests.rs | 195 +++++++++++++++++------------- 2 files changed, 130 insertions(+), 173 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 78f0aab5a5..611ec9232e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -596,7 +596,6 @@ pub struct Editor { auto_signature_help: Option, find_all_references_task_sources: Vec, next_completion_id: CompletionId, - completion_documentation_pre_resolve_debounce: DebouncedDelay, available_code_actions: Option<(Location, Arc<[AvailableCodeAction]>)>, code_actions_task: Option>>, document_highlights_task: Option>, @@ -1006,7 +1005,7 @@ struct CompletionsMenu { matches: Arc<[StringMatch]>, selected_item: usize, scroll_handle: UniformListScrollHandle, - selected_completion_documentation_resolve_debounce: Option>>, + selected_completion_resolve_debounce: Option>>, } impl CompletionsMenu { @@ -1038,9 +1037,7 @@ impl CompletionsMenu { matches: Vec::new().into(), selected_item: 0, scroll_handle: UniformListScrollHandle::new(), - selected_completion_documentation_resolve_debounce: Some(Arc::new(Mutex::new( - DebouncedDelay::new(), - ))), + selected_completion_resolve_debounce: Some(Arc::new(Mutex::new(DebouncedDelay::new()))), } } @@ -1093,15 +1090,12 @@ impl CompletionsMenu { matches, selected_item: 0, scroll_handle: UniformListScrollHandle::new(), - selected_completion_documentation_resolve_debounce: Some(Arc::new(Mutex::new( - DebouncedDelay::new(), - ))), + selected_completion_resolve_debounce: Some(Arc::new(Mutex::new(DebouncedDelay::new()))), } } fn suppress_documentation_resolution(mut self) -> Self { - self.selected_completion_documentation_resolve_debounce - .take(); + self.selected_completion_resolve_debounce.take(); self } @@ -1113,7 +1107,7 @@ impl CompletionsMenu { self.selected_item = 0; self.scroll_handle .scroll_to_item(self.selected_item, ScrollStrategy::Top); - self.attempt_resolve_selected_completion_documentation(provider, cx); + self.resolve_selected_completion(provider, cx); cx.notify(); } @@ -1129,7 +1123,7 @@ impl CompletionsMenu { } self.scroll_handle .scroll_to_item(self.selected_item, ScrollStrategy::Top); - self.attempt_resolve_selected_completion_documentation(provider, cx); + self.resolve_selected_completion(provider, cx); cx.notify(); } @@ -1145,7 +1139,7 @@ impl CompletionsMenu { } self.scroll_handle .scroll_to_item(self.selected_item, ScrollStrategy::Top); - self.attempt_resolve_selected_completion_documentation(provider, cx); + self.resolve_selected_completion(provider, cx); cx.notify(); } @@ -1157,58 +1151,20 @@ impl CompletionsMenu { self.selected_item = self.matches.len() - 1; self.scroll_handle .scroll_to_item(self.selected_item, ScrollStrategy::Top); - self.attempt_resolve_selected_completion_documentation(provider, cx); + self.resolve_selected_completion(provider, cx); cx.notify(); } - fn pre_resolve_completion_documentation( - buffer: Model, - completions: Arc>>, - matches: Arc<[StringMatch]>, - editor: &Editor, - cx: &mut ViewContext, - ) -> Task<()> { - let settings = EditorSettings::get_global(cx); - if !settings.show_completion_documentation { - return Task::ready(()); - } - - let Some(provider) = editor.completion_provider.as_ref() else { - return Task::ready(()); - }; - - let resolve_task = provider.resolve_completions( - buffer, - matches.iter().map(|m| m.candidate_id).collect(), - completions.clone(), - cx, - ); - - cx.spawn(move |this, mut cx| async move { - if let Some(true) = resolve_task.await.log_err() { - this.update(&mut cx, |_, cx| cx.notify()).ok(); - } - }) - } - - fn attempt_resolve_selected_completion_documentation( + fn resolve_selected_completion( &mut self, provider: Option<&dyn CompletionProvider>, cx: &mut ViewContext, ) { - let settings = EditorSettings::get_global(cx); - if !settings.show_completion_documentation { - return; - } - let completion_index = self.matches[self.selected_item].candidate_id; let Some(provider) = provider else { return; }; - let Some(documentation_resolve) = self - .selected_completion_documentation_resolve_debounce - .as_ref() - else { + let Some(completion_resolve) = self.selected_completion_resolve_debounce.as_ref() else { return; }; @@ -1223,7 +1179,7 @@ impl CompletionsMenu { EditorSettings::get_global(cx).completion_documentation_secondary_query_debounce; let delay = Duration::from_millis(delay_ms); - documentation_resolve.lock().fire_new(delay, cx, |_, cx| { + completion_resolve.lock().fire_new(delay, cx, |_, cx| { cx.spawn(move |this, mut cx| async move { if let Some(true) = resolve_task.await.log_err() { this.update(&mut cx, |_, cx| cx.notify()).ok(); @@ -2118,7 +2074,6 @@ impl Editor { auto_signature_help: None, find_all_references_task_sources: Vec::new(), next_completion_id: 0, - completion_documentation_pre_resolve_debounce: DebouncedDelay::new(), next_inlay_id: 0, code_action_providers, available_code_actions: Default::default(), @@ -4523,9 +4478,9 @@ impl Editor { let sort_completions = provider.sort_completions(); let id = post_inc(&mut self.next_completion_id); - let task = cx.spawn(|this, mut cx| { + let task = cx.spawn(|editor, mut cx| { async move { - this.update(&mut cx, |this, _| { + editor.update(&mut cx, |this, _| { this.completion_tasks.retain(|(task_id, _)| *task_id >= id); })?; let completions = completions.await.log_err(); @@ -4543,34 +4498,14 @@ impl Editor { if menu.matches.is_empty() { None } else { - this.update(&mut cx, |editor, cx| { - let completions = menu.completions.clone(); - let matches = menu.matches.clone(); - - let delay_ms = EditorSettings::get_global(cx) - .completion_documentation_secondary_query_debounce; - let delay = Duration::from_millis(delay_ms); - editor - .completion_documentation_pre_resolve_debounce - .fire_new(delay, cx, |editor, cx| { - CompletionsMenu::pre_resolve_completion_documentation( - buffer, - completions, - matches, - editor, - cx, - ) - }); - }) - .ok(); Some(menu) } } else { None }; - this.update(&mut cx, |this, cx| { - let mut context_menu = this.context_menu.write(); + editor.update(&mut cx, |editor, cx| { + let mut context_menu = editor.context_menu.write(); match context_menu.as_ref() { None => {} @@ -4583,19 +4518,20 @@ impl Editor { _ => return, } - if this.focus_handle.is_focused(cx) && menu.is_some() { - let menu = menu.unwrap(); + if editor.focus_handle.is_focused(cx) && menu.is_some() { + let mut menu = menu.unwrap(); + menu.resolve_selected_completion(editor.completion_provider.as_deref(), cx); *context_menu = Some(ContextMenu::Completions(menu)); drop(context_menu); - this.discard_inline_completion(false, cx); + editor.discard_inline_completion(false, cx); cx.notify(); - } else if this.completion_tasks.len() <= 1 { + } else if editor.completion_tasks.len() <= 1 { // If there are no more completion tasks and the last menu was // empty, we should hide it. If it was already hidden, we should // also show the copilot completion when available. drop(context_menu); - if this.hide_context_menu(cx).is_none() { - this.update_visible_inline_completion(cx); + if editor.hide_context_menu(cx).is_none() { + editor.update_visible_inline_completion(cx); } } })?; diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 669134ef10..b49b3fa33b 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -31,8 +31,8 @@ use project::{ project_settings::{LspSettings, ProjectSettings}, }; use serde_json::{self, json}; -use std::sync::atomic; use std::sync::atomic::AtomicUsize; +use std::sync::atomic::{self, AtomicBool}; use std::{cell::RefCell, future::Future, rc::Rc, time::Instant}; use unindent::Unindent; use util::{ @@ -10576,6 +10576,94 @@ async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppCo }, }; + let resolve_requests_number = Arc::new(AtomicUsize::new(0)); + let expect_first_item = Arc::new(AtomicBool::new(true)); + cx.lsp + .server + .on_request::({ + let closure_default_data = default_data.clone(); + let closure_resolve_requests_number = resolve_requests_number.clone(); + let closure_expect_first_item = expect_first_item.clone(); + let closure_default_commit_characters = default_commit_characters.clone(); + move |item_to_resolve, _| { + closure_resolve_requests_number.fetch_add(1, atomic::Ordering::Release); + let default_data = closure_default_data.clone(); + let default_commit_characters = closure_default_commit_characters.clone(); + let expect_first_item = closure_expect_first_item.clone(); + async move { + if expect_first_item.load(atomic::Ordering::Acquire) { + assert_eq!( + item_to_resolve.label, "Some(2)", + "Should have selected the first item" + ); + assert_eq!( + item_to_resolve.data, + Some(json!({ "very": "special"})), + "First item should bring its own data for resolving" + ); + assert_eq!( + item_to_resolve.commit_characters, + Some(default_commit_characters), + "First item had no own commit characters and should inherit the default ones" + ); + assert!( + matches!( + item_to_resolve.text_edit, + Some(lsp::CompletionTextEdit::InsertAndReplace { .. }) + ), + "First item should bring its own edit range for resolving" + ); + assert_eq!( + item_to_resolve.insert_text_format, + Some(default_insert_text_format), + "First item had no own insert text format and should inherit the default one" + ); + assert_eq!( + item_to_resolve.insert_text_mode, + Some(lsp::InsertTextMode::ADJUST_INDENTATION), + "First item should bring its own insert text mode for resolving" + ); + Ok(item_to_resolve) + } else { + assert_eq!( + item_to_resolve.label, "vec![2]", + "Should have selected the last item" + ); + assert_eq!( + item_to_resolve.data, + Some(default_data), + "Last item has no own resolve data and should inherit the default one" + ); + assert_eq!( + item_to_resolve.commit_characters, + Some(default_commit_characters), + "Last item had no own commit characters and should inherit the default ones" + ); + assert_eq!( + item_to_resolve.text_edit, + Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: default_edit_range, + new_text: "vec![2]".to_string() + })), + "Last item had no own edit range and should inherit the default one" + ); + assert_eq!( + item_to_resolve.insert_text_format, + Some(lsp::InsertTextFormat::PLAIN_TEXT), + "Last item should bring its own insert text format for resolving" + ); + assert_eq!( + item_to_resolve.insert_text_mode, + Some(default_insert_text_mode), + "Last item had no own insert text mode and should inherit the default one" + ); + + Ok(item_to_resolve) + } + } + } + }).detach(); + let completion_data = default_data.clone(); let completion_characters = default_commit_characters.clone(); cx.handle_request::(move |_, _, _| { @@ -10623,7 +10711,7 @@ async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppCo cx.condition(|editor, _| editor.context_menu_visible()) .await; - + cx.run_until_parked(); cx.update_editor(|editor, _| { let menu = editor.context_menu.read(); match menu.as_ref().expect("should have the completions menu") { @@ -10640,99 +10728,32 @@ async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppCo ContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"), } }); + assert_eq!( + resolve_requests_number.load(atomic::Ordering::Acquire), + 1, + "While there are 2 items in the completion list, only 1 resolve request should have been sent, for the selected item" + ); cx.update_editor(|editor, cx| { editor.context_menu_first(&ContextMenuFirst, cx); }); - let first_item_resolve_characters = default_commit_characters.clone(); - cx.handle_request::(move |_, item_to_resolve, _| { - let default_commit_characters = first_item_resolve_characters.clone(); - - async move { - assert_eq!( - item_to_resolve.label, "Some(2)", - "Should have selected the first item" - ); - assert_eq!( - item_to_resolve.data, - Some(json!({ "very": "special"})), - "First item should bring its own data for resolving" - ); - assert_eq!( - item_to_resolve.commit_characters, - Some(default_commit_characters), - "First item had no own commit characters and should inherit the default ones" - ); - assert!( - matches!( - item_to_resolve.text_edit, - Some(lsp::CompletionTextEdit::InsertAndReplace { .. }) - ), - "First item should bring its own edit range for resolving" - ); - assert_eq!( - item_to_resolve.insert_text_format, - Some(default_insert_text_format), - "First item had no own insert text format and should inherit the default one" - ); - assert_eq!( - item_to_resolve.insert_text_mode, - Some(lsp::InsertTextMode::ADJUST_INDENTATION), - "First item should bring its own insert text mode for resolving" - ); - Ok(item_to_resolve) - } - }) - .next() - .await - .unwrap(); + cx.run_until_parked(); + assert_eq!( + resolve_requests_number.load(atomic::Ordering::Acquire), + 2, + "After re-selecting the first item, another resolve request should have been sent" + ); + expect_first_item.store(false, atomic::Ordering::Release); cx.update_editor(|editor, cx| { editor.context_menu_last(&ContextMenuLast, cx); }); - cx.handle_request::(move |_, item_to_resolve, _| { - let default_data = default_data.clone(); - let default_commit_characters = default_commit_characters.clone(); - async move { - assert_eq!( - item_to_resolve.label, "vec![2]", - "Should have selected the last item" - ); - assert_eq!( - item_to_resolve.data, - Some(default_data), - "Last item has no own resolve data and should inherit the default one" - ); - assert_eq!( - item_to_resolve.commit_characters, - Some(default_commit_characters), - "Last item had no own commit characters and should inherit the default ones" - ); - assert_eq!( - item_to_resolve.text_edit, - Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { - range: default_edit_range, - new_text: "vec![2]".to_string() - })), - "Last item had no own edit range and should inherit the default one" - ); - assert_eq!( - item_to_resolve.insert_text_format, - Some(lsp::InsertTextFormat::PLAIN_TEXT), - "Last item should bring its own insert text format for resolving" - ); - assert_eq!( - item_to_resolve.insert_text_mode, - Some(default_insert_text_mode), - "Last item had no own insert text mode and should inherit the default one" - ); - - Ok(item_to_resolve) - } - }) - .next() - .await - .unwrap(); + cx.run_until_parked(); + assert_eq!( + resolve_requests_number.load(atomic::Ordering::Acquire), + 3, + "After selecting the other item, another resolve request should have been sent" + ); } #[gpui::test] From 301a8900a5b7e3fda5d5ae01c8070b1023e4d558 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Thu, 28 Nov 2024 13:39:49 -0300 Subject: [PATCH 193/886] Add consistency between buffer and project search design (#20754) Follow up to https://github.com/zed-industries/zed/pull/20242 This PR adds the `SearchInputWidth` util, which sets a threshold container size in which an input's width stops filling the available space. In practice, this is in place to make the buffer and project search input fill the whole container width up to a certain point (where this point is really an arbitrary number that can be fine-tuned per taste). For folks using huge monitors, the UX isn't excellent if you have a gigantic input. In the future, upon further review, maybe it makes more sense to reorganize this code better, baking it in as a default behavior of the input component. Or even exposing this is a function many other components could use, given we may want to have dynamic width in different scenarios. For now, I just wanted to make the design of these search UIs better and more consistent. | Buffer Search | Project Search | |--------|--------| | Screenshot 2024-11-15 at 20 39 21 | Screenshot 2024-11-15 at 20 39 24 | Release Notes: - N/A --- crates/search/src/buffer_search.rs | 125 ++++++++++++++-------------- crates/search/src/project_search.rs | 38 +++++---- crates/ui/src/utils.rs | 2 + crates/ui/src/utils/search_input.rs | 22 +++++ 4 files changed, 109 insertions(+), 78 deletions(-) create mode 100644 crates/ui/src/utils/search_input.rs diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index b8603b8649..5b1a482f5e 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -27,7 +27,10 @@ use settings::Settings; use std::sync::Arc; use theme::ThemeSettings; -use ui::{h_flex, prelude::*, IconButton, IconButtonShape, IconName, Tooltip, BASE_REM_SIZE_IN_PX}; +use ui::{ + h_flex, prelude::*, utils::SearchInputWidth, IconButton, IconButtonShape, IconName, Tooltip, + BASE_REM_SIZE_IN_PX, +}; use util::ResultExt; use workspace::{ item::ItemHandle, @@ -38,8 +41,6 @@ use workspace::{ pub use registrar::DivRegistrar; use registrar::{ForDeployed, ForDismissed, SearchActionsRegistrar, WithResults}; -const MIN_INPUT_WIDTH_REMS: f32 = 10.; -const MAX_INPUT_WIDTH_REMS: f32 = 30.; const MAX_BUFFER_SEARCH_HISTORY_SIZE: usize = 50; #[derive(PartialEq, Clone, Deserialize)] @@ -160,12 +161,12 @@ impl Render for BufferSearchBar { query_editor.placeholder_text(cx).is_none() }) { self.query_editor.update(cx, |editor, cx| { - editor.set_placeholder_text("Search", cx); + editor.set_placeholder_text("Search…", cx); }); } self.replacement_editor.update(cx, |editor, cx| { - editor.set_placeholder_text("Replace with...", cx); + editor.set_placeholder_text("Replace with…", cx); }); let mut text_color = Color::Default; @@ -203,21 +204,26 @@ impl Render for BufferSearchBar { cx.theme().colors().border }; + let container_width = cx.viewport_size().width; + let input_width = SearchInputWidth::calc_width(container_width); + + let input_base_styles = || { + h_flex() + .w(input_width) + .h_8() + .px_2() + .py_1() + .border_1() + .border_color(editor_border) + .rounded_lg() + }; + let search_line = h_flex() .gap_2() .child( - h_flex() + input_base_styles() .id("editor-scroll") .track_scroll(&self.editor_scroll_handle) - .flex_1() - .h_8() - .px_2() - .py_1() - .border_1() - .border_color(editor_border) - .min_w(rems(MIN_INPUT_WIDTH_REMS)) - .max_w(rems(MAX_INPUT_WIDTH_REMS)) - .rounded_lg() .child(self.render_text_input(&self.query_editor, text_color.color(cx), cx)) .when(!hide_inline_icons, |div| { div.children(supported_options.case.then(|| { @@ -249,8 +255,8 @@ impl Render for BufferSearchBar { ) .child( h_flex() - .flex_none() - .gap_0p5() + .gap_1() + .min_w_64() .when(supported_options.replacement, |this| { this.child( IconButton::new( @@ -323,20 +329,27 @@ impl Render for BufferSearchBar { } }), ) - .child(render_nav_button( - ui::IconName::ChevronLeft, - self.active_match_index.is_some(), - "Select Previous Match", - &SelectPrevMatch, - focus_handle.clone(), - )) - .child(render_nav_button( - ui::IconName::ChevronRight, - self.active_match_index.is_some(), - "Select Next Match", - &SelectNextMatch, - focus_handle.clone(), - )) + .child( + h_flex() + .pl_2() + .ml_2() + .border_l_1() + .border_color(cx.theme().colors().border_variant) + .child(render_nav_button( + ui::IconName::ChevronLeft, + self.active_match_index.is_some(), + "Select Previous Match", + &SelectPrevMatch, + focus_handle.clone(), + )) + .child(render_nav_button( + ui::IconName::ChevronRight, + self.active_match_index.is_some(), + "Select Next Match", + &SelectNextMatch, + focus_handle.clone(), + )), + ) .when(!narrow_mode, |this| { this.child(h_flex().ml_2().min_w(rems_from_px(40.)).child( Label::new(match_text).size(LabelSize::Small).color( @@ -353,30 +366,15 @@ impl Render for BufferSearchBar { let replace_line = should_show_replace_input.then(|| { h_flex() .gap_2() - .flex_1() + .child(input_base_styles().child(self.render_text_input( + &self.replacement_editor, + cx.theme().colors().text, + cx, + ))) .child( h_flex() - .flex_1() - // We're giving this a fixed height to match the height of the search input, - // which has an icon inside that is increasing its height. - .h_8() - .px_2() - .py_1() - .border_1() - .border_color(cx.theme().colors().border) - .rounded_lg() - .min_w(rems(MIN_INPUT_WIDTH_REMS)) - .max_w(rems(MAX_INPUT_WIDTH_REMS)) - .child(self.render_text_input( - &self.replacement_editor, - cx.theme().colors().text, - cx, - )), - ) - .child( - h_flex() - .flex_none() - .gap_0p5() + .min_w_64() + .gap_1() .child( IconButton::new("search-replace-next", ui::IconName::ReplaceNext) .shape(IconButtonShape::Square) @@ -418,6 +416,7 @@ impl Render for BufferSearchBar { v_flex() .id("buffer_search") + .gap_2() .track_scroll(&self.scroll_handle) .key_context(key_context) .capture_action(cx.listener(Self::tab)) @@ -446,20 +445,22 @@ impl Render for BufferSearchBar { .when(self.supported_options().selection, |this| { this.on_action(cx.listener(Self::toggle_selection)) }) - .gap_2() .child( h_flex() + .relative() .child(search_line.w_full()) .when(!narrow_mode, |div| { div.child( - IconButton::new(SharedString::from("Close"), IconName::Close) - .shape(IconButtonShape::Square) - .tooltip(move |cx| { - Tooltip::for_action("Close Search Bar", &Dismiss, cx) - }) - .on_click(cx.listener(|this, _: &ClickEvent, cx| { - this.dismiss(&Dismiss, cx) - })), + h_flex().absolute().right_0().child( + IconButton::new(SharedString::from("Close"), IconName::Close) + .shape(IconButtonShape::Square) + .tooltip(move |cx| { + Tooltip::for_action("Close Search Bar", &Dismiss, cx) + }) + .on_click(cx.listener(|this, _: &ClickEvent, cx| { + this.dismiss(&Dismiss, cx) + })), + ), ) }), ) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 8430fd1f37..3ec2ac2aba 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -34,8 +34,8 @@ use std::{ }; use theme::ThemeSettings; use ui::{ - h_flex, prelude::*, v_flex, Icon, IconButton, IconButtonShape, IconName, KeyBinding, Label, - LabelCommon, LabelSize, Selectable, Tooltip, + h_flex, prelude::*, utils::SearchInputWidth, v_flex, Icon, IconButton, IconButtonShape, + IconName, KeyBinding, Label, LabelCommon, LabelSize, Selectable, Tooltip, }; use util::paths::PathMatcher; use workspace::{ @@ -669,7 +669,7 @@ impl ProjectSearchView { let query_editor = cx.new_view(|cx| { let mut editor = Editor::single_line(cx); - editor.set_placeholder_text("Search all files...", cx); + editor.set_placeholder_text("Search all files…", cx); editor.set_text(query_text, cx); editor }); @@ -692,7 +692,7 @@ impl ProjectSearchView { ); let replacement_editor = cx.new_view(|cx| { let mut editor = Editor::single_line(cx); - editor.set_placeholder_text("Replace in project...", cx); + editor.set_placeholder_text("Replace in project…", cx); if let Some(text) = replacement_text { editor.set_text(text, cx); } @@ -1586,9 +1586,12 @@ impl Render for ProjectSearchBar { let search = search.read(cx); let focus_handle = search.focus_handle(cx); + let container_width = cx.viewport_size().width; + let input_width = SearchInputWidth::calc_width(container_width); + let input_base_styles = || { h_flex() - .w_full() + .w(input_width) .h_8() .px_2() .py_1() @@ -1701,6 +1704,10 @@ impl Render for ProjectSearchBar { .unwrap_or_else(|| "0/0".to_string()); let matches_column = h_flex() + .pl_2() + .ml_2() + .border_l_1() + .border_color(cx.theme().colors().border_variant) .child( IconButton::new("project-search-prev-match", IconName::ChevronLeft) .shape(IconButtonShape::Square) @@ -1751,13 +1758,13 @@ impl Render for ProjectSearchBar { div() .id("matches") .ml_1() - .child( - Label::new(match_text).color(if search.active_match_index.is_some() { + .child(Label::new(match_text).size(LabelSize::Small).color( + if search.active_match_index.is_some() { Color::Default } else { Color::Disabled - }), - ) + }, + )) .when(limit_reached, |el| { el.tooltip(|cx| { Tooltip::text("Search limits reached.\nTry narrowing your search.", cx) @@ -1767,9 +1774,9 @@ impl Render for ProjectSearchBar { let search_line = h_flex() .w_full() - .gap_1p5() + .gap_2() .child(query_column) - .child(h_flex().min_w_40().child(mode_column).child(matches_column)); + .child(h_flex().min_w_64().child(mode_column).child(matches_column)); let replace_line = search.replace_enabled.then(|| { let replace_column = @@ -1779,7 +1786,7 @@ impl Render for ProjectSearchBar { let replace_actions = h_flex() - .min_w_40() + .min_w_64() .gap_1() .when(search.replace_enabled, |this| { this.child( @@ -1830,7 +1837,7 @@ impl Render for ProjectSearchBar { h_flex() .w_full() - .gap_1p5() + .gap_2() .child(replace_column) .child(replace_actions) }); @@ -1838,7 +1845,7 @@ impl Render for ProjectSearchBar { let filter_line = search.filters_enabled.then(|| { h_flex() .w_full() - .gap_1p5() + .gap_2() .child( input_base_styles() .on_action( @@ -1861,12 +1868,11 @@ impl Render for ProjectSearchBar { ) .child( h_flex() - .min_w_40() + .min_w_64() .gap_1() .child( IconButton::new("project-search-opened-only", IconName::FileSearch) .shape(IconButtonShape::Square) - .icon_size(IconSize::XSmall) .selected(self.is_opened_only_enabled(cx)) .tooltip(|cx| Tooltip::text("Only Search Open Files", cx)) .on_click(cx.listener(|this, _, cx| { diff --git a/crates/ui/src/utils.rs b/crates/ui/src/utils.rs index 25477194dc..e5c591a970 100644 --- a/crates/ui/src/utils.rs +++ b/crates/ui/src/utils.rs @@ -2,8 +2,10 @@ mod color_contrast; mod format_distance; +mod search_input; mod with_rem_size; pub use color_contrast::*; pub use format_distance::*; +pub use search_input::*; pub use with_rem_size::*; diff --git a/crates/ui/src/utils/search_input.rs b/crates/ui/src/utils/search_input.rs new file mode 100644 index 0000000000..3a507f9a5a --- /dev/null +++ b/crates/ui/src/utils/search_input.rs @@ -0,0 +1,22 @@ +#![allow(missing_docs)] + +use gpui::Pixels; + +pub struct SearchInputWidth; + +impl SearchInputWidth { + /// The containzer size in which the input stops filling the whole width. + pub const THRESHOLD_WIDTH: f32 = 1200.0; + + /// The maximum width for the search input when the container is larger than the threshold. + pub const MAX_WIDTH: f32 = 1200.0; + + /// Calculates the actual width in pixels based on the container width. + pub fn calc_width(container_width: Pixels) -> Pixels { + if container_width.0 < Self::THRESHOLD_WIDTH { + container_width + } else { + Pixels(container_width.0.min(Self::MAX_WIDTH)) + } + } +} From 4a96db026c6abba56aadd666f4627edf246e5935 Mon Sep 17 00:00:00 2001 From: Matin Aniss <76515905+MatinAniss@users.noreply.github.com> Date: Fri, 29 Nov 2024 03:45:10 +1100 Subject: [PATCH 194/886] gpui: Implement hover for Windows (#20894) --- crates/gpui/src/platform/windows/events.rs | 38 +++++++++++++++++++++- crates/gpui/src/platform/windows/window.rs | 11 +++++-- crates/gpui/src/window.rs | 6 +++- 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/crates/gpui/src/platform/windows/events.rs b/crates/gpui/src/platform/windows/events.rs index 5f45d260d9..025fbba4ac 100644 --- a/crates/gpui/src/platform/windows/events.rs +++ b/crates/gpui/src/platform/windows/events.rs @@ -7,6 +7,7 @@ use windows::Win32::{ Graphics::Gdi::*, System::SystemServices::*, UI::{ + Controls::*, HiDpi::*, Input::{Ime::*, KeyboardAndMouse::*}, WindowsAndMessaging::*, @@ -43,7 +44,8 @@ pub(crate) fn handle_msg( WM_PAINT => handle_paint_msg(handle, state_ptr), WM_CLOSE => handle_close_msg(state_ptr), WM_DESTROY => handle_destroy_msg(handle, state_ptr), - WM_MOUSEMOVE => handle_mouse_move_msg(lparam, wparam, state_ptr), + WM_MOUSEMOVE => handle_mouse_move_msg(handle, lparam, wparam, state_ptr), + WM_MOUSELEAVE => handle_mouse_leave_msg(state_ptr), WM_NCMOUSEMOVE => handle_nc_mouse_move_msg(handle, lparam, state_ptr), WM_NCLBUTTONDOWN => { handle_nc_mouse_down_msg(handle, MouseButton::Left, wparam, lparam, state_ptr) @@ -234,10 +236,32 @@ fn handle_destroy_msg(handle: HWND, state_ptr: Rc) -> Opt } fn handle_mouse_move_msg( + handle: HWND, lparam: LPARAM, wparam: WPARAM, state_ptr: Rc, ) -> Option { + let mut lock = state_ptr.state.borrow_mut(); + if !lock.hovered { + lock.hovered = true; + unsafe { + TrackMouseEvent(&mut TRACKMOUSEEVENT { + cbSize: std::mem::size_of::() as u32, + dwFlags: TME_LEAVE, + hwndTrack: handle, + dwHoverTime: HOVER_DEFAULT, + }) + .log_err() + }; + if let Some(mut callback) = lock.callbacks.hovered_status_change.take() { + drop(lock); + callback(true); + state_ptr.state.borrow_mut().callbacks.hovered_status_change = Some(callback); + } + } else { + drop(lock); + } + let mut lock = state_ptr.state.borrow_mut(); if let Some(mut callback) = lock.callbacks.input.take() { let scale_factor = lock.scale_factor; @@ -272,6 +296,18 @@ fn handle_mouse_move_msg( Some(1) } +fn handle_mouse_leave_msg(state_ptr: Rc) -> Option { + let mut lock = state_ptr.state.borrow_mut(); + lock.hovered = false; + if let Some(mut callback) = lock.callbacks.hovered_status_change.take() { + drop(lock); + callback(false); + state_ptr.state.borrow_mut().callbacks.hovered_status_change = Some(callback); + } + + Some(0) +} + fn handle_syskeydown_msg( wparam: WPARAM, lparam: LPARAM, diff --git a/crates/gpui/src/platform/windows/window.rs b/crates/gpui/src/platform/windows/window.rs index f2600d3c6f..93671f9b89 100644 --- a/crates/gpui/src/platform/windows/window.rs +++ b/crates/gpui/src/platform/windows/window.rs @@ -42,6 +42,7 @@ pub struct WindowsWindowState { pub callbacks: Callbacks, pub input_handler: Option, pub system_key_handled: bool, + pub hovered: bool, pub renderer: BladeRenderer, @@ -95,6 +96,7 @@ impl WindowsWindowState { let callbacks = Callbacks::default(); let input_handler = None; let system_key_handled = false; + let hovered = false; let click_state = ClickState::new(); let system_settings = WindowsSystemSettings::new(display); let nc_button_pressed = None; @@ -110,6 +112,7 @@ impl WindowsWindowState { callbacks, input_handler, system_key_handled, + hovered, renderer, click_state, system_settings, @@ -326,6 +329,7 @@ pub(crate) struct Callbacks { pub(crate) request_frame: Option>, pub(crate) input: Option DispatchEventResult>>, pub(crate) active_status_change: Option>, + pub(crate) hovered_status_change: Option>, pub(crate) resize: Option, f32)>>, pub(crate) moved: Option>, pub(crate) should_close: Option bool>>, @@ -635,9 +639,8 @@ impl PlatformWindow for WindowsWindow { self.0.hwnd == unsafe { GetActiveWindow() } } - // is_hovered is unused on Windows. See WindowContext::is_window_hovered. fn is_hovered(&self) -> bool { - false + self.0.state.borrow().hovered } fn set_title(&mut self, title: &str) { @@ -728,7 +731,9 @@ impl PlatformWindow for WindowsWindow { self.0.state.borrow_mut().callbacks.active_status_change = Some(callback); } - fn on_hover_status_change(&self, _: Box) {} + fn on_hover_status_change(&self, callback: Box) { + self.0.state.borrow_mut().callbacks.hovered_status_change = Some(callback); + } fn on_resize(&self, callback: Box, f32)>) { self.0.state.borrow_mut().callbacks.resize = Some(callback); diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 902c699cb7..06298a81ad 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -1241,7 +1241,11 @@ impl<'a> WindowContext<'a> { /// that currently owns the mouse cursor. /// On mac, this is equivalent to `is_window_active`. pub fn is_window_hovered(&self) -> bool { - if cfg!(any(target_os = "linux", target_os = "freebsd")) { + if cfg!(any( + target_os = "windows", + target_os = "linux", + target_os = "freebsd" + )) { self.window.hovered.get() } else { self.is_window_active() From 0acd98a07e949cdd0e6de09cd0061f7fb7bd48db Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 28 Nov 2024 20:42:57 +0200 Subject: [PATCH 195/886] Do not show cursor position for empty files (#21295) Closes https://github.com/zed-industries/zed/issues/21289 Fixes most of the issues: does not display cursor position in empty multi buffers and on non-full editors. Does not fix the startup issue, as it's caused by the AssistantPanel's `ContextEditor` acting as an `Editor`, so whenever default prompts are added, those are registered as added editors, and Zed shows some line numbers for them. We cannot replace `item.act_as::(cx)` with `item.downcast::()` as then multi bufers' navigation will fall off (arguably, those line numbers do not make that much sense too, but still seem useful). This will will fix itself in the future, when assistant panel gets reworked into readonly view by default, as `assistant2` crate already shows (there's no `act_as` impl there and nothing cause issue). Since the remaining issue is minor and will go away on any focus change, and future changes will alter this, closing the original issue. Release Notes: - Improved cursor position display --- crates/go_to_line/src/cursor_position.rs | 52 ++++++++++++++---------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/crates/go_to_line/src/cursor_position.rs b/crates/go_to_line/src/cursor_position.rs index 3931cac284..4f27c64256 100644 --- a/crates/go_to_line/src/cursor_position.rs +++ b/crates/go_to_line/src/cursor_position.rs @@ -52,34 +52,44 @@ impl CursorPosition { editor .update(&mut cx, |editor, cx| { - let buffer = editor.buffer().read(cx).snapshot(cx); cursor_position.update(cx, |cursor_position, cx| { cursor_position.selected_count = SelectionStats::default(); cursor_position.selected_count.selections = editor.selections.count(); - let mut last_selection = None::>; - for selection in editor.selections.all::(cx) { - cursor_position.selected_count.characters += buffer - .text_for_range(selection.start..selection.end) - .map(|t| t.chars().count()) - .sum::(); - if last_selection - .as_ref() - .map_or(true, |last_selection| selection.id > last_selection.id) - { - last_selection = Some(selection); + match editor.mode() { + editor::EditorMode::AutoHeight { .. } + | editor::EditorMode::SingleLine { .. } => { + cursor_position.position = None } - } - for selection in editor.selections.all::(cx) { - if selection.end != selection.start { - cursor_position.selected_count.lines += - (selection.end.row - selection.start.row) as usize; - if selection.end.column != 0 { - cursor_position.selected_count.lines += 1; + editor::EditorMode::Full => { + let mut last_selection = None::>; + let buffer = editor.buffer().read(cx).snapshot(cx); + if buffer.excerpts().count() > 0 { + for selection in editor.selections.all::(cx) { + cursor_position.selected_count.characters += buffer + .text_for_range(selection.start..selection.end) + .map(|t| t.chars().count()) + .sum::(); + if last_selection.as_ref().map_or(true, |last_selection| { + selection.id > last_selection.id + }) { + last_selection = Some(selection); + } + } + for selection in editor.selections.all::(cx) { + if selection.end != selection.start { + cursor_position.selected_count.lines += + (selection.end.row - selection.start.row) as usize; + if selection.end.column != 0 { + cursor_position.selected_count.lines += 1; + } + } + } } + cursor_position.position = + last_selection.map(|s| s.head().to_point(&buffer)); } } - cursor_position.position = - last_selection.map(|s| s.head().to_point(&buffer)); + cx.notify(); }) }) From ae85ecba2d54abe2dbdfc11c408c78bc2d256aa0 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Thu, 28 Nov 2024 18:26:59 -0300 Subject: [PATCH 196/886] Make fetch slash command visible in the command selector (#21302) The `/fetch` command is naturally already accessible via the completion menu when you type / in the assistant panel, but it wasn't on the "Add Context" command selector. I think it should! It's a super nice/powerful one, and I've seen folks not knowing it existed. Side-note: maybe, in the near future, it'd be best to rename it to "`/web`, as that's an easier name to parse and assume what it does. Screenshot 2024-11-28 at 16 52 07 Release Notes: - N/A --- assets/icons/globe.svg | 1 + crates/assistant/src/assistant.rs | 3 +-- crates/assistant/src/slash_command/fetch_command.rs | 6 +++++- crates/ui/src/components/icon.rs | 9 +++++---- 4 files changed, 12 insertions(+), 7 deletions(-) create mode 100644 assets/icons/globe.svg diff --git a/assets/icons/globe.svg b/assets/icons/globe.svg new file mode 100644 index 0000000000..2082a43984 --- /dev/null +++ b/assets/icons/globe.svg @@ -0,0 +1 @@ + diff --git a/crates/assistant/src/assistant.rs b/crates/assistant/src/assistant.rs index 7e4e38e320..6d619a76b9 100644 --- a/crates/assistant/src/assistant.rs +++ b/crates/assistant/src/assistant.rs @@ -342,8 +342,7 @@ fn register_slash_commands(prompt_builder: Option>, cx: &mut slash_command_registry.register_command(terminal_command::TerminalSlashCommand, true); slash_command_registry.register_command(now_command::NowSlashCommand, false); slash_command_registry.register_command(diagnostics_command::DiagnosticsSlashCommand, true); - slash_command_registry.register_command(fetch_command::FetchSlashCommand, false); - slash_command_registry.register_command(fetch_command::FetchSlashCommand, false); + slash_command_registry.register_command(fetch_command::FetchSlashCommand, true); if let Some(prompt_builder) = prompt_builder { cx.observe_flag::({ diff --git a/crates/assistant/src/slash_command/fetch_command.rs b/crates/assistant/src/slash_command/fetch_command.rs index 4d38bb20a7..96ea05c302 100644 --- a/crates/assistant/src/slash_command/fetch_command.rs +++ b/crates/assistant/src/slash_command/fetch_command.rs @@ -108,6 +108,10 @@ impl SlashCommand for FetchSlashCommand { "Insert fetched URL contents".into() } + fn icon(&self) -> IconName { + IconName::Globe + } + fn menu_text(&self) -> String { self.description() } @@ -162,7 +166,7 @@ impl SlashCommand for FetchSlashCommand { text, sections: vec![SlashCommandOutputSection { range, - icon: IconName::AtSign, + icon: IconName::Globe, label: format!("fetch {}", url).into(), metadata: None, }], diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index 161f4c60b7..03000f0638 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -160,7 +160,6 @@ pub enum IconName { Copy, CountdownTimer, CursorIBeam, - TextSnippet, Dash, DatabaseZap, Delete, @@ -171,8 +170,8 @@ pub enum IconName { EllipsisVertical, Envelope, Escape, - Exit, ExpandVertical, + Exit, ExternalLink, Eye, File, @@ -198,6 +197,7 @@ pub enum IconName { GenericMinimize, GenericRestore, Github, + Globe, Hash, HistoryRerun, Indicator, @@ -223,13 +223,13 @@ pub enum IconName { PageUp, Pencil, Person, + PhoneIncoming, Pin, Play, Plus, PocketKnife, Public, PullRequest, - PhoneIncoming, Quote, RefreshTitle, Regex, @@ -275,6 +275,7 @@ pub enum IconName { SwatchBook, Tab, Terminal, + TextSnippet, Trash, TrashAlt, Triangle, @@ -287,11 +288,11 @@ pub enum IconName { Wand, Warning, WholeWord, + X, XCircle, ZedAssistant, ZedAssistantFilled, ZedXCopilot, - X, } impl From for Icon { From e76589107dd7677ee80d8313a2c2e2662a4023e8 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Thu, 28 Nov 2024 18:28:05 -0300 Subject: [PATCH 197/886] Improve the "go to line" modal (#21301) Just a small, mostly visual refinement to this component. Screenshot 2024-11-28 at 16 30 27 Release Notes: - N/A --- crates/go_to_line/src/go_to_line.rs | 45 ++++++++++++----------------- 1 file changed, 18 insertions(+), 27 deletions(-) diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index c848d28eaa..df673ef823 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/crates/go_to_line/src/go_to_line.rs @@ -9,7 +9,7 @@ use gpui::{ use settings::Settings; use text::{Bias, Point}; use theme::ActiveTheme; -use ui::{h_flex, prelude::*, v_flex, Label}; +use ui::prelude::*; use util::paths::FILE_ROW_COLUMN_DELIMITER; use workspace::ModalView; @@ -73,7 +73,7 @@ impl GoToLine { let last_line = editor.buffer().read(cx).snapshot(cx).max_point().row; let scroll_position = active_editor.update(cx, |editor, cx| editor.scroll_position(cx)); - let current_text = format!("line {} of {} (column {})", line, last_line + 1, column); + let current_text = format!("{} of {} (column {})", line, last_line + 1, column); Self { line_editor, @@ -186,36 +186,27 @@ impl Render for GoToLine { } } - div() + v_flex() + .w(rems(24.)) .elevation_2(cx) .key_context("GoToLine") .on_action(cx.listener(Self::cancel)) .on_action(cx.listener(Self::confirm)) - .w_96() .child( - v_flex() - .px_1() - .pt_0p5() - .gap_px() - .child( - v_flex() - .py_0p5() - .px_1() - .child(div().px_1().py_0p5().child(self.line_editor.clone())), - ) - .child( - div() - .h_px() - .w_full() - .bg(cx.theme().colors().element_background), - ) - .child( - h_flex() - .justify_between() - .px_2() - .py_1() - .child(Label::new(help_text).color(Color::Muted)), - ), + div() + .border_b_1() + .border_color(cx.theme().colors().border_variant) + .px_2() + .py_1() + .child(self.line_editor.clone()), + ) + .child( + h_flex() + .px_2() + .py_1() + .gap_1() + .child(Label::new("Current Line:").color(Color::Muted)) + .child(Label::new(help_text).color(Color::Muted)), ) } } From 3458687300e6d226531019738ad0669993b5c17d Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Thu, 28 Nov 2024 18:28:20 -0300 Subject: [PATCH 198/886] Add keybinding to the language selector tooltip (#21299) Just making sure sure we're always making keyboard navigation discoverable. Screenshot 2024-11-28 at 16 05 40 Release Notes: - N/A --- crates/language_selector/src/active_buffer_language.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/language_selector/src/active_buffer_language.rs b/crates/language_selector/src/active_buffer_language.rs index 1d5f82d285..bfa31b2f69 100644 --- a/crates/language_selector/src/active_buffer_language.rs +++ b/crates/language_selector/src/active_buffer_language.rs @@ -6,6 +6,8 @@ use workspace::{item::ItemHandle, StatusItemView, Workspace}; use crate::LanguageSelector; +gpui::actions!(language_selector, [Toggle]); + pub struct ActiveBufferLanguage { active_language: Option>, workspace: WeakView, @@ -54,7 +56,7 @@ impl Render for ActiveBufferLanguage { }); } })) - .tooltip(|cx| Tooltip::text("Select Language", cx)), + .tooltip(|cx| Tooltip::for_action("Select Language", &Toggle, cx)), ) }) } From eb2c0b33dff361a268b0276f8ec8bb38811d2c5e Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Thu, 28 Nov 2024 19:15:30 -0300 Subject: [PATCH 199/886] Fine-tune status bar left-side spacing (#21306) Closes https://github.com/zed-industries/zed/issues/21291 This PR also adds a small divider separating the panel-opening controls from the other items that appear on the left side of the status bar. The spacing was a bit bigger before because all three items on the left open panels, whereas each other item does different things (e.g., open the diagnostics tab, update the app, display language server status, etc.). Therefore, they needed to be separated somehow to communicate the difference in behavior. Hopefully, now, the border will help sort of figuring this out. | With error | Normal state | |--------|--------| | Screenshot 2024-11-28 at 18 52 58 | Screenshot 2024-11-28 at 18 53 03 | Release Notes: - N/A --- crates/diagnostics/src/items.rs | 8 +++++--- crates/workspace/src/status_bar.rs | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs index 2c580c44de..495987c516 100644 --- a/crates/diagnostics/src/items.rs +++ b/crates/diagnostics/src/items.rs @@ -1,7 +1,7 @@ use editor::Editor; use gpui::{ - rems, EventEmitter, IntoElement, ParentElement, Render, Styled, Subscription, View, - ViewContext, WeakView, + EventEmitter, IntoElement, ParentElement, Render, Styled, Subscription, View, ViewContext, + WeakView, }; use language::Diagnostic; use ui::{h_flex, prelude::*, Button, ButtonLike, Color, Icon, IconName, Label, Tooltip}; @@ -77,8 +77,10 @@ impl Render for DiagnosticIndicator { }; h_flex() - .h(rems(1.375)) .gap_2() + .pl_1() + .border_l_1() + .border_color(cx.theme().colors().border) .child( ButtonLike::new("diagnostic-indicator") .child(diagnostic_indicator) diff --git a/crates/workspace/src/status_bar.rs b/crates/workspace/src/status_bar.rs index 00a0078032..274aee063c 100644 --- a/crates/workspace/src/status_bar.rs +++ b/crates/workspace/src/status_bar.rs @@ -64,7 +64,7 @@ impl Render for StatusBar { impl StatusBar { fn render_left_tools(&self, cx: &mut ViewContext) -> impl IntoElement { h_flex() - .gap(DynamicSpacing::Base08.rems(cx)) + .gap(DynamicSpacing::Base04.rems(cx)) .overflow_x_hidden() .children(self.left_items.iter().map(|item| item.to_any())) } From 73f546ea5fcb7945b3d7d48b1b6a96e6a8411f9c Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 29 Nov 2024 11:02:56 +0200 Subject: [PATCH 200/886] Force `ashpd` crate to not use `tokio` (#21315) https://github.com/zed-industries/zed/issues/21304 Fixes a regression after https://github.com/zed-industries/zed/pull/20939 Release Notes: - N/A --- Cargo.lock | 12 +++++++++--- Cargo.toml | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fc4de8bd7c..e046359cc7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -346,13 +346,14 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9c39d707614dbcc6bed00015539f488d8e3fe3e66ed60961efc0c90f4b380b3" dependencies = [ + "async-fs 2.1.2", + "async-net 2.0.0", "enumflags2", "futures-channel", "futures-util", "rand 0.8.5", "serde", "serde_repr", - "tokio", "url", "zbus 5.1.1", ] @@ -12796,7 +12797,6 @@ dependencies = [ "signal-hook-registry", "socket2 0.5.7", "tokio-macros", - "tracing", "windows-sys 0.52.0", ] @@ -15602,8 +15602,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1162094dc63b1629fcc44150bcceeaa80798cd28bcbe7fa987b65a034c258608" dependencies = [ "async-broadcast", + "async-executor", + "async-fs 2.1.2", + "async-io 2.4.0", + "async-lock 3.4.0", + "async-process 2.3.0", "async-recursion 1.1.1", + "async-task", "async-trait", + "blocking", "enumflags2", "event-listener 5.3.1", "futures-core", @@ -15614,7 +15621,6 @@ dependencies = [ "serde", "serde_repr", "static_assertions", - "tokio", "tracing", "uds_windows", "windows-sys 0.59.0", diff --git a/Cargo.toml b/Cargo.toml index 996d41e803..b50b6d9f9d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -333,7 +333,7 @@ alacritty_terminal = { git = "https://github.com/alacritty/alacritty", rev = "91 any_vec = "0.14" anyhow = "1.0.86" arrayvec = { version = "0.7.4", features = ["serde"] } -ashpd = "0.10.0" +ashpd = { version = "0.10", default-features = false, features = ["async-std"]} async-compat = "0.2.1" async-compression = { version = "0.4", features = ["gzip", "futures-io"] } async-dispatcher = "0.1" From 94faf9dd56c494d369513e885fe1e08a95256bd3 Mon Sep 17 00:00:00 2001 From: Stanislav Alekseev <43210583+WeetHet@users.noreply.github.com> Date: Fri, 29 Nov 2024 11:09:33 +0200 Subject: [PATCH 201/886] nix: Return to building with crane (#21292) This removes .envrc, putting it into gitignore as well as building with crane, as it does not require an up to date hash for a FOD. Release Notes: - N/A cc @mrnugget @jaredramirez --- .envrc | 2 - .gitignore | 1 + flake.lock | 16 +++ flake.nix | 14 ++- nix/build.nix | 342 ++++++++++++++++++++++++-------------------------- 5 files changed, 193 insertions(+), 182 deletions(-) delete mode 100644 .envrc diff --git a/.envrc b/.envrc deleted file mode 100644 index 082c01feeb..0000000000 --- a/.envrc +++ /dev/null @@ -1,2 +0,0 @@ -watch_file nix/shell.nix -use flake diff --git a/.gitignore b/.gitignore index d19c5a102a..fc6263eb7e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /.direnv +.envrc .idea **/target **/cargo-target diff --git a/flake.lock b/flake.lock index 4011b38c4b..ae27b51678 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,20 @@ { "nodes": { + "crane": { + "locked": { + "lastModified": 1732407143, + "narHash": "sha256-qJOGDT6PACoX+GbNH2PPx2ievlmtT1NVeTB80EkRLys=", + "owner": "ipetkov", + "repo": "crane", + "rev": "f2b4b472983817021d9ffb60838b2b36b9376b20", + "type": "github" + }, + "original": { + "owner": "ipetkov", + "repo": "crane", + "type": "github" + } + }, "flake-compat": { "locked": { "lastModified": 1696426674, @@ -33,6 +48,7 @@ }, "root": { "inputs": { + "crane": "crane", "flake-compat": "flake-compat", "nixpkgs": "nixpkgs", "rust-overlay": "rust-overlay" diff --git a/flake.nix b/flake.nix index 3258522eb4..f797227fba 100644 --- a/flake.nix +++ b/flake.nix @@ -7,11 +7,17 @@ url = "github:oxalica/rust-overlay"; inputs.nixpkgs.follows = "nixpkgs"; }; + crane.url = "github:ipetkov/crane"; flake-compat.url = "github:edolstra/flake-compat"; }; outputs = - { nixpkgs, rust-overlay, ... }: + { + nixpkgs, + rust-overlay, + crane, + ... + }: let systems = [ "x86_64-linux" @@ -27,10 +33,8 @@ }; zed-editor = final: prev: { zed-editor = final.callPackage ./nix/build.nix { - rustPlatform = final.makeRustPlatform { - cargo = final.rustToolchain; - rustc = final.rustToolchain; - }; + crane = crane.mkLib final; + rustToolchain = final.rustToolchain; }; }; }; diff --git a/nix/build.nix b/nix/build.nix index d3d3d1aab1..e78025dffd 100644 --- a/nix/build.nix +++ b/nix/build.nix @@ -1,6 +1,7 @@ { lib, - rustPlatform, + crane, + rustToolchain, fetchpatch, clang, cmake, @@ -26,7 +27,6 @@ vulkan-loader, envsubst, cargo-about, - versionCheckHook, cargo-bundle, git, apple-sdk_15, @@ -50,207 +50,199 @@ let in !( inRootDir - && ( - baseName == "docs" - || baseName == ".github" - || baseName == "script" - || baseName == ".git" - || baseName == "target" - ) + && (baseName == "docs" || baseName == ".github" || baseName == ".git" || baseName == "target") ); -in -rustPlatform.buildRustPackage rec { - pname = "zed-editor"; - version = "nightly"; - - src = lib.cleanSourceWith { + craneLib = crane.overrideToolchain rustToolchain; + commonSrc = lib.cleanSourceWith { src = nix-gitignore.gitignoreSource [ ] ../.; filter = includeFilter; name = "source"; }; + commonArgs = rec { + pname = "zed-editor"; + version = "nightly"; - patches = - [ - # Zed uses cargo-install to install cargo-about during the script execution. - # We provide cargo-about ourselves and can skip this step. - # Until https://github.com/zed-industries/zed/issues/19971 is fixed, - # we also skip any crate for which the license cannot be determined. - (fetchpatch { - url = "https://raw.githubusercontent.com/NixOS/nixpkgs/1fd02d90c6c097f91349df35da62d36c19359ba7/pkgs/by-name/ze/zed-editor/0001-generate-licenses.patch"; - hash = "sha256-cLgqLDXW1JtQ2OQFLd5UolAjfy7bMoTw40lEx2jA2pk="; - }) - ] - ++ lib.optionals stdenv.hostPlatform.isDarwin [ - # Livekit requires Swift 6 - # We need this until livekit-rust sdk is used - (fetchpatch { - url = "https://raw.githubusercontent.com/NixOS/nixpkgs/1fd02d90c6c097f91349df35da62d36c19359ba7/pkgs/by-name/ze/zed-editor/0002-disable-livekit-darwin.patch"; - hash = "sha256-whZ7RaXv8hrVzWAveU3qiBnZSrvGNEHTuyNhxgMIo5w="; - }) - ]; + src = commonSrc; - useFetchCargoVendor = true; - cargoHash = "sha256-KURM1W9UP65BU9gbvEBgQj3jwSYfQT7X18gcSmOMguI="; + nativeBuildInputs = + [ + clang + cmake + copyDesktopItems + curl + perl + pkg-config + protobuf + cargo-about + ] + ++ lib.optionals stdenv.hostPlatform.isLinux [ makeWrapper ] + ++ lib.optionals stdenv.hostPlatform.isDarwin [ cargo-bundle ]; - nativeBuildInputs = - [ - clang - cmake - copyDesktopItems - curl - perl - pkg-config - protobuf - rustPlatform.bindgenHook - cargo-about - ] - ++ lib.optionals stdenv.hostPlatform.isLinux [ makeWrapper ] - ++ lib.optionals stdenv.hostPlatform.isDarwin [ cargo-bundle ]; - - dontUseCmakeConfigure = true; - - buildInputs = - [ - curl - fontconfig - freetype - libgit2 - openssl - sqlite - zlib - zstd - ] - ++ lib.optionals stdenv.hostPlatform.isLinux [ - alsa-lib - libxkbcommon - wayland - xorg.libxcb - ] - ++ lib.optionals stdenv.hostPlatform.isDarwin [ - apple-sdk_15 - (darwinMinVersionHook "10.15") - ]; - - cargoBuildFlags = [ - "--package=zed" - "--package=cli" - ]; - - buildFeatures = lib.optionals stdenv.hostPlatform.isDarwin [ "gpui/runtime_shaders" ]; - - env = { - ZSTD_SYS_USE_PKG_CONFIG = true; - FONTCONFIG_FILE = makeFontsConf { - fontDirectories = [ - "${src}/assets/fonts/plex-mono" - "${src}/assets/fonts/plex-sans" + buildInputs = + [ + curl + fontconfig + freetype + libgit2 + openssl + sqlite + zlib + zstd + ] + ++ lib.optionals stdenv.hostPlatform.isLinux [ + alsa-lib + libxkbcommon + wayland + xorg.libxcb + ] + ++ lib.optionals stdenv.hostPlatform.isDarwin [ + apple-sdk_15 + (darwinMinVersionHook "10.15") ]; + + env = { + ZSTD_SYS_USE_PKG_CONFIG = true; + FONTCONFIG_FILE = makeFontsConf { + fontDirectories = [ + "${src}/assets/fonts/plex-mono" + "${src}/assets/fonts/plex-sans" + ]; + }; + ZED_UPDATE_EXPLANATION = "Zed has been installed using Nix. Auto-updates have thus been disabled."; + RELEASE_VERSION = version; }; - ZED_UPDATE_EXPLANATION = "Zed has been installed using Nix. Auto-updates have thus been disabled."; - RELEASE_VERSION = version; }; + cargoArtifacts = craneLib.buildDepsOnly commonArgs; +in +craneLib.buildPackage ( + commonArgs + // rec { + inherit cargoArtifacts; - RUSTFLAGS = if withGLES then "--cfg gles" else ""; - gpu-lib = if withGLES then libglvnd else vulkan-loader; + patches = + [ + # Zed uses cargo-install to install cargo-about during the script execution. + # We provide cargo-about ourselves and can skip this step. + # Until https://github.com/zed-industries/zed/issues/19971 is fixed, + # we also skip any crate for which the license cannot be determined. + (fetchpatch { + url = "https://raw.githubusercontent.com/NixOS/nixpkgs/1fd02d90c6c097f91349df35da62d36c19359ba7/pkgs/by-name/ze/zed-editor/0001-generate-licenses.patch"; + hash = "sha256-cLgqLDXW1JtQ2OQFLd5UolAjfy7bMoTw40lEx2jA2pk="; + }) + ] + ++ lib.optionals stdenv.hostPlatform.isDarwin [ + # Livekit requires Swift 6 + # We need this until livekit-rust sdk is used + (fetchpatch { + url = "https://raw.githubusercontent.com/NixOS/nixpkgs/1fd02d90c6c097f91349df35da62d36c19359ba7/pkgs/by-name/ze/zed-editor/0002-disable-livekit-darwin.patch"; + hash = "sha256-whZ7RaXv8hrVzWAveU3qiBnZSrvGNEHTuyNhxgMIo5w="; + }) + ]; - preBuild = '' - bash script/generate-licenses - ''; + cargoExtraArgs = "--package=zed --package=cli --features=gpui/runtime_shaders"; - postFixup = lib.optionalString stdenv.hostPlatform.isLinux '' - patchelf --add-rpath ${gpu-lib}/lib $out/libexec/* - patchelf --add-rpath ${wayland}/lib $out/libexec/* - wrapProgram $out/libexec/zed-editor --suffix PATH : ${lib.makeBinPath [ nodejs_22 ]} - ''; + dontUseCmakeConfigure = true; + preBuild = '' + bash script/generate-licenses + ''; - preCheck = '' - export HOME=$(mktemp -d); - ''; + postFixup = lib.optionalString stdenv.hostPlatform.isLinux '' + patchelf --add-rpath ${gpu-lib}/lib $out/libexec/* + patchelf --add-rpath ${wayland}/lib $out/libexec/* + wrapProgram $out/libexec/zed-editor --suffix PATH : ${lib.makeBinPath [ nodejs_22 ]} + ''; - checkFlags = - [ - # Flaky: unreliably fails on certain hosts (including Hydra) - "--skip=zed::tests::test_window_edit_state_restoring_enabled" - ] - ++ lib.optionals stdenv.hostPlatform.isLinux [ - # Fails on certain hosts (including Hydra) for unclear reason - "--skip=test_open_paths_action" - ]; + RUSTFLAGS = if withGLES then "--cfg gles" else ""; + gpu-lib = if withGLES then libglvnd else vulkan-loader; - installPhase = - if stdenv.hostPlatform.isDarwin then - '' - runHook preInstall + preCheck = '' + export HOME=$(mktemp -d); + ''; - # cargo-bundle expects the binary in target/release - mv target/${stdenv.hostPlatform.rust.cargoShortTarget}/release/zed target/release/zed + cargoTestExtraArgs = + "-- " + + lib.concatStringsSep " " ( + [ + # Flaky: unreliably fails on certain hosts (including Hydra) + "--skip=zed::tests::test_window_edit_state_restoring_enabled" + ] + ++ lib.optionals stdenv.hostPlatform.isLinux [ + # Fails on certain hosts (including Hydra) for unclear reason + "--skip=test_open_paths_action" + ] + ); - pushd crates/zed + installPhase = + if stdenv.hostPlatform.isDarwin then + '' + runHook preInstall - # Note that this is GNU sed, while Zed's bundle-mac uses BSD sed - sed -i "s/package.metadata.bundle-stable/package.metadata.bundle/" Cargo.toml - export CARGO_BUNDLE_SKIP_BUILD=true - app_path=$(cargo bundle --release | xargs) + # cargo-bundle expects the binary in target/release + mv target/release/zed target/release/zed - # We're not using the fork of cargo-bundle, so we must manually append plist extensions - # Remove closing tags from Info.plist (last two lines) - head -n -2 $app_path/Contents/Info.plist > Info.plist - # Append extensions - cat resources/info/*.plist >> Info.plist - # Add closing tags - printf "\n\n" >> Info.plist - mv Info.plist $app_path/Contents/Info.plist + pushd crates/zed - popd + # Note that this is GNU sed, while Zed's bundle-mac uses BSD sed + sed -i "s/package.metadata.bundle-stable/package.metadata.bundle/" Cargo.toml + export CARGO_BUNDLE_SKIP_BUILD=true + app_path=$(cargo bundle --release | xargs) - mkdir -p $out/Applications $out/bin - # Zed expects git next to its own binary - ln -s ${git}/bin/git $app_path/Contents/MacOS/git - mv target/${stdenv.hostPlatform.rust.cargoShortTarget}/release/cli $app_path/Contents/MacOS/cli - mv $app_path $out/Applications/ + # We're not using the fork of cargo-bundle, so we must manually append plist extensions + # Remove closing tags from Info.plist (last two lines) + head -n -2 $app_path/Contents/Info.plist > Info.plist + # Append extensions + cat resources/info/*.plist >> Info.plist + # Add closing tags + printf "\n\n" >> Info.plist + mv Info.plist $app_path/Contents/Info.plist - # Physical location of the CLI must be inside the app bundle as this is used - # to determine which app to start - ln -s $out/Applications/Zed.app/Contents/MacOS/cli $out/bin/zed + popd - runHook postInstall - '' - else - '' - runHook preInstall + mkdir -p $out/Applications $out/bin + # Zed expects git next to its own binary + ln -s ${git}/bin/git $app_path/Contents/MacOS/git + mv target/release/cli $app_path/Contents/MacOS/cli + mv $app_path $out/Applications/ - mkdir -p $out/bin $out/libexec - cp target/${stdenv.hostPlatform.rust.cargoShortTarget}/release/zed $out/libexec/zed-editor - cp target/${stdenv.hostPlatform.rust.cargoShortTarget}/release/cli $out/bin/zed + # Physical location of the CLI must be inside the app bundle as this is used + # to determine which app to start + ln -s $out/Applications/Zed.app/Contents/MacOS/cli $out/bin/zed - install -D ${src}/crates/zed/resources/app-icon@2x.png $out/share/icons/hicolor/1024x1024@2x/apps/zed.png - install -D ${src}/crates/zed/resources/app-icon.png $out/share/icons/hicolor/512x512/apps/zed.png + runHook postInstall + '' + else + '' + runHook preInstall - # extracted from https://github.com/zed-industries/zed/blob/v0.141.2/script/bundle-linux (envsubst) - # and https://github.com/zed-industries/zed/blob/v0.141.2/script/install.sh (final desktop file name) - ( - export DO_STARTUP_NOTIFY="true" - export APP_CLI="zed" - export APP_ICON="zed" - export APP_NAME="Zed" - export APP_ARGS="%U" - mkdir -p "$out/share/applications" - ${lib.getExe envsubst} < "crates/zed/resources/zed.desktop.in" > "$out/share/applications/dev.zed.Zed.desktop" - ) + mkdir -p $out/bin $out/libexec + cp target/release/zed $out/libexec/zed-editor + cp target/release/cli $out/bin/zed - runHook postInstall - ''; + install -D ${commonSrc}/crates/zed/resources/app-icon@2x.png $out/share/icons/hicolor/1024x1024@2x/apps/zed.png + install -D ${commonSrc}/crates/zed/resources/app-icon.png $out/share/icons/hicolor/512x512/apps/zed.png - nativeInstallCheckInputs = [ - versionCheckHook - ]; + # extracted from https://github.com/zed-industries/zed/blob/v0.141.2/script/bundle-linux (envsubst) + # and https://github.com/zed-industries/zed/blob/v0.141.2/script/install.sh (final desktop file name) + ( + export DO_STARTUP_NOTIFY="true" + export APP_CLI="zed" + export APP_ICON="zed" + export APP_NAME="Zed" + export APP_ARGS="%U" + mkdir -p "$out/share/applications" + ${lib.getExe envsubst} < "crates/zed/resources/zed.desktop.in" > "$out/share/applications/dev.zed.Zed.desktop" + ) - meta = { - description = "High-performance, multiplayer code editor from the creators of Atom and Tree-sitter"; - homepage = "https://zed.dev"; - changelog = "https://zed.dev/releases/preview"; - license = lib.licenses.gpl3Only; - mainProgram = "zed"; - platforms = lib.platforms.linux ++ lib.platforms.darwin; - }; -} + runHook postInstall + ''; + + meta = { + description = "High-performance, multiplayer code editor from the creators of Atom and Tree-sitter"; + homepage = "https://zed.dev"; + changelog = "https://zed.dev/releases/preview"; + license = lib.licenses.gpl3Only; + mainProgram = "zed"; + platforms = lib.platforms.linux ++ lib.platforms.darwin; + }; + } +) From eadb107339341cc7b0deec1c516f303fba2c45d7 Mon Sep 17 00:00:00 2001 From: Haru Kim Date: Fri, 29 Nov 2024 20:04:58 +0900 Subject: [PATCH 202/886] Allow `workspace::ActivatePaneInDirection` to navigate out of the terminal panel (#21313) Enhancement for #21238 Release Notes: - N/A --- crates/terminal_view/src/terminal_panel.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 1bc8a9e19b..1799d24c7d 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -957,6 +957,13 @@ impl Render for TerminalPanel { cx, ) { cx.focus_view(&pane); + } else { + terminal_panel + .workspace + .update(cx, |workspace, cx| { + workspace.activate_pane_in_direction(action.0, cx) + }) + .ok(); } }) }) From f9d5de834a33c266ceadf098423f6f4c0276fb28 Mon Sep 17 00:00:00 2001 From: Haru Kim Date: Fri, 29 Nov 2024 20:51:36 +0900 Subject: [PATCH 203/886] Disable editor autoscroll on mouse clicks (#20287) Closes #18148 Release Notes: - Stop scrolling when clicking to the edges of the visible text area. Use `autoscroll_on_clicks` to configure this behavior. https://github.com/user-attachments/assets/3afd5cbb-5957-4e39-94c6-cd2e927038fd --------- Co-authored-by: Kirill Bulatov --- assets/settings/default.json | 2 ++ crates/editor/src/editor.rs | 3 ++- crates/editor/src/editor_settings.rs | 5 +++++ docs/src/configuring-zed.md | 10 ++++++++++ 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index efb0cc9479..b844be7fa2 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -300,6 +300,8 @@ "scroll_beyond_last_line": "one_page", // The number of lines to keep above/below the cursor when scrolling. "vertical_scroll_margin": 3, + // Whether to scroll when clicking near the edge of the visible text area. + "autoscroll_on_clicks": false, // Scroll sensitivity multiplier. This multiplier is applied // to both the horizontal and vertical delta values while scrolling. "scroll_sensitivity": 1.0, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 611ec9232e..24ae84b035 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2931,7 +2931,7 @@ impl Editor { let start; let end; let mode; - let auto_scroll; + let mut auto_scroll; match click_count { 1 => { start = buffer.anchor_before(position.to_point(&display_map)); @@ -2967,6 +2967,7 @@ impl Editor { auto_scroll = false; } } + auto_scroll &= !EditorSettings::get_global(cx).autoscroll_on_clicks; let point_to_delete: Option = { let selected_points: Vec> = diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index ff743db9b6..e669c21554 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -18,6 +18,7 @@ pub struct EditorSettings { pub gutter: Gutter, pub scroll_beyond_last_line: ScrollBeyondLastLine, pub vertical_scroll_margin: f32, + pub autoscroll_on_clicks: bool, pub scroll_sensitivity: f32, pub relative_line_numbers: bool, pub seed_search_query_from_cursor: SeedQuerySetting, @@ -222,6 +223,10 @@ pub struct EditorSettingsContent { /// /// Default: 3. pub vertical_scroll_margin: Option, + /// Whether to scroll when clicking near the edge of the visible text area. + /// + /// Default: false + pub autoscroll_on_clicks: Option, /// Scroll sensitivity multiplier. This multiplier is applied /// to both the horizontal and vertical delta values while scrolling. /// diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index 5eacf4136d..bd1da9ece8 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -133,6 +133,16 @@ Define extensions which should be installed (`true`) or never installed (`false` } ``` +## Autoscroll on Clicks + +- Description: Whether to scroll when clicking near the edge of the visible text area. +- Setting: `autoscroll_on_clicks` +- Default: `false` + +**Options** + +`boolean` values + ## Auto Update - Description: Whether or not to automatically check for updates. From 74f265e5cfc932a3bfdf3dbdef8e136080bc7ccc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Fri, 29 Nov 2024 13:43:40 +0100 Subject: [PATCH 204/886] Update to embed-resource 3.0 (fixes build below windows \?\ path) (#21288) Accd'g to https://github.com/zed-industries/zed/pull/9009#issuecomment-1983599232 the manifest is required Followup for https://github.com/nabijaczleweli/rust-embed-resource/issues/71 Release Notes: - N/A --- crates/gpui/Cargo.toml | 2 +- crates/gpui/build.rs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 347e5502ca..ed523c769a 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -119,7 +119,7 @@ http_client = { workspace = true, features = ["test-support"] } unicode-segmentation.workspace = true [build-dependencies] -embed-resource = "2.4" +embed-resource = "3.0" [target.'cfg(target_os = "macos")'.build-dependencies] bindgen = "0.70.0" diff --git a/crates/gpui/build.rs b/crates/gpui/build.rs index 5a015106c7..ef29d7cc82 100644 --- a/crates/gpui/build.rs +++ b/crates/gpui/build.rs @@ -18,7 +18,9 @@ fn main() { let rc_file = std::path::Path::new("resources/windows/gpui.rc"); println!("cargo:rerun-if-changed={}", manifest.display()); println!("cargo:rerun-if-changed={}", rc_file.display()); - embed_resource::compile(rc_file, embed_resource::NONE); + embed_resource::compile(rc_file, embed_resource::NONE) + .manifest_required() + .unwrap(); } _ => (), }; From a593a04da42a7ea8e20dcc093ca61fd5fd48796d Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 29 Nov 2024 15:39:18 +0200 Subject: [PATCH 205/886] Update the lockfile after a recent dependency update (#21328) Follow-up of https://github.com/zed-industries/zed/pull/21288 Release Notes: - N/A --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e046359cc7..bdb839e78b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3902,9 +3902,9 @@ dependencies = [ [[package]] name = "embed-resource" -version = "2.5.1" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b68b6f9f63a0b6a38bc447d4ce84e2b388f3ec95c99c641c8ff0dd3ef89a6379" +checksum = "4762ce03154ba57ebaeee60cc631901ceae4f18219cbb874e464347471594742" dependencies = [ "cc", "memchr", From de55bd8307fd683780e013aae581db7aa78f3b69 Mon Sep 17 00:00:00 2001 From: yoleuh Date: Fri, 29 Nov 2024 08:56:32 -0500 Subject: [PATCH 206/886] Status bar: Reduce right tools lateral margin (#21329) Closes #21316 | Before | After | |--------|-------| | ![image](https://github.com/user-attachments/assets/525d16b0-c1f0-4d93-9a8e-19112b927e78)| ![image](https://github.com/user-attachments/assets/c6947c3e-6b46-4498-a672-5f418f5faad0)| Changes: changed `Base08` to `Base04` in `render_right_tools` Release Notes: - N/A --- crates/workspace/src/status_bar.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/workspace/src/status_bar.rs b/crates/workspace/src/status_bar.rs index 274aee063c..585b2700b4 100644 --- a/crates/workspace/src/status_bar.rs +++ b/crates/workspace/src/status_bar.rs @@ -71,7 +71,7 @@ impl StatusBar { fn render_right_tools(&self, cx: &mut ViewContext) -> impl IntoElement { h_flex() - .gap(DynamicSpacing::Base08.rems(cx)) + .gap(DynamicSpacing::Base04.rems(cx)) .children(self.right_items.iter().rev().map(|item| item.to_any())) } } From 0306bdc695494af0ef7564e6a409423c40ab23a8 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 29 Nov 2024 16:02:57 +0200 Subject: [PATCH 207/886] Use a single action for toggling the language (#21331) Follow-up of https://github.com/zed-industries/zed/pull/21299 Release Notes: - N/A --- crates/language_selector/src/active_buffer_language.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/language_selector/src/active_buffer_language.rs b/crates/language_selector/src/active_buffer_language.rs index bfa31b2f69..eeaa403e20 100644 --- a/crates/language_selector/src/active_buffer_language.rs +++ b/crates/language_selector/src/active_buffer_language.rs @@ -4,9 +4,7 @@ use language::LanguageName; use ui::{Button, ButtonCommon, Clickable, FluentBuilder, LabelSize, Tooltip}; use workspace::{item::ItemHandle, StatusItemView, Workspace}; -use crate::LanguageSelector; - -gpui::actions!(language_selector, [Toggle]); +use crate::{LanguageSelector, Toggle}; pub struct ActiveBufferLanguage { active_language: Option>, From 69c761f5a5e8a33e86966fa59d2a58622b5cda62 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Fri, 29 Nov 2024 11:39:02 -0300 Subject: [PATCH 208/886] Adjust project search landing page layout (#21332) Closes https://github.com/zed-industries/zed/issues/21317 https://github.com/user-attachments/assets/a4970c08-9715-4c90-ad48-8f6e80c6fcd0 Release Notes: - N/A --- crates/search/src/project_search.rs | 34 +++++++++++++++-------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 3ec2ac2aba..ce894397c3 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -333,20 +333,20 @@ impl Render for ProjectSearchView { let model = self.model.read(cx); let has_no_results = model.no_results.unwrap_or(false); let is_search_underway = model.pending_search.is_some(); - let major_text = if is_search_underway { - "Searching..." + + let heading_text = if is_search_underway { + "Searching…" } else if has_no_results { - "No results" + "No Results" } else { - "Search all files" + "Search All Files" }; - let major_text = div() + let heading_text = div() .justify_center() - .max_w_96() - .child(Label::new(major_text).size(LabelSize::Large)); + .child(Label::new(heading_text).size(LabelSize::Large)); - let minor_text: Option = if let Some(no_results) = model.no_results { + let page_content: Option = if let Some(no_results) = model.no_results { if model.pending_search.is_none() && no_results { Some( Label::new("No results found in this project for the provided query") @@ -359,20 +359,22 @@ impl Render for ProjectSearchView { } else { Some(self.landing_text_minor(cx).into_any_element()) }; - let minor_text = minor_text.map(|text| div().items_center().max_w_96().child(text)); + + let page_content = page_content.map(|text| div().child(text)); + v_flex() - .flex_1() .size_full() + .items_center() .justify_center() + .overflow_hidden() .bg(cx.theme().colors().editor_background) .track_focus(&self.focus_handle(cx)) .child( - h_flex() - .size_full() - .justify_center() - .child(h_flex().flex_1()) - .child(v_flex().gap_1().child(major_text).children(minor_text)) - .child(h_flex().flex_1()), + v_flex() + .max_w_80() + .gap_1() + .child(heading_text) + .children(page_content), ) } } From 1903a29cca012e68431d96adb13fe8f2fb6a03ac Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Fri, 29 Nov 2024 12:38:12 -0300 Subject: [PATCH 209/886] Expose "Column Git Blame" in the editor controls menu (#21336) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes https://github.com/zed-industries/zed/issues/10196 I think having this action exposed in the editor controls menu, close to the inline Git Blame option, makes more sense than a more prominent item somewhere else in the app. Maybe having it there will increase its discoverability. I myself didn't know this until a few weeks ago! Next steps would be ensuring the menu exposes its keybindings. (Quick note about the menu item name: I think maybe "_Git Blame Column_" would make more sense and feel grammatically more correct, but then we would have two Git Blame-related options, one with "Git Blame" at the start (Inline...) and another with "Git Blame" at the end (... Column). I guess one had to be sacrificed for the sake of consistency 😅.) Screenshot 2024-11-29 at 12 01 33 Release Notes: - N/A --- assets/icons/cursor_i_beam.svg | 6 ++- crates/editor/src/editor.rs | 4 ++ crates/zed/src/zed/quick_action_bar.rs | 63 ++++++++++++++++++-------- 3 files changed, 52 insertions(+), 21 deletions(-) diff --git a/assets/icons/cursor_i_beam.svg b/assets/icons/cursor_i_beam.svg index 2e7b95b203..93ac068fe2 100644 --- a/assets/icons/cursor_i_beam.svg +++ b/assets/icons/cursor_i_beam.svg @@ -1 +1,5 @@ - + + + + + diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 24ae84b035..6e729a654d 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -11796,6 +11796,10 @@ impl Editor { self.blame.as_ref() } + pub fn show_git_blame_gutter(&self) -> bool { + self.show_git_blame_gutter + } + pub fn render_git_blame_gutter(&mut self, cx: &mut WindowContext) -> bool { self.show_git_blame_gutter && self.has_blame_entries(cx) } diff --git a/crates/zed/src/zed/quick_action_bar.rs b/crates/zed/src/zed/quick_action_bar.rs index 85090a1b97..bfcd3fa391 100644 --- a/crates/zed/src/zed/quick_action_bar.rs +++ b/crates/zed/src/zed/quick_action_bar.rs @@ -91,6 +91,7 @@ impl Render for QuickActionBar { inlay_hints_enabled, supports_inlay_hints, git_blame_inline_enabled, + show_git_blame_gutter, auto_signature_help_enabled, ) = { let editor = editor.read(cx); @@ -98,6 +99,7 @@ impl Render for QuickActionBar { let inlay_hints_enabled = editor.inlay_hints_enabled(); let supports_inlay_hints = editor.supports_inlay_hints(cx); let git_blame_inline_enabled = editor.git_blame_inline_enabled(); + let show_git_blame_gutter = editor.show_git_blame_gutter(); let auto_signature_help_enabled = editor.auto_signature_help_enabled(cx); ( @@ -105,6 +107,7 @@ impl Render for QuickActionBar { inlay_hints_enabled, supports_inlay_hints, git_blame_inline_enabled, + show_git_blame_gutter, auto_signature_help_enabled, ) }; @@ -235,26 +238,6 @@ impl Render for QuickActionBar { ); } - menu = menu.toggleable_entry( - "Inline Git Blame", - git_blame_inline_enabled, - IconPosition::Start, - Some(editor::actions::ToggleGitBlameInline.boxed_clone()), - { - let editor = editor.clone(); - move |cx| { - editor - .update(cx, |editor, cx| { - editor.toggle_git_blame_inline( - &editor::actions::ToggleGitBlameInline, - cx, - ) - }) - .ok(); - } - }, - ); - menu = menu.toggleable_entry( "Selection Menu", selection_menu_enabled, @@ -295,6 +278,46 @@ impl Render for QuickActionBar { }, ); + menu = menu.separator(); + + menu = menu.toggleable_entry( + "Inline Git Blame", + git_blame_inline_enabled, + IconPosition::Start, + Some(editor::actions::ToggleGitBlameInline.boxed_clone()), + { + let editor = editor.clone(); + move |cx| { + editor + .update(cx, |editor, cx| { + editor.toggle_git_blame_inline( + &editor::actions::ToggleGitBlameInline, + cx, + ) + }) + .ok(); + } + }, + ); + + menu = menu.toggleable_entry( + "Column Git Blame", + show_git_blame_gutter, + IconPosition::Start, + Some(editor::actions::ToggleGitBlame.boxed_clone()), + { + let editor = editor.clone(); + move |cx| { + editor + .update(cx, |editor, cx| { + editor + .toggle_git_blame(&editor::actions::ToggleGitBlame, cx) + }) + .ok(); + } + }, + ); + menu }); Some(menu) From 4137d1adb9574d9f9c99b9e9bb3d351ba036ee02 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Fri, 29 Nov 2024 12:45:08 -0300 Subject: [PATCH 210/886] Make project search landing page scrollable if too small (#21338) Address https://github.com/zed-industries/zed/issues/21317#issuecomment-2508011556 https://github.com/user-attachments/assets/089844fc-a485-44a6-8e8b-d294f28e9ea2 Release Notes: - N/A --- crates/search/src/project_search.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index ce894397c3..4055def5b0 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -371,6 +371,8 @@ impl Render for ProjectSearchView { .track_focus(&self.focus_handle(cx)) .child( v_flex() + .id("project-search-landing-page") + .overflow_y_scroll() .max_w_80() .gap_1() .child(heading_text) From aea6fa0c09828e74986cad67882f7726b704246b Mon Sep 17 00:00:00 2001 From: moshyfawn Date: Fri, 29 Nov 2024 15:37:24 -0500 Subject: [PATCH 211/886] Remove project panel trash action for remote projects (#21300) Closes #20845 I'm uncertain about my placement for the logic to remove actions from the command palette list. If anyone has insights or alternative approaches, I'm open to changing the code. Release Notes: - Removed project panel `Trash` action for remote projects. --------- Co-authored-by: Finn Evers --- Cargo.lock | 1 + crates/project_panel/Cargo.toml | 1 + crates/project_panel/src/project_panel.rs | 17 +++++++++++++++-- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bdb839e78b..7768dac710 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9313,6 +9313,7 @@ dependencies = [ "anyhow", "client", "collections", + "command_palette_hooks", "db", "editor", "file_icons", diff --git a/crates/project_panel/Cargo.toml b/crates/project_panel/Cargo.toml index dbcabc9f83..af913d9d6b 100644 --- a/crates/project_panel/Cargo.toml +++ b/crates/project_panel/Cargo.toml @@ -15,6 +15,7 @@ doctest = false [dependencies] anyhow.workspace = true collections.workspace = true +command_palette_hooks.workspace = true db.workspace = true editor.workspace = true file_icons.workspace = true diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 9803742966..bfb07fc7fd 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -17,6 +17,7 @@ use file_icons::FileIcons; use anyhow::{anyhow, Context as _, Result}; use collections::{hash_map, BTreeSet, HashMap}; +use command_palette_hooks::CommandPaletteFilter; use git::repository::GitFileStatus; use gpui::{ actions, anchored, deferred, div, impl_actions, point, px, size, uniform_list, Action, @@ -38,6 +39,7 @@ use project_panel_settings::{ }; use serde::{Deserialize, Serialize}; use smallvec::SmallVec; +use std::any::TypeId; use std::{ cell::OnceCell, cmp, @@ -311,6 +313,15 @@ impl ProjectPanel { }) .detach(); + let trash_action = [TypeId::of::()]; + let is_remote = project.read(cx).is_via_collab(); + + if is_remote { + CommandPaletteFilter::update_global(cx, |filter, _cx| { + filter.hide_action_types(&trash_action); + }); + } + let filename_editor = cx.new_view(Editor::single_line); cx.subscribe( @@ -655,9 +666,11 @@ impl ProjectPanel { .action("Copy Relative Path", Box::new(CopyRelativePath)) .separator() .action("Rename", Box::new(Rename)) - .when(!is_root, |menu| { + .when(!is_root & !is_remote, |menu| { menu.action("Trash", Box::new(Trash { skip_prompt: false })) - .action("Delete", Box::new(Delete { skip_prompt: false })) + }) + .when(!is_root, |menu| { + menu.action("Delete", Box::new(Delete { skip_prompt: false })) }) .when(!is_remote & is_root, |menu| { menu.separator() From 4bf59393ecb1317f1494d945883240c0c2a94d04 Mon Sep 17 00:00:00 2001 From: tims <0xtimsb@gmail.com> Date: Sat, 30 Nov 2024 02:29:04 +0530 Subject: [PATCH 212/886] linux: Fix Zed not visible in "Open With" list in file manager for Flatpak (#21177) - Closes #19030 When `%U` is used in desktop entries, file managers pick this and use it: - When you right-click a file and choose "Open with..." - When you drag and drop files onto an application icon image Adding it to CLI args, changes Flatpak desktop entry `Exec` from: ```diff - Exec=/usr/bin/flatpak run --branch=master --arch=x86_64 --command=zed dev.zed.ZedDev --foreground + Exec=/usr/bin/flatpak run --branch=master --arch=x86_64 --command=zed --file-forwarding dev.zed.ZedDev --foreground @@u %U @@ ``` This is Flatpak's way of doing `%U`, by adding `--file-forwarding` and wrapping arg with `@@u` and `@@`. Read more below ([source](https://docs.flatpak.org/en/latest/flatpak-command-reference.html)): > --file-forwarding > > If this option is specified, the remaining arguments are scanned, and all arguments that are enclosed between a pair of '@@' arguments are interpreted as file paths, exported in the document store, and passed to the command in the form of the resulting document path. Arguments between "@@u" and "@@" are considered URIs, and any "file:" URIs are exported. The exports are non-persistent and with read and write permissions for the application. Release Notes: - Fixed Zed not visible in the "Open with" list in the file manager for Flatpak. --- crates/zed/resources/flatpak/manifest-template.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/zed/resources/flatpak/manifest-template.json b/crates/zed/resources/flatpak/manifest-template.json index 7905058f44..1560027e9f 100644 --- a/crates/zed/resources/flatpak/manifest-template.json +++ b/crates/zed/resources/flatpak/manifest-template.json @@ -32,7 +32,7 @@ "BRANDING_LIGHT": "$BRANDING_LIGHT", "BRANDING_DARK": "$BRANDING_DARK", "APP_CLI": "zed", - "APP_ARGS": "--foreground", + "APP_ARGS": "--foreground %U", "DO_STARTUP_NOTIFY": "false" } }, From 5f29f214c3ac8a981b8951b06bd0c7555c3deb17 Mon Sep 17 00:00:00 2001 From: tims <0xtimsb@gmail.com> Date: Sat, 30 Nov 2024 02:31:29 +0530 Subject: [PATCH 213/886] linux: Fix file not opening from file explorer (#21137) Closes #20070 Release Notes: - Fixed issue where files wouldn't open from the file explorer. - Fixed "Open a new workspace" option on the desktop entry right-click menu. Context: Zed consists of two binaries: - `zed` (CLI component, located at `crates/cli/main.rs`) - `zed-editor` (GUI component, located at `crates/zed/main.rs`) When `zed` is used in the terminal, it checks if an existing instance is running. If one is found, it sends data via a socket to open the specified file. Otherwise, it launches a new instance of `zed-editor`. For more details, see the `detect` and `boot_background` functions in `crates/cli/main.rs`. Root Cause: Install process creates directories like `.local/zed.app` and `.local/zed-preview.app`, which contain desktop entries for the corresponding release. For example, `.local/zed.app/share/applications` contains `zed.desktop`. This desktop entry includes a generic `Exec` field, which is correct by default: ```sh Comment=A high-performance, multiplayer code editor. TryExec=zed StartupNotify=true ``` The issue is in the `install.sh` script. This script copies the above desktop file to the common directory for desktop entries (.local/share/applications). During this process, it replaces the `TryExec` value from `zed` with the exact binary path to avoid relying on the shell's PATH resolution and to make it explicit. However, replacement incorrectly uses the path for `zed-editor` instead of the `zed` CLI binary. This results in not opening a file as if you use `zed-editor` directly to do this it will throw `zed is already running` error on production and open new instance on dev. Note: This PR solves it for new users. For existing users, they will either have to update `.desktop` file manually, or use `install.sh` script again. I'm not aware of zed auto-update method, if it runs `install.sh` under the hood. --- script/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/install.sh b/script/install.sh index 3f2c690779..9cd21119b7 100755 --- a/script/install.sh +++ b/script/install.sh @@ -125,7 +125,7 @@ linux() { desktop_file_path="$HOME/.local/share/applications/${appid}.desktop" cp "$HOME/.local/zed$suffix.app/share/applications/zed$suffix.desktop" "${desktop_file_path}" sed -i "s|Icon=zed|Icon=$HOME/.local/zed$suffix.app/share/icons/hicolor/512x512/apps/zed.png|g" "${desktop_file_path}" - sed -i "s|Exec=zed|Exec=$HOME/.local/zed$suffix.app/libexec/zed-editor|g" "${desktop_file_path}" + sed -i "s|Exec=zed|Exec=$HOME/.local/zed$suffix.app/bin/zed|g" "${desktop_file_path}" } macos() { From 57a45d80ad1e3d2b7c87d68fc4e3499527543d1f Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 30 Nov 2024 00:50:38 +0200 Subject: [PATCH 214/886] Add a keybinding to the Go to Line button (#21350) Release Notes: - N/A --- crates/go_to_line/src/cursor_position.rs | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/crates/go_to_line/src/cursor_position.rs b/crates/go_to_line/src/cursor_position.rs index 4f27c64256..2dc60475d3 100644 --- a/crates/go_to_line/src/cursor_position.rs +++ b/crates/go_to_line/src/cursor_position.rs @@ -1,5 +1,5 @@ use editor::{Editor, ToPoint}; -use gpui::{AppContext, Subscription, Task, View, WeakView}; +use gpui::{AppContext, FocusHandle, FocusableView, Subscription, Task, View, WeakView}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsSources}; @@ -22,6 +22,7 @@ pub(crate) struct SelectionStats { pub struct CursorPosition { position: Option, selected_count: SelectionStats, + context: Option, workspace: WeakView, update_position: Task<()>, _observe_active_editor: Option, @@ -31,6 +32,7 @@ impl CursorPosition { pub fn new(workspace: &Workspace) -> Self { Self { position: None, + context: None, selected_count: Default::default(), workspace: workspace.weak_handle(), update_position: Task::ready(()), @@ -58,7 +60,8 @@ impl CursorPosition { match editor.mode() { editor::EditorMode::AutoHeight { .. } | editor::EditorMode::SingleLine { .. } => { - cursor_position.position = None + cursor_position.position = None; + cursor_position.context = None; } editor::EditorMode::Full => { let mut last_selection = None::>; @@ -87,6 +90,7 @@ impl CursorPosition { } cursor_position.position = last_selection.map(|s| s.head().to_point(&buffer)); + cursor_position.context = Some(editor.focus_handle(cx)); } } @@ -158,6 +162,8 @@ impl Render for CursorPosition { ); self.write_position(&mut text, cx); + let context = self.context.clone(); + el.child( Button::new("go-to-line-column", text) .label_size(LabelSize::Small) @@ -174,12 +180,18 @@ impl Render for CursorPosition { }); } })) - .tooltip(|cx| { - Tooltip::for_action( + .tooltip(move |cx| match context.as_ref() { + Some(context) => Tooltip::for_action_in( + "Go to Line/Column", + &editor::actions::ToggleGoToLine, + context, + cx, + ), + None => Tooltip::for_action( "Go to Line/Column", &editor::actions::ToggleGoToLine, cx, - ) + ), }), ) }) From c1de606581b091d1db51857c7f0710b9b5f2c3d6 Mon Sep 17 00:00:00 2001 From: Haru Kim Date: Sat, 30 Nov 2024 21:30:27 +0900 Subject: [PATCH 215/886] Fix the `autoscroll_on_clicks` setting working incorrectly (#21362) --- crates/editor/src/editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 6e729a654d..339401ee46 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2967,7 +2967,7 @@ impl Editor { auto_scroll = false; } } - auto_scroll &= !EditorSettings::get_global(cx).autoscroll_on_clicks; + auto_scroll &= EditorSettings::get_global(cx).autoscroll_on_clicks; let point_to_delete: Option = { let selected_points: Vec> = From fd7180134661e772bf33487820115f7b9c6ac524 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 30 Nov 2024 13:55:14 +0100 Subject: [PATCH 216/886] Improve JavaScript runnable detection followup (#21363) Followup: https://github.com/zed-industries/zed/pull/21246 **Before** Screenshot 2024-11-30 at 13 27 15 **After** Screenshot 2024-11-30 at 13 27 36 We did not need to add the `*` as it was already matching one of them, we actually need at least one of them, so making it optional was a mistake. Don't think we need to add release notes, as the change is only on main the branch now. Release Notes: - N/A --- crates/languages/src/javascript/outline.scm | 4 ++-- crates/languages/src/javascript/runnables.scm | 2 +- crates/languages/src/tsx/outline.scm | 4 ++-- crates/languages/src/tsx/runnables.scm | 2 +- crates/languages/src/typescript/outline.scm | 4 ++-- crates/languages/src/typescript/runnables.scm | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/languages/src/javascript/outline.scm b/crates/languages/src/javascript/outline.scm index da6a1e0d31..0159d452cc 100644 --- a/crates/languages/src/javascript/outline.scm +++ b/crates/languages/src/javascript/outline.scm @@ -70,9 +70,9 @@ object: [ (identifier) @_name (member_expression object: (identifier) @_name) - ]* + ] ) - ]* @context + ] @context (#any-of? @_name "it" "test" "describe") arguments: ( arguments . (string (string_fragment) @name) diff --git a/crates/languages/src/javascript/runnables.scm b/crates/languages/src/javascript/runnables.scm index 615bd2f51a..af619dacb7 100644 --- a/crates/languages/src/javascript/runnables.scm +++ b/crates/languages/src/javascript/runnables.scm @@ -8,7 +8,7 @@ object: [ (identifier) @_name (member_expression object: (identifier) @_name) - ]* + ] ) ] (#any-of? @_name "it" "test" "describe") diff --git a/crates/languages/src/tsx/outline.scm b/crates/languages/src/tsx/outline.scm index 14dbf1cc0a..34b80b733b 100644 --- a/crates/languages/src/tsx/outline.scm +++ b/crates/languages/src/tsx/outline.scm @@ -78,9 +78,9 @@ object: [ (identifier) @_name (member_expression object: (identifier) @_name) - ]* + ] ) - ]* @context + ] @context (#any-of? @_name "it" "test" "describe") arguments: ( arguments . (string (string_fragment) @name) diff --git a/crates/languages/src/tsx/runnables.scm b/crates/languages/src/tsx/runnables.scm index 615bd2f51a..af619dacb7 100644 --- a/crates/languages/src/tsx/runnables.scm +++ b/crates/languages/src/tsx/runnables.scm @@ -8,7 +8,7 @@ object: [ (identifier) @_name (member_expression object: (identifier) @_name) - ]* + ] ) ] (#any-of? @_name "it" "test" "describe") diff --git a/crates/languages/src/typescript/outline.scm b/crates/languages/src/typescript/outline.scm index 14dbf1cc0a..34b80b733b 100644 --- a/crates/languages/src/typescript/outline.scm +++ b/crates/languages/src/typescript/outline.scm @@ -78,9 +78,9 @@ object: [ (identifier) @_name (member_expression object: (identifier) @_name) - ]* + ] ) - ]* @context + ] @context (#any-of? @_name "it" "test" "describe") arguments: ( arguments . (string (string_fragment) @name) diff --git a/crates/languages/src/typescript/runnables.scm b/crates/languages/src/typescript/runnables.scm index 615bd2f51a..af619dacb7 100644 --- a/crates/languages/src/typescript/runnables.scm +++ b/crates/languages/src/typescript/runnables.scm @@ -8,7 +8,7 @@ object: [ (identifier) @_name (member_expression object: (identifier) @_name) - ]* + ] ) ] (#any-of? @_name "it" "test" "describe") From d609931e1c27e9c42aa18ce328808bbce3149b64 Mon Sep 17 00:00:00 2001 From: tims <0xtimsb@gmail.com> Date: Sun, 1 Dec 2024 02:49:44 +0530 Subject: [PATCH 217/886] linux: Fix mouse cursor size and blur on Wayland (#21373) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #15788, #13258 This is a long-standing issue with a few previous attempts to fix it, such as [this one](https://github.com/zed-industries/zed/pull/17496). However, that fix was later reverted because it resolved the blur issue but caused a size issue. Currently, both blur and size issues persist when you set a custom cursor size from GNOME Settings and use fractional scaling. This PR addresses both issues. --- ### Context A new Wayland protocol, [cursor-shape-v1](https://gitlab.freedesktop.org/wayland/wayland-protocols/-/merge_requests/194), allows the compositor to handle rendering the cursor at the correct size and shape. This protocol is implemented by KDE, wlroots (Sway-like environments), etc. Zed supports this protocol, so there are no issues on these desktop environments. However, GNOME has not yet [adopted](https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/6212) this protocol. As a result, apps must fall back to manually rendering the cursor by specifying the theme, size, scale, etc., themselves. Zed also implements this fallback but does not correctly account for the display scale. --- ### Scale Fix For example, if your cursor size is `64px` and you’re using fractional scaling (e.g., `150%`), the display scale reported by the window query will be an integer value, `2` in this case. Why `2` if the scale is `150%`? That’s what the new protocol aims to improve. However, since GNOME Wayland uses this integer scale everywhere, it’s sufficient for our use case. To fix the issue, we set the `buffer_scale` to this value. But that alone doesn’t solve the problem. We also need to generate a matching theme cursor size for this scaled version. This can be calculated as `64px` * `2`, resulting in `128px` as the theme cursor size. --- ### Size Fix The XDG Desktop Portal’s `cursor-size` event fails to read the cursor size because it expects an `i32` but encounters a type error with `u32`. Due to this, the cursor size was interpreted as the default `24px` instead of the actual size set via user. --- ### Tested This fix has been tested with all possible combinations of the following: - [x] GNOME Normal Scale (100%, 200%, etc.) - [x] GNOME Fractional Scaling (125%, 150%, etc.) - [x] GNOME Cursor Sizes (**Settings > Accessibility > Seeing**, e.g., `24px`, `64px`, etc.) - [x] GNOME Experimental Feature `scale-monitor-framebuffer` (both enabled and disabled) - [x] KDE (`cursor-shape-v1` protocol) --- **Result:** 64px custom cursor size + 150% Fractional Scale: https://github.com/user-attachments/assets/cf3b1a0f-9a25-45d0-ab03-75059d3305e7 --- Release Notes: - Fixed mouse cursor size and blur issues on Wayland --- .../gpui/src/platform/linux/wayland/client.rs | 17 +++--- .../gpui/src/platform/linux/wayland/cursor.rs | 53 ++++++++++++++----- .../gpui/src/platform/linux/wayland/window.rs | 42 ++++++++------- .../src/platform/linux/xdg_desktop_portal.rs | 10 ++-- 4 files changed, 79 insertions(+), 43 deletions(-) diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/crates/gpui/src/platform/linux/wayland/client.rs index e193201957..2cafffa725 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/crates/gpui/src/platform/linux/wayland/client.rs @@ -496,7 +496,7 @@ impl WaylandClient { XDPEvent::CursorTheme(theme) => { if let Some(client) = client.0.upgrade() { let mut client = client.borrow_mut(); - client.cursor.set_theme(theme.as_str(), None); + client.cursor.set_theme(theme.as_str()); } } XDPEvent::CursorSize(size) => { @@ -649,15 +649,16 @@ impl LinuxClient for WaylandClient { if let Some(cursor_shape_device) = &state.cursor_shape_device { cursor_shape_device.set_shape(serial, style.to_shape()); - } else if state.mouse_focused_window.is_some() { + } else if let Some(focused_window) = &state.mouse_focused_window { // cursor-shape-v1 isn't supported, set the cursor using a surface. let wl_pointer = state .wl_pointer .clone() .expect("window is focused by pointer"); + let scale = focused_window.primary_output_scale(); state .cursor - .set_icon(&wl_pointer, serial, &style.to_icon_name()); + .set_icon(&wl_pointer, serial, &style.to_icon_name(), scale); } } } @@ -1439,9 +1440,13 @@ impl Dispatch for WaylandClientStatePtr { if let Some(cursor_shape_device) = &state.cursor_shape_device { cursor_shape_device.set_shape(serial, style.to_shape()); } else { - state - .cursor - .set_icon(&wl_pointer, serial, &style.to_icon_name()); + let scale = window.primary_output_scale(); + state.cursor.set_icon( + &wl_pointer, + serial, + &style.to_icon_name(), + scale, + ); } } drop(state); diff --git a/crates/gpui/src/platform/linux/wayland/cursor.rs b/crates/gpui/src/platform/linux/wayland/cursor.rs index 6a52765042..09aa414deb 100644 --- a/crates/gpui/src/platform/linux/wayland/cursor.rs +++ b/crates/gpui/src/platform/linux/wayland/cursor.rs @@ -9,6 +9,7 @@ use wayland_cursor::{CursorImageBuffer, CursorTheme}; pub(crate) struct Cursor { theme: Option, theme_name: Option, + theme_size: u32, surface: WlSurface, size: u32, shm: WlShm, @@ -27,6 +28,7 @@ impl Cursor { Self { theme: CursorTheme::load(&connection, globals.shm.clone(), size).log_err(), theme_name: None, + theme_size: size, surface: globals.compositor.create_surface(&globals.qh, ()), shm: globals.shm.clone(), connection: connection.clone(), @@ -34,26 +36,26 @@ impl Cursor { } } - pub fn set_theme(&mut self, theme_name: &str, size: Option) { - if let Some(size) = size { - self.size = size; - } - if let Some(theme) = - CursorTheme::load_from_name(&self.connection, self.shm.clone(), theme_name, self.size) - .log_err() + pub fn set_theme(&mut self, theme_name: &str) { + if let Some(theme) = CursorTheme::load_from_name( + &self.connection, + self.shm.clone(), + theme_name, + self.theme_size, + ) + .log_err() { self.theme = Some(theme); self.theme_name = Some(theme_name.to_string()); } else if let Some(theme) = - CursorTheme::load(&self.connection, self.shm.clone(), self.size).log_err() + CursorTheme::load(&self.connection, self.shm.clone(), self.theme_size).log_err() { self.theme = Some(theme); self.theme_name = None; } } - pub fn set_size(&mut self, size: u32) { - self.size = size; + fn set_theme_size(&mut self, theme_size: u32) { self.theme = self .theme_name .as_ref() @@ -62,14 +64,29 @@ impl Cursor { &self.connection, self.shm.clone(), name.as_str(), - self.size, + theme_size, ) .log_err() }) - .or_else(|| CursorTheme::load(&self.connection, self.shm.clone(), self.size).log_err()); + .or_else(|| { + CursorTheme::load(&self.connection, self.shm.clone(), theme_size).log_err() + }); } - pub fn set_icon(&mut self, wl_pointer: &WlPointer, serial_id: u32, mut cursor_icon_name: &str) { + pub fn set_size(&mut self, size: u32) { + self.size = size; + self.set_theme_size(size); + } + + pub fn set_icon( + &mut self, + wl_pointer: &WlPointer, + serial_id: u32, + mut cursor_icon_name: &str, + scale: i32, + ) { + self.set_theme_size(self.size * scale as u32); + if let Some(theme) = &mut self.theme { let mut buffer: Option<&CursorImageBuffer>; @@ -91,7 +108,15 @@ impl Cursor { let (width, height) = buffer.dimensions(); let (hot_x, hot_y) = buffer.hotspot(); - wl_pointer.set_cursor(serial_id, Some(&self.surface), hot_x as i32, hot_y as i32); + self.surface.set_buffer_scale(scale); + + wl_pointer.set_cursor( + serial_id, + Some(&self.surface), + hot_x as i32 / scale, + hot_y as i32 / scale, + ); + self.surface.attach(Some(&buffer), 0, 0); self.surface.damage(0, 0, width as i32, height as i32); self.surface.commit(); diff --git a/crates/gpui/src/platform/linux/wayland/window.rs b/crates/gpui/src/platform/linux/wayland/window.rs index 55ba4f6004..4cdf88e262 100644 --- a/crates/gpui/src/platform/linux/wayland/window.rs +++ b/crates/gpui/src/platform/linux/wayland/window.rs @@ -194,6 +194,23 @@ impl WaylandWindowState { self.decorations == WindowDecorations::Client || self.background_appearance != WindowBackgroundAppearance::Opaque } + + pub fn primary_output_scale(&mut self) -> i32 { + let mut scale = 1; + let mut current_output = self.display.take(); + for (id, output) in self.outputs.iter() { + if let Some((_, output_data)) = ¤t_output { + if output.scale > output_data.scale { + current_output = Some((id.clone(), output.clone())); + } + } else { + current_output = Some((id.clone(), output.clone())); + } + scale = scale.max(output.scale); + } + self.display = current_output; + scale + } } pub(crate) struct WaylandWindow(pub WaylandWindowStatePtr); @@ -560,7 +577,7 @@ impl WaylandWindowStatePtr { state.outputs.insert(id, output.clone()); - let scale = primary_output_scale(&mut state); + let scale = state.primary_output_scale(); // We use `PreferredBufferScale` instead to set the scale if it's available if state.surface.version() < wl_surface::EVT_PREFERRED_BUFFER_SCALE_SINCE { @@ -572,7 +589,7 @@ impl WaylandWindowStatePtr { wl_surface::Event::Leave { output } => { state.outputs.remove(&output.id()); - let scale = primary_output_scale(&mut state); + let scale = state.primary_output_scale(); // We use `PreferredBufferScale` instead to set the scale if it's available if state.surface.version() < wl_surface::EVT_PREFERRED_BUFFER_SCALE_SINCE { @@ -719,6 +736,10 @@ impl WaylandWindowStatePtr { (fun)() } } + + pub fn primary_output_scale(&self) -> i32 { + self.state.borrow_mut().primary_output_scale() + } } fn extract_states<'a, S: TryFrom + 'a>(states: &'a [u8]) -> impl Iterator + 'a @@ -732,23 +753,6 @@ where .flat_map(S::try_from) } -fn primary_output_scale(state: &mut RefMut) -> i32 { - let mut scale = 1; - let mut current_output = state.display.take(); - for (id, output) in state.outputs.iter() { - if let Some((_, output_data)) = ¤t_output { - if output.scale > output_data.scale { - current_output = Some((id.clone(), output.clone())); - } - } else { - current_output = Some((id.clone(), output.clone())); - } - scale = scale.max(output.scale); - } - state.display = current_output; - scale -} - impl rwh::HasWindowHandle for WaylandWindow { fn window_handle(&self) -> Result, rwh::HandleError> { unimplemented!() diff --git a/crates/gpui/src/platform/linux/xdg_desktop_portal.rs b/crates/gpui/src/platform/linux/xdg_desktop_portal.rs index 64aa3975b8..722947a299 100644 --- a/crates/gpui/src/platform/linux/xdg_desktop_portal.rs +++ b/crates/gpui/src/platform/linux/xdg_desktop_portal.rs @@ -42,11 +42,13 @@ impl XDPEventSource { { sender.send(Event::CursorTheme(initial_theme))?; } + + // If u32 is used here, it throws invalid type error if let Ok(initial_size) = settings - .read::("org.gnome.desktop.interface", "cursor-size") + .read::("org.gnome.desktop.interface", "cursor-size") .await { - sender.send(Event::CursorSize(initial_size))?; + sender.send(Event::CursorSize(initial_size as u32))?; } if let Ok(mut cursor_theme_changed) = settings @@ -69,7 +71,7 @@ impl XDPEventSource { } if let Ok(mut cursor_size_changed) = settings - .receive_setting_changed_with_args::( + .receive_setting_changed_with_args::( "org.gnome.desktop.interface", "cursor-size", ) @@ -80,7 +82,7 @@ impl XDPEventSource { .spawn(async move { while let Some(size) = cursor_size_changed.next().await { let size = size?; - sender.send(Event::CursorSize(size))?; + sender.send(Event::CursorSize(size as u32))?; } anyhow::Ok(()) }) From c2cd84a749f605473ba293292766264a4027e600 Mon Sep 17 00:00:00 2001 From: Agustin Gomes Date: Sat, 30 Nov 2024 22:20:31 +0100 Subject: [PATCH 218/886] Add musl-gcc as dependency (#21366) This addition comes after attempting building Zed from source. As part of the process, one of the components (a crate I presume) called `ring` failed to compile due to the following sequence of console messages: ```log warning: ring@0.17.8: Compiler family detection failed due to error: ToolNotFound: Failed to find tool. Is `musl-gcc` installed? warning: ring@0.17.8: Compiler family detection failed due to error: ToolNotFound: Failed to find tool. Is `musl-gcc` installed? error: failed to run custom build command for `ring v0.17.8` ``` Adding this library should help fix the issue on Fedora 41 at least, and possibly will help fixing it for other RedHat based distributions as well. Closes #ISSUE Release Notes: - Add musl-gcc as dependency Signed-off-by: Agustin Gomes --- script/linux | 1 + 1 file changed, 1 insertion(+) diff --git a/script/linux b/script/linux index eecf70f90e..f1fe751154 100755 --- a/script/linux +++ b/script/linux @@ -67,6 +67,7 @@ yum=$(command -v yum || true) if [[ -n $dnf ]] || [[ -n $yum ]]; then pkg_cmd="${dnf:-${yum}}" deps=( + musl-gcc gcc clang cmake From 28849dd2a8859002a77804048ea60a5a735df3d7 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sun, 1 Dec 2024 01:48:31 +0200 Subject: [PATCH 219/886] Fix item closing overly triggering save dialogues (#21374) Closes https://github.com/zed-industries/zed/issues/12029 Allows to introspect project items inside items more deeply, checking them for being dirty. For that: * renames `project::Item` into `project::ProjectItem` * adds an `is_dirty(&self) -> bool` method to the renamed trait * changes the closing logic to only care about dirty project items when checking for save prompts conditions * save prompts are raised only if the item is singleton without a project path; or if the item has dirty project items that are not open elsewhere Release Notes: - Fixed item closing overly triggering save dialogues --- crates/diagnostics/src/diagnostics.rs | 2 +- crates/editor/src/editor.rs | 4 +- crates/editor/src/git/blame.rs | 2 +- crates/editor/src/items.rs | 6 +- crates/image_viewer/src/image_viewer.rs | 2 +- crates/outline_panel/src/outline_panel.rs | 2 +- crates/project/src/buffer_store.rs | 2 +- crates/project/src/image_store.rs | 8 +- crates/project/src/lsp_store.rs | 2 +- crates/project/src/project.rs | 9 +- crates/project_panel/src/project_panel.rs | 6 +- crates/repl/src/notebook/notebook_ui.rs | 27 +- crates/repl/src/repl_editor.rs | 2 +- crates/repl/src/repl_sessions_ui.rs | 2 +- crates/search/src/project_search.rs | 2 +- crates/workspace/src/item.rs | 20 +- crates/workspace/src/pane.rs | 112 +++-- crates/workspace/src/workspace.rs | 473 +++++++++++++++++++++- crates/zed/src/zed.rs | 2 +- 19 files changed, 600 insertions(+), 85 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 6db831c1ff..48a92d906e 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -716,7 +716,7 @@ impl Item for ProjectDiagnosticsEditor { fn for_each_project_item( &self, cx: &AppContext, - f: &mut dyn FnMut(gpui::EntityId, &dyn project::Item), + f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem), ) { self.editor.for_each_project_item(cx, f) } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 339401ee46..d5d96436e8 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -125,8 +125,8 @@ use parking_lot::{Mutex, RwLock}; use project::{ lsp_store::{FormatTarget, FormatTrigger}, project_settings::{GitGutterSetting, ProjectSettings}, - CodeAction, Completion, CompletionIntent, DocumentHighlight, InlayHint, Item, Location, - LocationLink, Project, ProjectTransaction, TaskSourceKind, + CodeAction, Completion, CompletionIntent, DocumentHighlight, InlayHint, Location, LocationLink, + Project, ProjectItem, ProjectTransaction, TaskSourceKind, }; use rand::prelude::*; use rpc::{proto::*, ErrorExt}; diff --git a/crates/editor/src/git/blame.rs b/crates/editor/src/git/blame.rs index 9dfc379ae7..c5cfb2e850 100644 --- a/crates/editor/src/git/blame.rs +++ b/crates/editor/src/git/blame.rs @@ -10,7 +10,7 @@ use gpui::{Model, ModelContext, Subscription, Task}; use http_client::HttpClient; use language::{markdown, Bias, Buffer, BufferSnapshot, Edit, LanguageRegistry, ParsedMarkdown}; use multi_buffer::MultiBufferRow; -use project::{Item, Project}; +use project::{Project, ProjectItem}; use smallvec::SmallVec; use sum_tree::SumTree; use url::Url; diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 813b212761..2f2eb493bb 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -22,8 +22,8 @@ use language::{ use lsp::DiagnosticSeverity; use multi_buffer::AnchorRangeExt; use project::{ - lsp_store::FormatTrigger, project_settings::ProjectSettings, search::SearchQuery, Item as _, - Project, ProjectPath, + lsp_store::FormatTrigger, project_settings::ProjectSettings, search::SearchQuery, Project, + ProjectItem as _, ProjectPath, }; use rpc::proto::{self, update_view, PeerId}; use settings::Settings; @@ -665,7 +665,7 @@ impl Item for Editor { fn for_each_project_item( &self, cx: &AppContext, - f: &mut dyn FnMut(EntityId, &dyn project::Item), + f: &mut dyn FnMut(EntityId, &dyn project::ProjectItem), ) { self.buffer .read(cx) diff --git a/crates/image_viewer/src/image_viewer.rs b/crates/image_viewer/src/image_viewer.rs index f7647223e5..c3f264d863 100644 --- a/crates/image_viewer/src/image_viewer.rs +++ b/crates/image_viewer/src/image_viewer.rs @@ -78,7 +78,7 @@ impl Item for ImageView { fn for_each_project_item( &self, cx: &AppContext, - f: &mut dyn FnMut(gpui::EntityId, &dyn project::Item), + f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem), ) { f(self.image_item.entity_id(), self.image_item.read(cx)) } diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index f878b582d9..66db3a3103 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -36,7 +36,7 @@ use language::{BufferId, BufferSnapshot, OffsetRangeExt, OutlineItem}; use menu::{Cancel, SelectFirst, SelectLast, SelectNext, SelectPrev}; use outline_panel_settings::{OutlinePanelDockPosition, OutlinePanelSettings, ShowIndentGuides}; -use project::{File, Fs, Item, Project}; +use project::{File, Fs, Project, ProjectItem}; use search::{BufferSearchBar, ProjectSearchView}; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsStore}; diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index 55b0f413a9..7a54f7cc47 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -1,7 +1,7 @@ use crate::{ search::SearchQuery, worktree_store::{WorktreeStore, WorktreeStoreEvent}, - Item, ProjectPath, + ProjectItem as _, ProjectPath, }; use ::git::{parse_git_remote_url, BuildPermalinkParams, GitHostingProviderRegistry}; use anyhow::{anyhow, Context as _, Result}; diff --git a/crates/project/src/image_store.rs b/crates/project/src/image_store.rs index 9f794d5248..949e1f484e 100644 --- a/crates/project/src/image_store.rs +++ b/crates/project/src/image_store.rs @@ -1,6 +1,6 @@ use crate::{ worktree_store::{WorktreeStore, WorktreeStoreEvent}, - Project, ProjectEntryId, ProjectPath, + Project, ProjectEntryId, ProjectItem, ProjectPath, }; use anyhow::{Context as _, Result}; use collections::{hash_map, HashMap, HashSet}; @@ -114,7 +114,7 @@ impl ImageItem { } } -impl crate::Item for ImageItem { +impl ProjectItem for ImageItem { fn try_open( project: &Model, path: &ProjectPath, @@ -151,6 +151,10 @@ impl crate::Item for ImageItem { fn project_path(&self, cx: &AppContext) -> Option { Some(self.project_path(cx).clone()) } + + fn is_dirty(&self) -> bool { + false + } } trait ImageStoreImpl { diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 7d75347cf0..41a3ccc0a3 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -10,7 +10,7 @@ use crate::{ toolchain_store::{EmptyToolchainStore, ToolchainStoreEvent}, worktree_store::{WorktreeStore, WorktreeStoreEvent}, yarn::YarnPathStore, - CodeAction, Completion, CoreCompletion, Hover, InlayHint, Item as _, ProjectPath, + CodeAction, Completion, CoreCompletion, Hover, InlayHint, ProjectItem as _, ProjectPath, ProjectTransaction, ResolveState, Symbol, ToolchainStore, }; use anyhow::{anyhow, Context as _, Result}; diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 40da76ff3a..30732fc8b2 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -111,7 +111,7 @@ const MAX_PROJECT_SEARCH_HISTORY_SIZE: usize = 500; const MAX_SEARCH_RESULT_FILES: usize = 5_000; const MAX_SEARCH_RESULT_RANGES: usize = 10_000; -pub trait Item { +pub trait ProjectItem { fn try_open( project: &Model, path: &ProjectPath, @@ -121,6 +121,7 @@ pub trait Item { Self: Sized; fn entry_id(&self, cx: &AppContext) -> Option; fn project_path(&self, cx: &AppContext) -> Option; + fn is_dirty(&self) -> bool; } #[derive(Clone)] @@ -4354,7 +4355,7 @@ impl ResolvedPath { } } -impl Item for Buffer { +impl ProjectItem for Buffer { fn try_open( project: &Model, path: &ProjectPath, @@ -4373,6 +4374,10 @@ impl Item for Buffer { path: file.path().clone(), }) } + + fn is_dirty(&self) -> bool { + self.is_dirty() + } } impl Completion { diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index bfb07fc7fd..df78ff1118 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -7511,7 +7511,7 @@ mod tests { path: ProjectPath, } - impl project::Item for TestProjectItem { + impl project::ProjectItem for TestProjectItem { fn try_open( _project: &Model, path: &ProjectPath, @@ -7528,6 +7528,10 @@ mod tests { fn project_path(&self, _: &AppContext) -> Option { Some(self.path.clone()) } + + fn is_dirty(&self) -> bool { + false + } } impl ProjectItem for TestProjectItemView { diff --git a/crates/repl/src/notebook/notebook_ui.rs b/crates/repl/src/notebook/notebook_ui.rs index d10da13fd8..435dab2d0c 100644 --- a/crates/repl/src/notebook/notebook_ui.rs +++ b/crates/repl/src/notebook/notebook_ui.rs @@ -158,16 +158,6 @@ impl NotebookEditor { }) } - fn is_dirty(&self, cx: &AppContext) -> bool { - self.cell_map.values().any(|cell| { - if let Cell::Code(code_cell) = cell { - code_cell.read(cx).is_dirty(cx) - } else { - false - } - }) - } - fn clear_outputs(&mut self, cx: &mut ViewContext) { for cell in self.cell_map.values() { if let Cell::Code(code_cell) = cell { @@ -500,7 +490,7 @@ pub struct NotebookItem { id: ProjectEntryId, } -impl project::Item for NotebookItem { +impl project::ProjectItem for NotebookItem { fn try_open( project: &Model, path: &ProjectPath, @@ -561,6 +551,10 @@ impl project::Item for NotebookItem { fn project_path(&self, _: &AppContext) -> Option { Some(self.project_path.clone()) } + + fn is_dirty(&self) -> bool { + false + } } impl NotebookItem { @@ -656,7 +650,7 @@ impl Item for NotebookEditor { fn for_each_project_item( &self, cx: &AppContext, - f: &mut dyn FnMut(gpui::EntityId, &dyn project::Item), + f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem), ) { f(self.notebook_item.entity_id(), self.notebook_item.read(cx)) } @@ -734,8 +728,13 @@ impl Item for NotebookEditor { } fn is_dirty(&self, cx: &AppContext) -> bool { - // self.is_dirty(cx) TODO - false + self.cell_map.values().any(|cell| { + if let Cell::Code(code_cell) = cell { + code_cell.read(cx).is_dirty(cx) + } else { + false + } + }) } } diff --git a/crates/repl/src/repl_editor.rs b/crates/repl/src/repl_editor.rs index b032b1804a..3c203900da 100644 --- a/crates/repl/src/repl_editor.rs +++ b/crates/repl/src/repl_editor.rs @@ -7,7 +7,7 @@ use anyhow::{Context, Result}; use editor::Editor; use gpui::{prelude::*, Entity, View, WeakView, WindowContext}; use language::{BufferSnapshot, Language, LanguageName, Point}; -use project::{Item as _, WorktreeId}; +use project::{ProjectItem as _, WorktreeId}; use crate::repl_store::ReplStore; use crate::session::SessionEvent; diff --git a/crates/repl/src/repl_sessions_ui.rs b/crates/repl/src/repl_sessions_ui.rs index 32b91ce28c..11db19ef84 100644 --- a/crates/repl/src/repl_sessions_ui.rs +++ b/crates/repl/src/repl_sessions_ui.rs @@ -3,7 +3,7 @@ use gpui::{ actions, prelude::*, AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView, Subscription, View, }; -use project::Item as _; +use project::ProjectItem as _; use ui::{prelude::*, ButtonLike, ElevationIndex, KeyBinding}; use util::ResultExt as _; use workspace::item::ItemEvent; diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 4055def5b0..9caec6af34 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -449,7 +449,7 @@ impl Item for ProjectSearchView { fn for_each_project_item( &self, cx: &AppContext, - f: &mut dyn FnMut(EntityId, &dyn project::Item), + f: &mut dyn FnMut(EntityId, &dyn project::ProjectItem), ) { self.results_editor.for_each_project_item(cx, f) } diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 20437145cb..40d92666a0 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -208,7 +208,7 @@ pub trait Item: FocusableView + EventEmitter { fn for_each_project_item( &self, _: &AppContext, - _: &mut dyn FnMut(EntityId, &dyn project::Item), + _: &mut dyn FnMut(EntityId, &dyn project::ProjectItem), ) { } fn is_singleton(&self, _cx: &AppContext) -> bool { @@ -386,7 +386,7 @@ pub trait ItemHandle: 'static + Send { fn for_each_project_item( &self, _: &AppContext, - _: &mut dyn FnMut(EntityId, &dyn project::Item), + _: &mut dyn FnMut(EntityId, &dyn project::ProjectItem), ); fn is_singleton(&self, cx: &AppContext) -> bool; fn boxed_clone(&self) -> Box; @@ -563,7 +563,7 @@ impl ItemHandle for View { fn for_each_project_item( &self, cx: &AppContext, - f: &mut dyn FnMut(EntityId, &dyn project::Item), + f: &mut dyn FnMut(EntityId, &dyn project::ProjectItem), ) { self.read(cx).for_each_project_item(cx, f) } @@ -891,7 +891,7 @@ impl WeakItemHandle for WeakView { } pub trait ProjectItem: Item { - type Item: project::Item; + type Item: project::ProjectItem; fn for_project_item( project: Model, @@ -1045,6 +1045,7 @@ pub mod test { pub struct TestProjectItem { pub entry_id: Option, pub project_path: Option, + pub is_dirty: bool, } pub struct TestItem { @@ -1065,7 +1066,7 @@ pub mod test { focus_handle: gpui::FocusHandle, } - impl project::Item for TestProjectItem { + impl project::ProjectItem for TestProjectItem { fn try_open( _project: &Model, _path: &ProjectPath, @@ -1073,7 +1074,6 @@ pub mod test { ) -> Option>>> { None } - fn entry_id(&self, _: &AppContext) -> Option { self.entry_id } @@ -1081,6 +1081,10 @@ pub mod test { fn project_path(&self, _: &AppContext) -> Option { self.project_path.clone() } + + fn is_dirty(&self) -> bool { + self.is_dirty + } } pub enum TestItemEvent { @@ -1097,6 +1101,7 @@ pub mod test { cx.new_model(|_| Self { entry_id, project_path, + is_dirty: false, }) } @@ -1104,6 +1109,7 @@ pub mod test { cx.new_model(|_| Self { project_path: None, entry_id: None, + is_dirty: false, }) } } @@ -1225,7 +1231,7 @@ pub mod test { fn for_each_project_item( &self, cx: &AppContext, - f: &mut dyn FnMut(EntityId, &dyn project::Item), + f: &mut dyn FnMut(EntityId, &dyn project::ProjectItem), ) { self.project_items .iter() diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index dc7b92a13b..66db71553f 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1295,10 +1295,12 @@ impl Pane { ) -> Task> { // Find the items to close. let mut items_to_close = Vec::new(); + let mut item_ids_to_close = HashSet::default(); let mut dirty_items = Vec::new(); for item in &self.items { if should_close(item.item_id()) { items_to_close.push(item.boxed_clone()); + item_ids_to_close.insert(item.item_id()); if item.is_dirty(cx) { dirty_items.push(item.boxed_clone()); } @@ -1339,16 +1341,23 @@ impl Pane { } } let mut saved_project_items_ids = HashSet::default(); - for item in items_to_close.clone() { - // Find the item's current index and its set of project item models. Avoid + for item_to_close in items_to_close { + // Find the item's current index and its set of dirty project item models. Avoid // storing these in advance, in case they have changed since this task // was started. - let (item_ix, mut project_item_ids) = pane.update(&mut cx, |pane, cx| { - (pane.index_for_item(&*item), item.project_item_model_ids(cx)) - })?; - let item_ix = if let Some(ix) = item_ix { - ix - } else { + let mut dirty_project_item_ids = Vec::new(); + let Some(item_ix) = pane.update(&mut cx, |pane, cx| { + item_to_close.for_each_project_item( + cx, + &mut |project_item_id, project_item| { + if project_item.is_dirty() { + dirty_project_item_ids.push(project_item_id); + } + }, + ); + pane.index_for_item(&*item_to_close) + })? + else { continue; }; @@ -1356,27 +1365,34 @@ impl Pane { // in the workspace, AND that the user has not already been prompted to save. // If there are any such project entries, prompt the user to save this item. let project = workspace.update(&mut cx, |workspace, cx| { - for item in workspace.items(cx) { - if !items_to_close - .iter() - .any(|item_to_close| item_to_close.item_id() == item.item_id()) - { - let other_project_item_ids = item.project_item_model_ids(cx); - project_item_ids.retain(|id| !other_project_item_ids.contains(id)); + for open_item in workspace.items(cx) { + let open_item_id = open_item.item_id(); + if !item_ids_to_close.contains(&open_item_id) { + let other_project_item_ids = open_item.project_item_model_ids(cx); + dirty_project_item_ids + .retain(|id| !other_project_item_ids.contains(id)); } } workspace.project().clone() })?; - let should_save = project_item_ids + let should_save = dirty_project_item_ids .iter() - .any(|id| saved_project_items_ids.insert(*id)); + .any(|id| saved_project_items_ids.insert(*id)) + // Always propose to save singleton files without any project paths: those cannot be saved via multibuffer, as require a file path selection modal. + || cx + .update(|cx| { + item_to_close.is_dirty(cx) + && item_to_close.is_singleton(cx) + && item_to_close.project_path(cx).is_none() + }) + .unwrap_or(false); if should_save && !Self::save_item( project.clone(), &pane, item_ix, - &*item, + &*item_to_close, save_intent, &mut cx, ) @@ -1390,7 +1406,7 @@ impl Pane { if let Some(item_ix) = pane .items .iter() - .position(|i| i.item_id() == item.item_id()) + .position(|i| i.item_id() == item_to_close.item_id()) { pane.remove_item(item_ix, false, true, cx); } @@ -3725,9 +3741,18 @@ mod tests { assert_item_labels(&pane, [], cx); - add_labeled_item(&pane, "A", true, cx); - add_labeled_item(&pane, "B", true, cx); - add_labeled_item(&pane, "C", true, cx); + add_labeled_item(&pane, "A", true, cx).update(cx, |item, cx| { + item.project_items + .push(TestProjectItem::new(1, "A.txt", cx)) + }); + add_labeled_item(&pane, "B", true, cx).update(cx, |item, cx| { + item.project_items + .push(TestProjectItem::new(2, "B.txt", cx)) + }); + add_labeled_item(&pane, "C", true, cx).update(cx, |item, cx| { + item.project_items + .push(TestProjectItem::new(3, "C.txt", cx)) + }); assert_item_labels(&pane, ["A^", "B^", "C*^"], cx); let save = pane @@ -3746,6 +3771,30 @@ mod tests { cx.simulate_prompt_answer(2); save.await.unwrap(); assert_item_labels(&pane, [], cx); + + add_labeled_item(&pane, "A", true, cx); + add_labeled_item(&pane, "B", true, cx); + add_labeled_item(&pane, "C", true, cx); + assert_item_labels(&pane, ["A^", "B^", "C*^"], cx); + let save = pane + .update(cx, |pane, cx| { + pane.close_all_items( + &CloseAllItems { + save_intent: None, + close_pinned: false, + }, + cx, + ) + }) + .unwrap(); + + cx.executor().run_until_parked(); + cx.simulate_prompt_answer(2); + cx.executor().run_until_parked(); + cx.simulate_prompt_answer(2); + cx.executor().run_until_parked(); + save.await.unwrap(); + assert_item_labels(&pane, ["A*^", "B^", "C^"], cx); } #[gpui::test] @@ -3833,14 +3882,14 @@ mod tests { } // Assert the item label, with the active item label suffixed with a '*' + #[track_caller] fn assert_item_labels( pane: &View, expected_states: [&str; COUNT], cx: &mut VisualTestContext, ) { - pane.update(cx, |pane, cx| { - let actual_states = pane - .items + let actual_states = pane.update(cx, |pane, cx| { + pane.items .iter() .enumerate() .map(|(ix, item)| { @@ -3859,12 +3908,11 @@ mod tests { } state }) - .collect::>(); - - assert_eq!( - actual_states, expected_states, - "pane items do not match expectation" - ); - }) + .collect::>() + }); + assert_eq!( + actual_states, expected_states, + "pane items do not match expectation" + ); } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index ed5aaa6e49..7945c4e404 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -391,12 +391,12 @@ impl Global for ProjectItemOpeners {} pub fn register_project_item(cx: &mut AppContext) { let builders = cx.default_global::(); builders.push(|project, project_path, cx| { - let project_item = ::try_open(project, project_path, cx)?; + let project_item = ::try_open(project, project_path, cx)?; let project = project.clone(); Some(cx.spawn(|cx| async move { let project_item = project_item.await?; let project_entry_id: Option = - project_item.read_with(&cx, project::Item::entry_id)?; + project_item.read_with(&cx, project::ProjectItem::entry_id)?; let build_workspace_item = Box::new(|cx: &mut ViewContext| { Box::new(cx.new_view(|cx| I::for_project_item(project, project_item, cx))) as Box @@ -2721,7 +2721,7 @@ impl Workspace { where T: ProjectItem, { - use project::Item as _; + use project::ProjectItem as _; let project_item = project_item.read(cx); let entry_id = project_item.entry_id(cx); let project_path = project_item.project_path(cx); @@ -6422,24 +6422,26 @@ mod tests { let item1 = cx.new_view(|cx| { TestItem::new(cx) .with_dirty(true) - .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) + .with_project_items(&[dirty_project_item(1, "1.txt", cx)]) }); let item2 = cx.new_view(|cx| { TestItem::new(cx) .with_dirty(true) .with_conflict(true) - .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)]) + .with_project_items(&[dirty_project_item(2, "2.txt", cx)]) }); let item3 = cx.new_view(|cx| { TestItem::new(cx) .with_dirty(true) .with_conflict(true) - .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)]) + .with_project_items(&[dirty_project_item(3, "3.txt", cx)]) }); let item4 = cx.new_view(|cx| { - TestItem::new(cx) - .with_dirty(true) - .with_project_items(&[TestProjectItem::new_untitled(cx)]) + TestItem::new(cx).with_dirty(true).with_project_items(&[{ + let project_item = TestProjectItem::new_untitled(cx); + project_item.update(cx, |project_item, _| project_item.is_dirty = true); + project_item + }]) }); let pane = workspace.update(cx, |workspace, cx| { workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx); @@ -6531,7 +6533,7 @@ mod tests { cx.new_view(|cx| { TestItem::new(cx) .with_dirty(true) - .with_project_items(&[TestProjectItem::new( + .with_project_items(&[dirty_project_item( project_entry_id, &format!("{project_entry_id}.txt"), cx, @@ -6713,6 +6715,9 @@ mod tests { }) }); item.is_dirty = true; + for project_item in &mut item.project_items { + project_item.update(cx, |project_item, _| project_item.is_dirty = true); + } }); pane.update(cx, |pane, cx| { @@ -7411,6 +7416,434 @@ mod tests { }); } + #[gpui::test] + async fn test_no_save_prompt_when_multi_buffer_dirty_items_closed(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.background_executor.clone()); + let project = Project::test(fs, [], cx).await; + let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); + let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone()); + + let dirty_regular_buffer = cx.new_view(|cx| { + TestItem::new(cx) + .with_dirty(true) + .with_label("1.txt") + .with_project_items(&[dirty_project_item(1, "1.txt", cx)]) + }); + let dirty_regular_buffer_2 = cx.new_view(|cx| { + TestItem::new(cx) + .with_dirty(true) + .with_label("2.txt") + .with_project_items(&[dirty_project_item(2, "2.txt", cx)]) + }); + let dirty_multi_buffer_with_both = cx.new_view(|cx| { + TestItem::new(cx) + .with_dirty(true) + .with_singleton(false) + .with_label("Fake Project Search") + .with_project_items(&[ + dirty_regular_buffer.read(cx).project_items[0].clone(), + dirty_regular_buffer_2.read(cx).project_items[0].clone(), + ]) + }); + let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id(); + workspace.update(cx, |workspace, cx| { + workspace.add_item( + pane.clone(), + Box::new(dirty_regular_buffer.clone()), + None, + false, + false, + cx, + ); + workspace.add_item( + pane.clone(), + Box::new(dirty_regular_buffer_2.clone()), + None, + false, + false, + cx, + ); + workspace.add_item( + pane.clone(), + Box::new(dirty_multi_buffer_with_both.clone()), + None, + false, + false, + cx, + ); + }); + + pane.update(cx, |pane, cx| { + pane.activate_item(2, true, true, cx); + assert_eq!( + pane.active_item().unwrap().item_id(), + multi_buffer_with_both_files_id, + "Should select the multi buffer in the pane" + ); + }); + let close_all_but_multi_buffer_task = pane + .update(cx, |pane, cx| { + pane.close_inactive_items( + &CloseInactiveItems { + save_intent: Some(SaveIntent::Save), + close_pinned: true, + }, + cx, + ) + }) + .expect("should have inactive files to close"); + cx.background_executor.run_until_parked(); + assert!( + !cx.has_pending_prompt(), + "Multi buffer still has the unsaved buffer inside, so no save prompt should be shown" + ); + close_all_but_multi_buffer_task + .await + .expect("Closing all buffers but the multi buffer failed"); + pane.update(cx, |pane, cx| { + assert_eq!(dirty_regular_buffer.read(cx).save_count, 0); + assert_eq!(dirty_multi_buffer_with_both.read(cx).save_count, 0); + assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0); + assert_eq!(pane.items_len(), 1); + assert_eq!( + pane.active_item().unwrap().item_id(), + multi_buffer_with_both_files_id, + "Should have only the multi buffer left in the pane" + ); + assert!( + dirty_multi_buffer_with_both.read(cx).is_dirty, + "The multi buffer containing the unsaved buffer should still be dirty" + ); + }); + + let close_multi_buffer_task = pane + .update(cx, |pane, cx| { + pane.close_active_item( + &CloseActiveItem { + save_intent: Some(SaveIntent::Close), + }, + cx, + ) + }) + .expect("should have the multi buffer to close"); + cx.background_executor.run_until_parked(); + assert!( + cx.has_pending_prompt(), + "Dirty multi buffer should prompt a save dialog" + ); + cx.simulate_prompt_answer(0); + cx.background_executor.run_until_parked(); + close_multi_buffer_task + .await + .expect("Closing the multi buffer failed"); + pane.update(cx, |pane, cx| { + assert_eq!( + dirty_multi_buffer_with_both.read(cx).save_count, + 1, + "Multi buffer item should get be saved" + ); + // Test impl does not save inner items, so we do not assert them + assert_eq!( + pane.items_len(), + 0, + "No more items should be left in the pane" + ); + assert!(pane.active_item().is_none()); + }); + } + + #[gpui::test] + async fn test_no_save_prompt_when_dirty_singleton_buffer_closed_with_a_multi_buffer_containing_it_present_in_the_pane( + cx: &mut TestAppContext, + ) { + init_test(cx); + + let fs = FakeFs::new(cx.background_executor.clone()); + let project = Project::test(fs, [], cx).await; + let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); + let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone()); + + let dirty_regular_buffer = cx.new_view(|cx| { + TestItem::new(cx) + .with_dirty(true) + .with_label("1.txt") + .with_project_items(&[dirty_project_item(1, "1.txt", cx)]) + }); + let dirty_regular_buffer_2 = cx.new_view(|cx| { + TestItem::new(cx) + .with_dirty(true) + .with_label("2.txt") + .with_project_items(&[dirty_project_item(2, "2.txt", cx)]) + }); + let clear_regular_buffer = cx.new_view(|cx| { + TestItem::new(cx) + .with_label("3.txt") + .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)]) + }); + + let dirty_multi_buffer_with_both = cx.new_view(|cx| { + TestItem::new(cx) + .with_dirty(true) + .with_singleton(false) + .with_label("Fake Project Search") + .with_project_items(&[ + dirty_regular_buffer.read(cx).project_items[0].clone(), + dirty_regular_buffer_2.read(cx).project_items[0].clone(), + clear_regular_buffer.read(cx).project_items[0].clone(), + ]) + }); + workspace.update(cx, |workspace, cx| { + workspace.add_item( + pane.clone(), + Box::new(dirty_regular_buffer.clone()), + None, + false, + false, + cx, + ); + workspace.add_item( + pane.clone(), + Box::new(dirty_multi_buffer_with_both.clone()), + None, + false, + false, + cx, + ); + }); + + pane.update(cx, |pane, cx| { + pane.activate_item(0, true, true, cx); + assert_eq!( + pane.active_item().unwrap().item_id(), + dirty_regular_buffer.item_id(), + "Should select the dirty singleton buffer in the pane" + ); + }); + let close_singleton_buffer_task = pane + .update(cx, |pane, cx| { + pane.close_active_item(&CloseActiveItem { save_intent: None }, cx) + }) + .expect("should have active singleton buffer to close"); + cx.background_executor.run_until_parked(); + assert!( + !cx.has_pending_prompt(), + "Multi buffer is still in the pane and has the unsaved buffer inside, so no save prompt should be shown" + ); + + close_singleton_buffer_task + .await + .expect("Should not fail closing the singleton buffer"); + pane.update(cx, |pane, cx| { + assert_eq!(dirty_regular_buffer.read(cx).save_count, 0); + assert_eq!( + dirty_multi_buffer_with_both.read(cx).save_count, + 0, + "Multi buffer itself should not be saved" + ); + assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0); + assert_eq!( + pane.items_len(), + 1, + "A dirty multi buffer should be present in the pane" + ); + assert_eq!( + pane.active_item().unwrap().item_id(), + dirty_multi_buffer_with_both.item_id(), + "Should activate the only remaining item in the pane" + ); + }); + } + + #[gpui::test] + async fn test_save_prompt_when_dirty_multi_buffer_closed_with_some_of_its_dirty_items_not_present_in_the_pane( + cx: &mut TestAppContext, + ) { + init_test(cx); + + let fs = FakeFs::new(cx.background_executor.clone()); + let project = Project::test(fs, [], cx).await; + let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); + let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone()); + + let dirty_regular_buffer = cx.new_view(|cx| { + TestItem::new(cx) + .with_dirty(true) + .with_label("1.txt") + .with_project_items(&[dirty_project_item(1, "1.txt", cx)]) + }); + let dirty_regular_buffer_2 = cx.new_view(|cx| { + TestItem::new(cx) + .with_dirty(true) + .with_label("2.txt") + .with_project_items(&[dirty_project_item(2, "2.txt", cx)]) + }); + let clear_regular_buffer = cx.new_view(|cx| { + TestItem::new(cx) + .with_label("3.txt") + .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)]) + }); + + let dirty_multi_buffer_with_both = cx.new_view(|cx| { + TestItem::new(cx) + .with_dirty(true) + .with_singleton(false) + .with_label("Fake Project Search") + .with_project_items(&[ + dirty_regular_buffer.read(cx).project_items[0].clone(), + dirty_regular_buffer_2.read(cx).project_items[0].clone(), + clear_regular_buffer.read(cx).project_items[0].clone(), + ]) + }); + let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id(); + workspace.update(cx, |workspace, cx| { + workspace.add_item( + pane.clone(), + Box::new(dirty_regular_buffer.clone()), + None, + false, + false, + cx, + ); + workspace.add_item( + pane.clone(), + Box::new(dirty_multi_buffer_with_both.clone()), + None, + false, + false, + cx, + ); + }); + + pane.update(cx, |pane, cx| { + pane.activate_item(1, true, true, cx); + assert_eq!( + pane.active_item().unwrap().item_id(), + multi_buffer_with_both_files_id, + "Should select the multi buffer in the pane" + ); + }); + let _close_multi_buffer_task = pane + .update(cx, |pane, cx| { + pane.close_active_item(&CloseActiveItem { save_intent: None }, cx) + }) + .expect("should have active multi buffer to close"); + cx.background_executor.run_until_parked(); + assert!( + cx.has_pending_prompt(), + "With one dirty item from the multi buffer not being in the pane, a save prompt should be shown" + ); + } + + #[gpui::test] + async fn test_no_save_prompt_when_dirty_multi_buffer_closed_with_all_of_its_dirty_items_present_in_the_pane( + cx: &mut TestAppContext, + ) { + init_test(cx); + + let fs = FakeFs::new(cx.background_executor.clone()); + let project = Project::test(fs, [], cx).await; + let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); + let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone()); + + let dirty_regular_buffer = cx.new_view(|cx| { + TestItem::new(cx) + .with_dirty(true) + .with_label("1.txt") + .with_project_items(&[dirty_project_item(1, "1.txt", cx)]) + }); + let dirty_regular_buffer_2 = cx.new_view(|cx| { + TestItem::new(cx) + .with_dirty(true) + .with_label("2.txt") + .with_project_items(&[dirty_project_item(2, "2.txt", cx)]) + }); + let clear_regular_buffer = cx.new_view(|cx| { + TestItem::new(cx) + .with_label("3.txt") + .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)]) + }); + + let dirty_multi_buffer = cx.new_view(|cx| { + TestItem::new(cx) + .with_dirty(true) + .with_singleton(false) + .with_label("Fake Project Search") + .with_project_items(&[ + dirty_regular_buffer.read(cx).project_items[0].clone(), + dirty_regular_buffer_2.read(cx).project_items[0].clone(), + clear_regular_buffer.read(cx).project_items[0].clone(), + ]) + }); + workspace.update(cx, |workspace, cx| { + workspace.add_item( + pane.clone(), + Box::new(dirty_regular_buffer.clone()), + None, + false, + false, + cx, + ); + workspace.add_item( + pane.clone(), + Box::new(dirty_regular_buffer_2.clone()), + None, + false, + false, + cx, + ); + workspace.add_item( + pane.clone(), + Box::new(dirty_multi_buffer.clone()), + None, + false, + false, + cx, + ); + }); + + pane.update(cx, |pane, cx| { + pane.activate_item(2, true, true, cx); + assert_eq!( + pane.active_item().unwrap().item_id(), + dirty_multi_buffer.item_id(), + "Should select the multi buffer in the pane" + ); + }); + let close_multi_buffer_task = pane + .update(cx, |pane, cx| { + pane.close_active_item(&CloseActiveItem { save_intent: None }, cx) + }) + .expect("should have active multi buffer to close"); + cx.background_executor.run_until_parked(); + assert!( + !cx.has_pending_prompt(), + "All dirty items from the multi buffer are in the pane still, no save prompts should be shown" + ); + close_multi_buffer_task + .await + .expect("Closing multi buffer failed"); + pane.update(cx, |pane, cx| { + assert_eq!(dirty_regular_buffer.read(cx).save_count, 0); + assert_eq!(dirty_multi_buffer.read(cx).save_count, 0); + assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0); + assert_eq!( + pane.items() + .map(|item| item.item_id()) + .sorted() + .collect::>(), + vec![ + dirty_regular_buffer.item_id(), + dirty_regular_buffer_2.item_id(), + ], + "Should have no multi buffer left in the pane" + ); + assert!(dirty_regular_buffer.read(cx).is_dirty); + assert!(dirty_regular_buffer_2.read(cx).is_dirty); + }); + } + mod register_project_item_tests { use ui::Context as _; @@ -7423,7 +7856,7 @@ mod tests { // Model struct TestPngItem {} - impl project::Item for TestPngItem { + impl project::ProjectItem for TestPngItem { fn try_open( _project: &Model, path: &ProjectPath, @@ -7443,6 +7876,10 @@ mod tests { fn project_path(&self, _: &AppContext) -> Option { None } + + fn is_dirty(&self) -> bool { + false + } } impl Item for TestPngItemView { @@ -7485,7 +7922,7 @@ mod tests { // Model struct TestIpynbItem {} - impl project::Item for TestIpynbItem { + impl project::ProjectItem for TestIpynbItem { fn try_open( _project: &Model, path: &ProjectPath, @@ -7505,6 +7942,10 @@ mod tests { fn project_path(&self, _: &AppContext) -> Option { None } + + fn is_dirty(&self) -> bool { + false + } } impl Item for TestIpynbItemView { @@ -7702,4 +8143,12 @@ mod tests { Project::init_settings(cx); }); } + + fn dirty_project_item(id: u64, path: &str, cx: &mut AppContext) -> Model { + let item = TestProjectItem::new(id, path, cx); + item.update(cx, |item, _| { + item.is_dirty = true; + }); + item + } } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 4e3d05d2fb..2adb287b4d 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -29,7 +29,7 @@ use gpui::{ pub use open_listener::*; use outline_panel::OutlinePanel; use paths::{local_settings_file_relative_path, local_tasks_file_relative_path}; -use project::{DirectoryLister, Item}; +use project::{DirectoryLister, ProjectItem}; use project_panel::ProjectPanel; use quick_action_bar::QuickActionBar; use recent_projects::open_ssh_project; From bf569d720e8628d78d3ab4449ec202ea746c0a42 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sun, 1 Dec 2024 01:49:41 +0200 Subject: [PATCH 220/886] Always change editor selection when navigating outline panel entries (#21375) Also scroll to the center when doing so. This way, related editor's breadcrumbs always update, bringing more information. Release Notes: - Adjust outline panel item opening behavior to always change the editor selection, and center it --- crates/outline_panel/src/outline_panel.rs | 39 +++++++++++++++-------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 66db3a3103..103bf10eec 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -811,7 +811,7 @@ impl OutlinePanel { if self.filter_editor.focus_handle(cx).is_focused(cx) { cx.propagate() } else if let Some(selected_entry) = self.selected_entry().cloned() { - self.open_entry(&selected_entry, true, cx); + self.open_entry(&selected_entry, true, false, cx); } } @@ -834,7 +834,7 @@ impl OutlinePanel { } else if let Some((active_editor, selected_entry)) = self.active_editor().zip(self.selected_entry().cloned()) { - self.open_entry(&selected_entry, true, cx); + self.open_entry(&selected_entry, true, true, cx); active_editor.update(cx, |editor, cx| editor.open_excerpts(action, cx)); } } @@ -849,7 +849,7 @@ impl OutlinePanel { } else if let Some((active_editor, selected_entry)) = self.active_editor().zip(self.selected_entry().cloned()) { - self.open_entry(&selected_entry, true, cx); + self.open_entry(&selected_entry, true, true, cx); active_editor.update(cx, |editor, cx| editor.open_excerpts_in_split(action, cx)); } } @@ -858,6 +858,7 @@ impl OutlinePanel { &mut self, entry: &PanelEntry, change_selection: bool, + change_focus: bool, cx: &mut ViewContext, ) { let Some(active_editor) = self.active_editor() else { @@ -929,9 +930,9 @@ impl OutlinePanel { .workspace .update(cx, |workspace, cx| match self.active_item() { Some(active_item) => { - workspace.activate_item(active_item.as_ref(), true, change_selection, cx) + workspace.activate_item(active_item.as_ref(), true, change_focus, cx) } - None => workspace.activate_item(&active_editor, true, change_selection, cx), + None => workspace.activate_item(&active_editor, true, change_focus, cx), }); if activate.is_ok() { @@ -939,16 +940,20 @@ impl OutlinePanel { if change_selection { active_editor.update(cx, |editor, cx| { editor.change_selections( - Some(Autoscroll::Strategy(AutoscrollStrategy::Top)), + Some(Autoscroll::Strategy(AutoscrollStrategy::Center)), cx, |s| s.select_ranges(Some(anchor..anchor)), ); }); - active_editor.focus_handle(cx).focus(cx); } else { active_editor.update(cx, |editor, cx| { editor.set_scroll_anchor(ScrollAnchor { offset, anchor }, cx); }); + } + + if change_focus { + active_editor.focus_handle(cx).focus(cx); + } else { self.focus_handle.focus(cx); } } @@ -969,7 +974,7 @@ impl OutlinePanel { self.select_first(&SelectFirst {}, cx) } if let Some(selected_entry) = self.selected_entry().cloned() { - self.open_entry(&selected_entry, false, cx); + self.open_entry(&selected_entry, true, false, cx); } } @@ -988,7 +993,7 @@ impl OutlinePanel { self.select_last(&SelectLast, cx) } if let Some(selected_entry) = self.selected_entry().cloned() { - self.open_entry(&selected_entry, false, cx); + self.open_entry(&selected_entry, true, false, cx); } } @@ -2027,9 +2032,9 @@ impl OutlinePanel { if event.down.button == MouseButton::Right || event.down.first_mouse { return; } - let change_selection = event.down.click_count > 1; + let change_focus = event.down.click_count > 1; outline_panel.toggle_expanded(&clicked_entry, cx); - outline_panel.open_entry(&clicked_entry, change_selection, cx); + outline_panel.open_entry(&clicked_entry, true, change_focus, cx); }) }) .cursor_pointer() @@ -4863,9 +4868,13 @@ mod tests { ), select_first_in_all_matches(navigated_outline_selection) ); + }); + cx.executor() + .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); + outline_panel.update(cx, |_, cx| { assert_eq!( selected_row_text(&active_editor, cx), - initial_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes + navigated_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes "Should still have the initial caret position after SelectNext calls" ); }); @@ -4895,9 +4904,13 @@ mod tests { ), select_first_in_all_matches(next_navigated_outline_selection) ); + }); + cx.executor() + .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); + outline_panel.update(cx, |_, cx| { assert_eq!( selected_row_text(&active_editor, cx), - navigated_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes + next_navigated_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes "Should again preserve the selection after another SelectNext call" ); }); From 4d5415273ea4b3798748b6c66dc8b7694db91d68 Mon Sep 17 00:00:00 2001 From: yoleuh Date: Sun, 1 Dec 2024 03:59:29 -0500 Subject: [PATCH 221/886] Docs: Update developing zed docs to match (#21379) Some changes just so the build docs for the different os matches each other :) macos: - moved `rust wasm toolchain install` up under `rust install` (match windows docs) - add instructions to update rust if already installed (match windows and linux docs) windows: - add `(required by a dependency)` to cmake install (match macos docs) Release Notes: - N/A --- docs/src/development/linux.md | 6 +----- docs/src/development/macos.md | 9 ++------- docs/src/development/windows.md | 14 ++------------ 3 files changed, 5 insertions(+), 24 deletions(-) diff --git a/docs/src/development/linux.md b/docs/src/development/linux.md index 5dba44d2f0..1505f99e88 100644 --- a/docs/src/development/linux.md +++ b/docs/src/development/linux.md @@ -6,11 +6,7 @@ Clone down the [Zed repository](https://github.com/zed-industries/zed). ## Dependencies -- Install [Rust](https://www.rust-lang.org/tools/install). If it's already installed, make sure it's up-to-date: - - ```sh - rustup update - ``` +- Install [rustup](https://www.rust-lang.org/tools/install) - Install the necessary system libraries: diff --git a/docs/src/development/macos.md b/docs/src/development/macos.md index 2fd076b0fa..fe15e9f56e 100644 --- a/docs/src/development/macos.md +++ b/docs/src/development/macos.md @@ -6,7 +6,8 @@ Clone down the [Zed repository](https://github.com/zed-industries/zed). ## Dependencies -- Install [Rust](https://www.rust-lang.org/tools/install) +- Install [rustup](https://www.rust-lang.org/tools/install) + - Install [Xcode](https://apps.apple.com/us/app/xcode/id497799835?mt=12) from the macOS App Store, or from the [Apple Developer](https://developer.apple.com/download/all/) website. Note this requires a developer account. > Ensure you launch Xcode after installing, and install the macOS components, which is the default option. @@ -24,12 +25,6 @@ Clone down the [Zed repository](https://github.com/zed-industries/zed). sudo xcodebuild -license accept ``` -- Install the Rust wasm toolchain: - - ```sh - rustup target add wasm32-wasip1 - ``` - - Install `cmake` (required by [a dependency](https://docs.rs/wasmtime-c-api-impl/latest/wasmtime_c_api/)) ```sh diff --git a/docs/src/development/windows.md b/docs/src/development/windows.md index f95cfb3ed0..9cb539366d 100644 --- a/docs/src/development/windows.md +++ b/docs/src/development/windows.md @@ -8,21 +8,11 @@ Clone down the [Zed repository](https://github.com/zed-industries/zed). ## Dependencies -- Install [Rust](https://www.rust-lang.org/tools/install). If it's already installed, make sure it's up-to-date: - - ```sh - rustup update - ``` - -- Install the Rust wasm toolchain: - - ```sh - rustup target add wasm32-wasip1 - ``` +- Install [rustup](https://www.rust-lang.org/tools/install) - Install [Visual Studio](https://visualstudio.microsoft.com/downloads/) with the optional components `MSVC v*** - VS YYYY C++ x64/x86 build tools` and `MSVC v*** - VS YYYY C++ x64/x86 Spectre-mitigated libs (latest)` (`v***` is your VS version and `YYYY` is year when your VS was released. Pay attention to the architecture and change it to yours if needed.) - Install Windows 11 or 10 SDK depending on your system, but ensure that at least `Windows 10 SDK version 2104 (10.0.20348.0)` is installed on your machine. You can download it from the [Windows SDK Archive](https://developer.microsoft.com/windows/downloads/windows-sdk/) -- Install [CMake](https://cmake.org/download) +- Install [CMake](https://cmake.org/download) (required by [a dependency](https://docs.rs/wasmtime-c-api-impl/latest/wasmtime_c_api/)) ## Backend dependencies From 5f6b200d8d206b77b0c3aac9edb4b8d80f17eb5a Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sun, 1 Dec 2024 14:28:48 +0200 Subject: [PATCH 222/886] Do not change selections when opening FS entries (#21382) Follow-up of https://github.com/zed-industries/zed/pull/21375 When changing selections for FS entries, outline panel will be forced to change item to the first excerpt which is not what we want. Release Notes: - N/A --- crates/outline_panel/src/outline_panel.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 103bf10eec..f36e144c88 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -857,7 +857,7 @@ impl OutlinePanel { fn open_entry( &mut self, entry: &PanelEntry, - change_selection: bool, + prefer_selection_change: bool, change_focus: bool, cx: &mut ViewContext, ) { @@ -872,9 +872,11 @@ impl OutlinePanel { Point::new(0.0, -(active_editor.read(cx).file_header_size() as f32)) }; + let mut change_selection = prefer_selection_change; let scroll_target = match entry { PanelEntry::FoldedDirs(..) | PanelEntry::Fs(FsEntry::Directory(..)) => None, PanelEntry::Fs(FsEntry::ExternalFile(buffer_id, _)) => { + change_selection = false; let scroll_target = multi_buffer_snapshot.excerpts().find_map( |(excerpt_id, buffer_snapshot, excerpt_range)| { if &buffer_snapshot.remote_id() == buffer_id { @@ -888,6 +890,7 @@ impl OutlinePanel { Some(offset_from_top).zip(scroll_target) } PanelEntry::Fs(FsEntry::File(_, file_entry, ..)) => { + change_selection = false; let scroll_target = self .project .update(cx, |project, cx| { From 89a56968f6570bc650b0283a45f60f83479beb84 Mon Sep 17 00:00:00 2001 From: moskirathe <39177599+moskirathe@users.noreply.github.com> Date: Sun, 1 Dec 2024 17:02:12 -0500 Subject: [PATCH 223/886] Fix typos in key-bindings documentation (#21390) Release Notes: Fixes two minor typos in the key-bindings documentation. --- docs/src/key-bindings.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/key-bindings.md b/docs/src/key-bindings.md index 68db517480..660a80ebd4 100644 --- a/docs/src/key-bindings.md +++ b/docs/src/key-bindings.md @@ -130,7 +130,7 @@ When multiple keybindings have the same keystroke and are active at the same tim The other kind of conflict that arises is when you have two bindings, one of which is a prefix of the other. For example if you have `"ctrl-w":"editor::DeleteToNextWordEnd"` and `"ctrl-w left":"editor::DeleteToEndOfLine"`. -When this happens, and both bindings are active in the current context, Zed will wait for 1 second after you tupe `ctrl-w` to se if you're about to type `left`. If you don't type anything, or if you type a different key, then `DeleteToNextWordEnd` will be triggered. If you do, then `DeleteToEndOfLine` will be triggered. +When this happens, and both bindings are active in the current context, Zed will wait for 1 second after you type `ctrl-w` to see if you're about to type `left`. If you don't type anything, or if you type a different key, then `DeleteToNextWordEnd` will be triggered. If you do, then `DeleteToEndOfLine` will be triggered. ### Non-QWERTY keyboards From 380679fcc23ba978401a8bb091716d4a05fab937 Mon Sep 17 00:00:00 2001 From: fred-sch <73998525+fred-sch@users.noreply.github.com> Date: Mon, 2 Dec 2024 10:35:29 +0100 Subject: [PATCH 224/886] Fix: Copilot Chat is logged out (#21360) Closes #21255 Release Notes: - Fixed Copilot Chat OAuth Token parsing --------- Co-authored-by: Bennet Bo Fenner --- crates/copilot/src/copilot_chat.rs | 41 ++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/crates/copilot/src/copilot_chat.rs b/crates/copilot/src/copilot_chat.rs index 075c3b69b1..daddefb579 100644 --- a/crates/copilot/src/copilot_chat.rs +++ b/crates/copilot/src/copilot_chat.rs @@ -197,7 +197,7 @@ pub fn init(fs: Arc, client: Arc, cx: &mut AppContext) { cx.set_global(GlobalCopilotChat(copilot_chat)); } -fn copilot_chat_config_path() -> &'static PathBuf { +fn copilot_chat_config_dir() -> &'static PathBuf { static COPILOT_CHAT_CONFIG_DIR: OnceLock = OnceLock::new(); COPILOT_CHAT_CONFIG_DIR.get_or_init(|| { @@ -207,10 +207,14 @@ fn copilot_chat_config_path() -> &'static PathBuf { home_dir().join(".config") } .join("github-copilot") - .join("hosts.json") }) } +fn copilot_chat_config_paths() -> [PathBuf; 2] { + let base_dir = copilot_chat_config_dir(); + [base_dir.join("hosts.json"), base_dir.join("apps.json")] +} + impl CopilotChat { pub fn global(cx: &AppContext) -> Option> { cx.try_global::() @@ -218,13 +222,24 @@ impl CopilotChat { } pub fn new(fs: Arc, client: Arc, cx: &AppContext) -> Self { - let mut config_file_rx = watch_config_file( - cx.background_executor(), - fs, - copilot_chat_config_path().clone(), - ); + let config_paths = copilot_chat_config_paths(); + + let resolve_config_path = { + let fs = fs.clone(); + async move { + for config_path in config_paths.iter() { + if fs.metadata(config_path).await.is_ok_and(|v| v.is_some()) { + return config_path.clone(); + } + } + config_paths[0].clone() + } + }; cx.spawn(|cx| async move { + let config_file = resolve_config_path.await; + let mut config_file_rx = watch_config_file(cx.background_executor(), fs, config_file); + while let Some(contents) = config_file_rx.next().await { let oauth_token = extract_oauth_token(contents); @@ -318,9 +333,15 @@ async fn request_api_token(oauth_token: &str, client: Arc) -> Re fn extract_oauth_token(contents: String) -> Option { serde_json::from_str::(&contents) .map(|v| { - v["github.com"]["oauth_token"] - .as_str() - .map(|v| v.to_string()) + v.as_object().and_then(|obj| { + obj.iter().find_map(|(key, value)| { + if key.starts_with("github.com") { + value["oauth_token"].as_str().map(|v| v.to_string()) + } else { + None + } + }) + }) }) .ok() .flatten() From 740ba7817bfa94cfc38f3523bc1cc492d950ecdc Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 2 Dec 2024 07:47:57 -0300 Subject: [PATCH 225/886] Fine-tune terminal tab bar actions spacing (#21391) Just quickly reducing the spacing between the terminal tab bar actions so they're tighter and matching other similar components. | Before | After | |--------|--------| | Screenshot 2024-12-01 at 19 20 50 | Screenshot 2024-12-01 at 19 18 19 | Release Notes: - N/A --- crates/terminal_view/src/terminal_panel.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 1799d24c7d..532d5d9040 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -26,8 +26,8 @@ use terminal::{ Terminal, }; use ui::{ - div, h_flex, ButtonCommon, Clickable, ContextMenu, FluentBuilder, IconButton, IconSize, - InteractiveElement, PopoverMenu, Selectable, Tooltip, + prelude::*, ButtonCommon, Clickable, ContextMenu, FluentBuilder, PopoverMenu, Selectable, + Tooltip, }; use util::{ResultExt, TryFutureExt}; use workspace::{ @@ -139,14 +139,13 @@ impl TerminalPanel { } let focus_handle = pane.focus_handle(cx); let right_children = h_flex() - .gap_2() - .children(assistant_tab_bar_button.clone()) + .gap(DynamicSpacing::Base02.rems(cx)) .child( PopoverMenu::new("terminal-tab-bar-popover-menu") .trigger( IconButton::new("plus", IconName::Plus) .icon_size(IconSize::Small) - .tooltip(|cx| Tooltip::text("New...", cx)), + .tooltip(|cx| Tooltip::text("New…", cx)), ) .anchor(AnchorCorner::TopRight) .with_handle(pane.new_item_context_menu_handle.clone()) @@ -170,6 +169,7 @@ impl TerminalPanel { Some(menu) }), ) + .children(assistant_tab_bar_button.clone()) .child( PopoverMenu::new("terminal-pane-tab-bar-split") .trigger( From dacd919e27aebbdc3dd466e395d6afbfd514b32a Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 2 Dec 2024 07:48:10 -0300 Subject: [PATCH 226/886] Add setting for making the tab's close button always visible (#21352) Closes https://github.com/zed-industries/zed/issues/20422 Screenshot 2024-11-29 at 22 00 20 Release Notes: - N/A --- assets/settings/default.json | 2 ++ crates/workspace/src/item.rs | 5 +++++ crates/workspace/src/pane.rs | 8 ++++++-- docs/src/configuring-zed.md | 9 ++++++++- 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index b844be7fa2..5930537856 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -559,6 +559,8 @@ "close_position": "right", // Whether to show the file icon for a tab. "file_icons": false, + // Whether to always show the close button on tabs. + "always_show_close_button": false, // What to do after closing the current tab. // // 1. Activate the tab that was open previously (default) diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 40d92666a0..eab3ddc755 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -42,6 +42,7 @@ pub struct ItemSettings { pub close_position: ClosePosition, pub activate_on_close: ActivateOnClose, pub file_icons: bool, + pub always_show_close_button: bool, } #[derive(Deserialize)] @@ -85,6 +86,10 @@ pub struct ItemSettingsContent { /// /// Default: history pub activate_on_close: Option, + /// Whether to always show the close button on tabs. + /// + /// Default: false + always_show_close_button: Option, } #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 66db71553f..83cc911a91 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1951,7 +1951,9 @@ impl Pane { }; let icon = item.tab_icon(cx); - let close_side = &ItemSettings::get_global(cx).close_position; + let settings = ItemSettings::get_global(cx); + let close_side = &settings.close_position; + let always_show_close_button = settings.always_show_close_button; let indicator = render_item_indicator(item.boxed_clone(), cx); let item_id = item.item_id(); let is_first_item = ix == 0; @@ -2046,7 +2048,9 @@ impl Pane { end_slot_action = &CloseActiveItem { save_intent: None }; end_slot_tooltip_text = "Close Tab"; IconButton::new("close tab", IconName::Close) - .visible_on_hover("") + .when(!always_show_close_button, |button| { + button.visible_on_hover("") + }) .shape(IconButtonShape::Square) .icon_color(Color::Muted) .size(ButtonSize::None) diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index bd1da9ece8..e71266a01f 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -634,7 +634,8 @@ List of `string` values "close_position": "right", "file_icons": false, "git_status": false, - "activate_on_close": "history" + "activate_on_close": "history", + "always_show_close_button": false }, ``` @@ -698,6 +699,12 @@ List of `string` values } ``` +### Always show the close button + +- Description: Whether to always show the close button on tabs. +- Setting: `always_show_close_button` +- Default: `false` + ## Editor Toolbar - Description: Whether or not to show various elements in the editor toolbar. From 2300f40cd987bdb3602769d312786eb4118d711c Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 2 Dec 2024 09:28:46 -0300 Subject: [PATCH 227/886] Add consistent placeholder text for terminal inline assist (#21398) Ensuring it is consistent with the buffer inline assistant. Just thought of not having "Transform" here as that felt it made less sense for terminal-related prompts, where arguably more frequently, one would be suggesting for actual commands rather than code transformation. Screenshot 2024-12-02 at 09 11 00 Release Notes: - N/A --- crates/assistant/src/terminal_inline_assistant.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/assistant/src/terminal_inline_assistant.rs b/crates/assistant/src/terminal_inline_assistant.rs index a5424a8d7e..d60a556cf0 100644 --- a/crates/assistant/src/terminal_inline_assistant.rs +++ b/crates/assistant/src/terminal_inline_assistant.rs @@ -32,7 +32,7 @@ use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase}; use terminal::Terminal; use terminal_view::TerminalView; use theme::ThemeSettings; -use ui::{prelude::*, IconButtonShape, Tooltip}; +use ui::{prelude::*, text_for_action, IconButtonShape, Tooltip}; use util::ResultExt; use workspace::{notifications::NotificationId, Toast, Workspace}; @@ -704,7 +704,7 @@ impl PromptEditor { cx, ); editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx); - editor.set_placeholder_text("Add a prompt…", cx); + editor.set_placeholder_text(Self::placeholder_text(cx), cx); editor }); @@ -737,6 +737,14 @@ impl PromptEditor { this } + fn placeholder_text(cx: &WindowContext) -> String { + let context_keybinding = text_for_action(&crate::ToggleFocus, cx) + .map(|keybinding| format!(" • {keybinding} for context")) + .unwrap_or_default(); + + format!("Generate…{context_keybinding} • ↓↑ for history") + } + fn subscribe_to_editor(&mut self, cx: &mut ViewContext) { self.editor_subscriptions.clear(); self.editor_subscriptions From 0cb3a6ed0ebbb0eec6bc7f1d15732a6b0da1c262 Mon Sep 17 00:00:00 2001 From: Delyan Angelov Date: Mon, 2 Dec 2024 15:51:28 +0200 Subject: [PATCH 228/886] Add V file icon (#20017) Here is a preview of the new `v.svg` in comparison with some of the existing icons: ![image](https://github.com/user-attachments/assets/451762ff-b13a-42b9-89ac-695f25a43a84) --------- Co-authored-by: Danilo Leal --- assets/icons/file_icons/file_types.json | 6 ++++++ assets/icons/file_icons/v.svg | 4 ++++ 2 files changed, 10 insertions(+) create mode 100644 assets/icons/file_icons/v.svg diff --git a/assets/icons/file_icons/file_types.json b/assets/icons/file_icons/file_types.json index fe293256b3..8c6a624416 100644 --- a/assets/icons/file_icons/file_types.json +++ b/assets/icons/file_icons/file_types.json @@ -173,6 +173,9 @@ "tsx": "react", "ttf": "font", "txt": "document", + "v": "v", + "vsh": "v", + "vv": "v", "vue": "vue", "wav": "audio", "webm": "video", @@ -379,6 +382,9 @@ "typescript": { "icon": "icons/file_icons/typescript.svg" }, + "v": { + "icon": "icons/file_icons/v.svg" + }, "vcs": { "icon": "icons/file_icons/git.svg" }, diff --git a/assets/icons/file_icons/v.svg b/assets/icons/file_icons/v.svg new file mode 100644 index 0000000000..485e27a378 --- /dev/null +++ b/assets/icons/file_icons/v.svg @@ -0,0 +1,4 @@ + + + + From 6cb758a1cd2a5c46b7074dfd1f455ea4159654be Mon Sep 17 00:00:00 2001 From: loczek <30776250+loczek@users.noreply.github.com> Date: Mon, 2 Dec 2024 15:37:41 +0100 Subject: [PATCH 229/886] theme_importer: Add more mappings (#21393) This PR adds `search_match_background` and `editor_document_highlight_bracket_background` color mappings as they appear to be missing. --- crates/theme_importer/src/vscode/converter.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/theme_importer/src/vscode/converter.rs b/crates/theme_importer/src/vscode/converter.rs index cca4b56321..a1a6c7a27c 100644 --- a/crates/theme_importer/src/vscode/converter.rs +++ b/crates/theme_importer/src/vscode/converter.rs @@ -159,7 +159,9 @@ impl VsCodeThemeConverter { .active_background .clone() .or(vscode_tab_inactive_background.clone()), + search_match_background: vscode_colors.editor.find_match_background.clone(), panel_background: vscode_colors.panel.background.clone(), + pane_group_border: vscode_colors.editor_group.border.clone(), scrollbar_thumb_background: vscode_scrollbar_slider_background.clone(), scrollbar_thumb_hover_background: vscode_colors .scrollbar_slider @@ -168,7 +170,6 @@ impl VsCodeThemeConverter { scrollbar_thumb_border: vscode_scrollbar_slider_background.clone(), scrollbar_track_background: vscode_editor_background.clone(), scrollbar_track_border: vscode_colors.editor_overview_ruler.border.clone(), - pane_group_border: vscode_colors.editor_group.border.clone(), editor_foreground: vscode_editor_foreground .clone() .or(vscode_token_colors_foreground.clone()), @@ -179,6 +180,10 @@ impl VsCodeThemeConverter { editor_active_line_number: vscode_colors.editor.foreground.clone(), editor_wrap_guide: vscode_panel_border.clone(), editor_active_wrap_guide: vscode_panel_border.clone(), + editor_document_highlight_bracket_background: vscode_colors + .editor_bracket_match + .background + .clone(), terminal_background: vscode_colors.terminal.background.clone(), terminal_ansi_black: vscode_colors.terminal.ansi_black.clone(), terminal_ansi_bright_black: vscode_colors.terminal.ansi_bright_black.clone(), From 3987d0d7317408091c0ac0a706c5508a3c97af92 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Mon, 2 Dec 2024 16:56:47 +0100 Subject: [PATCH 230/886] Treat `.pcss` files as CSS (#21402) This addresses https://github.com/zed-industries/zed/pull/19416#discussion_r1865019293 and also follows the [associated PostCSS file extensions for VS Code](https://github.com/csstools/postcss-language/blob/5d003170c5ed962b09b9a0f3725a6cae885df292/package.json#L37). Release Notes: - `.pcss` files are now recognized as CSS --------- Co-authored-by: Marshall Bowers --- assets/icons/file_icons/file_types.json | 1 + crates/languages/src/css/config.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/assets/icons/file_icons/file_types.json b/assets/icons/file_icons/file_types.json index 8c6a624416..5e927369d3 100644 --- a/assets/icons/file_icons/file_types.json +++ b/assets/icons/file_icons/file_types.json @@ -127,6 +127,7 @@ "ogg": "audio", "opus": "audio", "otf": "font", + "pcss": "css", "pdb": "storage", "pdf": "document", "php": "php", diff --git a/crates/languages/src/css/config.toml b/crates/languages/src/css/config.toml index 9b0c9c703c..d6ea2f9c7f 100644 --- a/crates/languages/src/css/config.toml +++ b/crates/languages/src/css/config.toml @@ -1,6 +1,6 @@ name = "CSS" grammar = "css" -path_suffixes = ["css", "postcss"] +path_suffixes = ["css", "postcss", "pcss"] autoclose_before = ";:.,=}])>" brackets = [ { start = "{", end = "}", close = true, newline = true }, From 89e46396f6d06a32c3a917fa4a392ab82b32e345 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 2 Dec 2024 17:08:16 +0100 Subject: [PATCH 231/886] workspace: Serialize active panel even if it's not visible (#21408) Fixes #21285 Closes #21285 Release Notes: - Fixed workspace serialization of collapsed panels --- crates/workspace/src/workspace.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 7945c4e404..a8681f22c5 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -4144,30 +4144,30 @@ impl Workspace { let left_dock = this.left_dock.read(cx); let left_visible = left_dock.is_open(); let left_active_panel = left_dock - .visible_panel() + .active_panel() .map(|panel| panel.persistent_name().to_string()); let left_dock_zoom = left_dock - .visible_panel() + .active_panel() .map(|panel| panel.is_zoomed(cx)) .unwrap_or(false); let right_dock = this.right_dock.read(cx); let right_visible = right_dock.is_open(); let right_active_panel = right_dock - .visible_panel() + .active_panel() .map(|panel| panel.persistent_name().to_string()); let right_dock_zoom = right_dock - .visible_panel() + .active_panel() .map(|panel| panel.is_zoomed(cx)) .unwrap_or(false); let bottom_dock = this.bottom_dock.read(cx); let bottom_visible = bottom_dock.is_open(); let bottom_active_panel = bottom_dock - .visible_panel() + .active_panel() .map(|panel| panel.persistent_name().to_string()); let bottom_dock_zoom = bottom_dock - .visible_panel() + .active_panel() .map(|panel| panel.is_zoomed(cx)) .unwrap_or(false); From 995b40f1498b20a27ed1c11adb9551a273884d7b Mon Sep 17 00:00:00 2001 From: uncenter <47499684+uncenter@users.noreply.github.com> Date: Mon, 2 Dec 2024 11:19:42 -0500 Subject: [PATCH 232/886] Add "Copy Extension ID" action to extension card dropdown (#21395) Adds a new "Copy Extension ID" action to the dropdown of remote extension cards in the extensions list UI. Would have liked for it to be a context menu where you could click anywhere on the card, but couldn't figure out how to integrate that with the existing setup. I've been missing this from VSCode's extension panel, which allows this on right click: ![CleanShot 2024-12-01 at 22 03 14](https://github.com/user-attachments/assets/64796f96-1a37-4ba2-bfe1-971b939aa50a) This is useful if you, say, want to add some extensions to https://zed.dev/docs/configuring-zed#auto-install-extensions, where you need the IDs. Release Notes: - Added "Copy Extension ID" action to extension card dropdown --------- Co-authored-by: Marshall Bowers --- crates/extensions_ui/src/extensions_ui.rs | 24 +++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs index eaffdafa41..aef99e6167 100644 --- a/crates/extensions_ui/src/extensions_ui.rs +++ b/crates/extensions_ui/src/extensions_ui.rs @@ -14,7 +14,7 @@ use editor::{Editor, EditorElement, EditorStyle}; use extension_host::{ExtensionManifest, ExtensionOperation, ExtensionStore}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ - actions, uniform_list, Action, AppContext, EventEmitter, Flatten, FocusableView, + actions, uniform_list, Action, AppContext, ClipboardItem, EventEmitter, Flatten, FocusableView, InteractiveElement, KeyContext, ParentElement, Render, Styled, Task, TextStyle, UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WindowContext, }; @@ -637,13 +637,21 @@ impl ExtensionsPage { cx: &mut WindowContext, ) -> View { let context_menu = ContextMenu::build(cx, |context_menu, cx| { - context_menu.entry( - "Install Another Version...", - None, - cx.handler_for(this, move |this, cx| { - this.show_extension_version_list(extension_id.clone(), cx) - }), - ) + context_menu + .entry( + "Install Another Version...", + None, + cx.handler_for(this, { + let extension_id = extension_id.clone(); + move |this, cx| this.show_extension_version_list(extension_id.clone(), cx) + }), + ) + .entry("Copy Extension ID", None, { + let extension_id = extension_id.clone(); + move |cx| { + cx.write_to_clipboard(ClipboardItem::new_string(extension_id.to_string())); + } + }) }); context_menu From f795ce9623cade05e7ba361632aea3b00d062f65 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 2 Dec 2024 15:01:09 -0300 Subject: [PATCH 233/886] Add language icons to the language selector (#21298) Closes https://github.com/zed-industries/zed/issues/21290 This is a first attempt to show the language icons to the selector. Ideally, I wouldn't like to have yet another place mapping extensions to icons, as we already have the `file_types.json` file doing that, but I'm not so sure how to pull from it yet. Maybe in a future pass we'll improve this and make it more solid. Screenshot 2024-11-28 at 16 10 27 Release Notes: - N/A --------- Co-authored-by: Kirill Bulatov Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> --- Cargo.lock | 3 + assets/icons/file_icons/diff.svg | 5 ++ assets/icons/file_icons/file_types.json | 6 ++ crates/extension/src/extension_host_proxy.rs | 4 +- crates/extension_host/src/extension_host.rs | 3 + .../src/extension_store_test.rs | 2 + crates/extension_host/src/headless_host.rs | 1 + crates/file_finder/src/file_finder.rs | 2 +- crates/language/src/language.rs | 8 ++ crates/language/src/language_registry.rs | 15 ++-- .../src/language_extension.rs | 3 +- crates/language_selector/Cargo.toml | 3 + .../src/language_selector.rs | 76 ++++++++++++++++--- crates/languages/src/jsdoc/config.toml | 1 + crates/languages/src/lib.rs | 4 + crates/languages/src/regex/config.toml | 1 + 16 files changed, 119 insertions(+), 18 deletions(-) create mode 100644 assets/icons/file_icons/diff.svg diff --git a/Cargo.lock b/Cargo.lock index 7768dac710..e3bdc89f5f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6709,11 +6709,14 @@ version = "0.1.0" dependencies = [ "anyhow", "editor", + "file_finder", + "file_icons", "fuzzy", "gpui", "language", "picker", "project", + "settings", "ui", "util", "workspace", diff --git a/assets/icons/file_icons/diff.svg b/assets/icons/file_icons/diff.svg new file mode 100644 index 0000000000..07c46f1799 --- /dev/null +++ b/assets/icons/file_icons/diff.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/file_icons/file_types.json b/assets/icons/file_icons/file_types.json index 5e927369d3..89da63ddda 100644 --- a/assets/icons/file_icons/file_types.json +++ b/assets/icons/file_icons/file_types.json @@ -34,6 +34,7 @@ "dat": "storage", "db": "storage", "dbf": "storage", + "diff": "diff", "dll": "storage", "doc": "document", "docx": "document", @@ -112,6 +113,7 @@ "mkv": "video", "ml": "ocaml", "mli": "ocaml", + "mod": "go", "mov": "video", "mp3": "audio", "mp4": "video", @@ -185,6 +187,7 @@ "wmv": "video", "woff": "font", "woff2": "font", + "work": "go", "wv": "audio", "xls": "document", "xlsx": "document", @@ -239,6 +242,9 @@ "default": { "icon": "icons/file_icons/file.svg" }, + "diff": { + "icon": "icons/file_icons/diff.svg" + }, "docker": { "icon": "icons/file_icons/docker.svg" }, diff --git a/crates/extension/src/extension_host_proxy.rs b/crates/extension/src/extension_host_proxy.rs index 8909a6082d..3fa35597a8 100644 --- a/crates/extension/src/extension_host_proxy.rs +++ b/crates/extension/src/extension_host_proxy.rs @@ -159,6 +159,7 @@ pub trait ExtensionLanguageProxy: Send + Sync + 'static { language: LanguageName, grammar: Option>, matcher: LanguageMatcher, + hidden: bool, load: Arc Result + Send + Sync + 'static>, ); @@ -175,13 +176,14 @@ impl ExtensionLanguageProxy for ExtensionHostProxy { language: LanguageName, grammar: Option>, matcher: LanguageMatcher, + hidden: bool, load: Arc Result + Send + Sync + 'static>, ) { let Some(proxy) = self.language_proxy.read().clone() else { return; }; - proxy.register_language(language, grammar, matcher, load) + proxy.register_language(language, grammar, matcher, hidden, load) } fn remove_languages( diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index aab5c258f5..7ceb1fa714 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -162,6 +162,7 @@ pub struct ExtensionIndexLanguageEntry { pub extension: Arc, pub path: PathBuf, pub matcher: LanguageMatcher, + pub hidden: bool, pub grammar: Option>, } @@ -1097,6 +1098,7 @@ impl ExtensionStore { language_name.clone(), language.grammar.clone(), language.matcher.clone(), + language.hidden, Arc::new(move || { let config = std::fs::read_to_string(language_path.join("config.toml"))?; let config: LanguageConfig = ::toml::from_str(&config)?; @@ -1324,6 +1326,7 @@ impl ExtensionStore { extension: extension_id.clone(), path: relative_path, matcher: config.matcher, + hidden: config.hidden, grammar: config.grammar, }, ); diff --git a/crates/extension_host/src/extension_store_test.rs b/crates/extension_host/src/extension_store_test.rs index 1359b5b202..8b5a2a7821 100644 --- a/crates/extension_host/src/extension_store_test.rs +++ b/crates/extension_host/src/extension_store_test.rs @@ -203,6 +203,7 @@ async fn test_extension_store(cx: &mut TestAppContext) { extension: "zed-ruby".into(), path: "languages/erb".into(), grammar: Some("embedded_template".into()), + hidden: false, matcher: LanguageMatcher { path_suffixes: vec!["erb".into()], first_line_pattern: None, @@ -215,6 +216,7 @@ async fn test_extension_store(cx: &mut TestAppContext) { extension: "zed-ruby".into(), path: "languages/ruby".into(), grammar: Some("ruby".into()), + hidden: false, matcher: LanguageMatcher { path_suffixes: vec!["rb".into()], first_line_pattern: None, diff --git a/crates/extension_host/src/headless_host.rs b/crates/extension_host/src/headless_host.rs index 19a574b9d4..687f05db47 100644 --- a/crates/extension_host/src/headless_host.rs +++ b/crates/extension_host/src/headless_host.rs @@ -156,6 +156,7 @@ impl HeadlessExtensionStore { config.name.clone(), None, config.matcher.clone(), + config.hidden, Arc::new(move || { Ok(LoadedLanguage { config: config.clone(), diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 6a758211f8..62e0818b74 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -1,7 +1,7 @@ #[cfg(test)] mod file_finder_tests; -mod file_finder_settings; +pub mod file_finder_settings; mod new_path_prompt; mod open_path_prompt; diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 2725122990..e9590448f8 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -129,6 +129,10 @@ pub static PLAIN_TEXT: LazyLock> = LazyLock::new(|| { LanguageConfig { name: "Plain Text".into(), soft_wrap: Some(SoftWrap::EditorWidth), + matcher: LanguageMatcher { + path_suffixes: vec!["txt".to_owned()], + first_line_pattern: None, + }, ..Default::default() }, None, @@ -1418,6 +1422,10 @@ impl Language { pub fn prettier_parser_name(&self) -> Option<&str> { self.config.prettier_parser_name.as_deref() } + + pub fn config(&self) -> &LanguageConfig { + &self.config + } } impl LanguageScope { diff --git a/crates/language/src/language_registry.rs b/crates/language/src/language_registry.rs index d8c2b0d510..e5f7815351 100644 --- a/crates/language/src/language_registry.rs +++ b/crates/language/src/language_registry.rs @@ -130,6 +130,7 @@ pub struct AvailableLanguage { name: LanguageName, grammar: Option>, matcher: LanguageMatcher, + hidden: bool, load: Arc Result + 'static + Send + Sync>, loaded: bool, } @@ -142,6 +143,9 @@ impl AvailableLanguage { pub fn matcher(&self) -> &LanguageMatcher { &self.matcher } + pub fn hidden(&self) -> bool { + self.hidden + } } enum AvailableGrammar { @@ -288,6 +292,7 @@ impl LanguageRegistry { config.name.clone(), config.grammar.clone(), config.matcher.clone(), + config.hidden, Arc::new(move || { Ok(LoadedLanguage { config: config.clone(), @@ -436,6 +441,7 @@ impl LanguageRegistry { name: LanguageName, grammar_name: Option>, matcher: LanguageMatcher, + hidden: bool, load: Arc Result + 'static + Send + Sync>, ) { let state = &mut *self.state.write(); @@ -455,6 +461,7 @@ impl LanguageRegistry { grammar: grammar_name, matcher, load, + hidden, loaded: false, }); state.version += 1; @@ -522,6 +529,7 @@ impl LanguageRegistry { name: language.name(), grammar: language.config.grammar.clone(), matcher: language.config.matcher.clone(), + hidden: language.config.hidden, load: Arc::new(|| Err(anyhow!("already loaded"))), loaded: true, }); @@ -590,15 +598,12 @@ impl LanguageRegistry { async move { rx.await? } } - pub fn available_language_for_name( - self: &Arc, - name: &LanguageName, - ) -> Option { + pub fn available_language_for_name(self: &Arc, name: &str) -> Option { let state = self.state.read(); state .available_languages .iter() - .find(|l| &l.name == name) + .find(|l| l.name.0.as_ref() == name) .cloned() } diff --git a/crates/language_extension/src/language_extension.rs b/crates/language_extension/src/language_extension.rs index d8ffc71d7c..59951c87e4 100644 --- a/crates/language_extension/src/language_extension.rs +++ b/crates/language_extension/src/language_extension.rs @@ -34,10 +34,11 @@ impl ExtensionLanguageProxy for LanguageServerRegistryProxy { language: LanguageName, grammar: Option>, matcher: LanguageMatcher, + hidden: bool, load: Arc Result + Send + Sync + 'static>, ) { self.language_registry - .register_language(language, grammar, matcher, load); + .register_language(language, grammar, matcher, hidden, load); } fn remove_languages( diff --git a/crates/language_selector/Cargo.toml b/crates/language_selector/Cargo.toml index b864ffc31f..276e9b0d42 100644 --- a/crates/language_selector/Cargo.toml +++ b/crates/language_selector/Cargo.toml @@ -15,11 +15,14 @@ doctest = false [dependencies] anyhow.workspace = true editor.workspace = true +file_finder.workspace = true +file_icons.workspace = true fuzzy.workspace = true gpui.workspace = true language.workspace = true picker.workspace = true project.workspace = true +settings.workspace = true ui.workspace = true util.workspace = true workspace.workspace = true diff --git a/crates/language_selector/src/language_selector.rs b/crates/language_selector/src/language_selector.rs index 489f6fd141..60da837baa 100644 --- a/crates/language_selector/src/language_selector.rs +++ b/crates/language_selector/src/language_selector.rs @@ -3,15 +3,18 @@ mod active_buffer_language; pub use active_buffer_language::ActiveBufferLanguage; use anyhow::anyhow; use editor::Editor; +use file_finder::file_finder_settings::FileFinderSettings; +use file_icons::FileIcons; use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; use gpui::{ actions, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model, ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView, }; -use language::{Buffer, LanguageRegistry}; +use language::{Buffer, LanguageMatcher, LanguageName, LanguageRegistry}; use picker::{Picker, PickerDelegate}; use project::Project; -use std::sync::Arc; +use settings::Settings; +use std::{ops::Not as _, path::Path, sync::Arc}; use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing}; use util::ResultExt; use workspace::{ModalView, Workspace}; @@ -102,7 +105,13 @@ impl LanguageSelectorDelegate { .language_names() .into_iter() .enumerate() - .map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, name)) + .filter_map(|(candidate_id, name)| { + language_registry + .available_language_for_name(&name)? + .hidden() + .not() + .then(|| StringMatchCandidate::new(candidate_id, name)) + }) .collect::>(); Self { @@ -115,13 +124,64 @@ impl LanguageSelectorDelegate { selected_index: 0, } } + + fn language_data_for_match( + &self, + mat: &StringMatch, + cx: &AppContext, + ) -> (String, Option) { + let mut label = mat.string.clone(); + let buffer_language = self.buffer.read(cx).language(); + let need_icon = FileFinderSettings::get_global(cx).file_icons; + if let Some(buffer_language) = buffer_language { + let buffer_language_name = buffer_language.name(); + if buffer_language_name.0.as_ref() == mat.string.as_str() { + label.push_str(" (current)"); + let icon = need_icon + .then(|| self.language_icon(&buffer_language.config().matcher, cx)) + .flatten(); + return (label, icon); + } + } + + if need_icon { + let language_name = LanguageName::new(mat.string.as_str()); + match self + .language_registry + .available_language_for_name(&language_name.0) + { + Some(available_language) => { + let icon = self.language_icon(available_language.matcher(), cx); + (label, icon) + } + None => (label, None), + } + } else { + (label, None) + } + } + + fn language_icon(&self, matcher: &LanguageMatcher, cx: &AppContext) -> Option { + matcher + .path_suffixes + .iter() + .find_map(|extension| { + if extension.contains('.') { + None + } else { + FileIcons::get_icon(Path::new(&format!("file.{extension}")), cx) + } + }) + .map(Icon::from_path) + .map(|icon| icon.color(Color::Muted)) + } } impl PickerDelegate for LanguageSelectorDelegate { type ListItem = ListItem; fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc { - "Select a language...".into() + "Select a language…".into() } fn match_count(&self) -> usize { @@ -215,17 +275,13 @@ impl PickerDelegate for LanguageSelectorDelegate { cx: &mut ViewContext>, ) -> Option { let mat = &self.matches[ix]; - let buffer_language_name = self.buffer.read(cx).language().map(|l| l.name()); - let mut label = mat.string.clone(); - if buffer_language_name.map(|n| n.0).as_deref() == Some(mat.string.as_str()) { - label.push_str(" (current)"); - } - + let (label, language_icon) = self.language_data_for_match(mat, cx); Some( ListItem::new(ix) .inset(true) .spacing(ListItemSpacing::Sparse) .selected(selected) + .start_slot::(language_icon) .child(HighlightedLabel::new(label, mat.positions.clone())), ) } diff --git a/crates/languages/src/jsdoc/config.toml b/crates/languages/src/jsdoc/config.toml index 444e657a38..0aa0d361bd 100644 --- a/crates/languages/src/jsdoc/config.toml +++ b/crates/languages/src/jsdoc/config.toml @@ -5,3 +5,4 @@ brackets = [ { start = "{", end = "}", close = true, newline = false }, { start = "[", end = "]", close = true, newline = false }, ] +hidden = true diff --git a/crates/languages/src/lib.rs b/crates/languages/src/lib.rs index 776d47a5f7..5ba6f5c034 100644 --- a/crates/languages/src/lib.rs +++ b/crates/languages/src/lib.rs @@ -62,6 +62,7 @@ pub fn init(languages: Arc, node_runtime: NodeRuntime, cx: &mu config.name.clone(), config.grammar.clone(), config.matcher.clone(), + config.hidden, Arc::new(move || { Ok(LoadedLanguage { config: config.clone(), @@ -83,6 +84,7 @@ pub fn init(languages: Arc, node_runtime: NodeRuntime, cx: &mu config.name.clone(), config.grammar.clone(), config.matcher.clone(), + config.hidden, Arc::new(move || { Ok(LoadedLanguage { config: config.clone(), @@ -104,6 +106,7 @@ pub fn init(languages: Arc, node_runtime: NodeRuntime, cx: &mu config.name.clone(), config.grammar.clone(), config.matcher.clone(), + config.hidden, Arc::new(move || { Ok(LoadedLanguage { config: config.clone(), @@ -125,6 +128,7 @@ pub fn init(languages: Arc, node_runtime: NodeRuntime, cx: &mu config.name.clone(), config.grammar.clone(), config.matcher.clone(), + config.hidden, Arc::new(move || { Ok(LoadedLanguage { config: config.clone(), diff --git a/crates/languages/src/regex/config.toml b/crates/languages/src/regex/config.toml index d0938024d6..85f2e370d6 100644 --- a/crates/languages/src/regex/config.toml +++ b/crates/languages/src/regex/config.toml @@ -6,3 +6,4 @@ brackets = [ { start = "{", end = "}", close = true, newline = false }, { start = "[", end = "]", close = true, newline = false }, ] +hidden = true From 4e12f0580a37a0ef615dbd74d40a81d60d3f1494 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 2 Dec 2024 10:20:27 -0800 Subject: [PATCH 234/886] Fix dismissing the IME viewer with escape (#21413) Co-Authored-By: Richard Feldman Closes #21392 Release Notes: - Fixed dismissing the macOS IME menu with escape when no marked text was present --------- Co-authored-by: Richard Feldman --- crates/gpui/src/platform/mac/window.rs | 27 ++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index ce9a4c05bf..f430af7495 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -331,6 +331,7 @@ struct MacWindowState { traffic_light_position: Option>, previous_modifiers_changed_event: Option, keystroke_for_do_command: Option, + do_command_handled: Option, external_files_dragged: bool, // Whether the next left-mouse click is also the focusing click. first_mouse: bool, @@ -609,6 +610,7 @@ impl MacWindow { .and_then(|titlebar| titlebar.traffic_light_position), previous_modifiers_changed_event: None, keystroke_for_do_command: None, + do_command_handled: None, external_files_dragged: false, first_mouse: false, fullscreen_restore_bounds: Bounds::default(), @@ -1251,14 +1253,22 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent: // otherwise we only send to the input handler if we don't have a matching binding. // The input handler may call `do_command_by_selector` if it doesn't know how to handle // a key. If it does so, it will return YES so we won't send the key twice. - if is_composing || event.keystroke.key.is_empty() { - window_state.as_ref().lock().keystroke_for_do_command = Some(event.keystroke.clone()); + if is_composing || event.keystroke.key_char.is_none() { + { + let mut lock = window_state.as_ref().lock(); + lock.keystroke_for_do_command = Some(event.keystroke.clone()); + lock.do_command_handled.take(); + drop(lock); + } + let handled: BOOL = unsafe { let input_context: id = msg_send![this, inputContext]; msg_send![input_context, handleEvent: native_event] }; window_state.as_ref().lock().keystroke_for_do_command.take(); - if handled == YES { + if let Some(handled) = window_state.as_ref().lock().do_command_handled.take() { + return handled as BOOL; + } else if handled == YES { return YES; } @@ -1377,6 +1387,14 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) { }; match &event { + PlatformInput::MouseDown(_) => { + drop(lock); + unsafe { + let input_context: id = msg_send![this, inputContext]; + msg_send![input_context, handleEvent: native_event] + } + lock = window_state.as_ref().lock(); + } PlatformInput::MouseMove( event @ MouseMoveEvent { pressed_button: Some(_), @@ -1790,10 +1808,11 @@ extern "C" fn do_command_by_selector(this: &Object, _: Sel, _: Sel) { drop(lock); if let Some((keystroke, mut callback)) = keystroke.zip(event_callback.as_mut()) { - (callback)(PlatformInput::KeyDown(KeyDownEvent { + let handled = (callback)(PlatformInput::KeyDown(KeyDownEvent { keystroke, is_held: false, })); + state.as_ref().lock().do_command_handled = Some(!handled.propagate); } state.as_ref().lock().event_callback = event_callback; From 7c408247835085c6c30d6ef69ef45c1a9e9c6c1f Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 2 Dec 2024 10:46:14 -0800 Subject: [PATCH 235/886] Fix macOS IME overlay positioning (#21416) Release Notes: - Improved positioning of macOS IME overlay --------- Co-authored-by: Richard Feldman --- crates/editor/src/editor.rs | 3 ++- crates/gpui/src/platform/mac/window.rs | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d5d96436e8..51a90a9206 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -14611,7 +14611,8 @@ impl ViewInputHandler for Editor { let start = OffsetUtf16(range_utf16.start).to_display_point(&snapshot); let x = snapshot.x_for_display_point(start, &text_layout_details) - scroll_left - + self.gutter_dimensions.width; + + self.gutter_dimensions.width + + self.gutter_dimensions.margin; let y = line_height * (start.row().as_f32() - scroll_position.y); Some(Bounds { diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index f430af7495..12a332e9bc 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -1701,7 +1701,10 @@ extern "C" fn first_rect_for_character_range( let lock = state.lock(); let mut frame = NSWindow::frame(lock.native_window); let content_layout_rect: CGRect = msg_send![lock.native_window, contentLayoutRect]; - frame.origin.y -= frame.size.height - content_layout_rect.size.height; + let style_mask: NSWindowStyleMask = msg_send![lock.native_window, styleMask]; + if !style_mask.contains(NSWindowStyleMask::NSFullSizeContentViewWindowMask) { + frame.origin.y -= frame.size.height - content_layout_rect.size.height; + } frame }; with_input_handler(this, |input_handler| { From dbe41823d9f5e720d35b7f40573296bb8cfe455d Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 2 Dec 2024 20:46:28 +0200 Subject: [PATCH 236/886] Use proper terminal item for splitting context (#21415) Closes https://github.com/zed-industries/zed/issues/21411 Release Notes: - N/A --- crates/terminal_view/src/terminal_panel.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 532d5d9040..b3804354c4 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -131,8 +131,8 @@ impl TerminalPanel { terminal_pane.update(cx, |pane, cx| { pane.set_render_tab_bar_buttons(cx, move |pane, cx| { let split_context = pane - .items() - .find_map(|item| item.downcast::()) + .active_item() + .and_then(|item| item.downcast::()) .map(|terminal_view| terminal_view.read(cx).focus_handle.clone()); if !pane.has_focus(cx) && !pane.context_menu_focused(cx) { return (None, None); From 95a047c11b8bddf8edbfc4e932474925c3d9e010 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 2 Dec 2024 19:53:51 +0100 Subject: [PATCH 237/886] tasks/rust: Add support for running examples as binary targets (#21412) Closes #21044 Release Notes: - Added support for running Rust examples as tasks. --- crates/languages/src/rust.rs | 94 ++++++++++++++++++++++++++++++------ 1 file changed, 80 insertions(+), 14 deletions(-) diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index 25cddae5a6..274d96f5fa 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -10,6 +10,7 @@ pub use language::*; use lsp::{LanguageServerBinary, LanguageServerName}; use regex::Regex; use smol::fs::{self}; +use std::fmt::Display; use std::{ any::Any, borrow::Cow, @@ -444,6 +445,10 @@ const RUST_PACKAGE_TASK_VARIABLE: VariableName = const RUST_BIN_NAME_TASK_VARIABLE: VariableName = VariableName::Custom(Cow::Borrowed("RUST_BIN_NAME")); +/// The bin kind (bin/example) corresponding to the current file in Cargo.toml +const RUST_BIN_KIND_TASK_VARIABLE: VariableName = + VariableName::Custom(Cow::Borrowed("RUST_BIN_KIND")); + const RUST_MAIN_FUNCTION_TASK_VARIABLE: VariableName = VariableName::Custom(Cow::Borrowed("_rust_main_function_end")); @@ -469,12 +474,16 @@ impl ContextProvider for RustContextProvider { .is_some(); if is_main_function { - if let Some((package_name, bin_name)) = local_abs_path.and_then(|path| { + if let Some(target) = local_abs_path.and_then(|path| { package_name_and_bin_name_from_abs_path(path, project_env.as_ref()) }) { return Task::ready(Ok(TaskVariables::from_iter([ - (RUST_PACKAGE_TASK_VARIABLE.clone(), package_name), - (RUST_BIN_NAME_TASK_VARIABLE.clone(), bin_name), + (RUST_PACKAGE_TASK_VARIABLE.clone(), target.package_name), + (RUST_BIN_NAME_TASK_VARIABLE.clone(), target.target_name), + ( + RUST_BIN_KIND_TASK_VARIABLE.clone(), + target.target_kind.to_string(), + ), ]))); } } @@ -568,8 +577,9 @@ impl ContextProvider for RustContextProvider { }, TaskTemplate { label: format!( - "cargo run -p {} --bin {}", + "cargo run -p {} --{} {}", RUST_PACKAGE_TASK_VARIABLE.template_value(), + RUST_BIN_KIND_TASK_VARIABLE.template_value(), RUST_BIN_NAME_TASK_VARIABLE.template_value(), ), command: "cargo".into(), @@ -577,7 +587,7 @@ impl ContextProvider for RustContextProvider { "run".into(), "-p".into(), RUST_PACKAGE_TASK_VARIABLE.template_value(), - "--bin".into(), + format!("--{}", RUST_BIN_KIND_TASK_VARIABLE.template_value()), RUST_BIN_NAME_TASK_VARIABLE.template_value(), ], cwd: Some("$ZED_DIRNAME".to_owned()), @@ -635,10 +645,42 @@ struct CargoTarget { src_path: String, } +#[derive(Debug, PartialEq)] +enum TargetKind { + Bin, + Example, +} + +impl Display for TargetKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TargetKind::Bin => write!(f, "bin"), + TargetKind::Example => write!(f, "example"), + } + } +} + +impl TryFrom<&str> for TargetKind { + type Error = (); + fn try_from(value: &str) -> Result { + match value { + "bin" => Ok(Self::Bin), + "example" => Ok(Self::Example), + _ => Err(()), + } + } +} +/// Which package and binary target are we in? +struct TargetInfo { + package_name: String, + target_name: String, + target_kind: TargetKind, +} + fn package_name_and_bin_name_from_abs_path( abs_path: &Path, project_env: Option<&HashMap>, -) -> Option<(String, String)> { +) -> Option { let mut command = util::command::new_std_command("cargo"); if let Some(envs) = project_env { command.envs(envs); @@ -656,10 +698,14 @@ fn package_name_and_bin_name_from_abs_path( let metadata: CargoMetadata = serde_json::from_slice(&output).log_err()?; retrieve_package_id_and_bin_name_from_metadata(metadata, abs_path).and_then( - |(package_id, bin_name)| { + |(package_id, bin_name, target_kind)| { let package_name = package_name_from_pkgid(&package_id); - package_name.map(|package_name| (package_name.to_owned(), bin_name)) + package_name.map(|package_name| TargetInfo { + package_name: package_name.to_owned(), + target_name: bin_name, + target_kind, + }) }, ) } @@ -667,13 +713,19 @@ fn package_name_and_bin_name_from_abs_path( fn retrieve_package_id_and_bin_name_from_metadata( metadata: CargoMetadata, abs_path: &Path, -) -> Option<(String, String)> { +) -> Option<(String, String, TargetKind)> { for package in metadata.packages { for target in package.targets { - let is_bin = target.kind.iter().any(|kind| kind == "bin"); + let Some(bin_kind) = target + .kind + .iter() + .find_map(|kind| TargetKind::try_from(kind.as_ref()).ok()) + else { + continue; + }; let target_path = PathBuf::from(target.src_path); - if target_path == abs_path && is_bin { - return Some((package.id, target.name)); + if target_path == abs_path { + return Some((package.id, target.name, bin_kind)); } } } @@ -1066,7 +1118,11 @@ mod tests { ( r#"{"packages":[{"id":"path+file:///path/to/zed/crates/zed#0.131.0","targets":[{"name":"zed","kind":["bin"],"src_path":"/path/to/zed/src/main.rs"}]}]}"#, "/path/to/zed/src/main.rs", - Some(("path+file:///path/to/zed/crates/zed#0.131.0", "zed")), + Some(( + "path+file:///path/to/zed/crates/zed#0.131.0", + "zed", + TargetKind::Bin, + )), ), ( r#"{"packages":[{"id":"path+file:///path/to/custom-package#my-custom-package@0.1.0","targets":[{"name":"my-custom-bin","kind":["bin"],"src_path":"/path/to/custom-package/src/main.rs"}]}]}"#, @@ -1074,6 +1130,16 @@ mod tests { Some(( "path+file:///path/to/custom-package#my-custom-package@0.1.0", "my-custom-bin", + TargetKind::Bin, + )), + ), + ( + r#"{"packages":[{"id":"path+file:///path/to/custom-package#my-custom-package@0.1.0","targets":[{"name":"my-custom-bin","kind":["example"],"src_path":"/path/to/custom-package/src/main.rs"}]}]}"#, + "/path/to/custom-package/src/main.rs", + Some(( + "path+file:///path/to/custom-package#my-custom-package@0.1.0", + "my-custom-bin", + TargetKind::Example, )), ), ( @@ -1088,7 +1154,7 @@ mod tests { assert_eq!( retrieve_package_id_and_bin_name_from_metadata(metadata, absolute_path), - expected.map(|(pkgid, bin)| (pkgid.to_owned(), bin.to_owned())) + expected.map(|(pkgid, name, kind)| (pkgid.to_owned(), name.to_owned(), kind)) ); } } From f32ffcf5bb005d18bef320226a69edad062e4fec Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 2 Dec 2024 19:56:52 +0100 Subject: [PATCH 238/886] workspace: Sanitize pinned tab count before usage (#21417) Fixes all sorts of panics around usage of incorrect pinned tab count that has been fixed in app itself, yet persists in user db. Closes #ISSUE Release Notes: - N/A --- crates/workspace/src/pane.rs | 2 +- crates/workspace/src/persistence/model.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 83cc911a91..fe6b08fd4a 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1890,7 +1890,7 @@ impl Pane { fn unpin_tab_at(&mut self, ix: usize, cx: &mut ViewContext<'_, Self>) { maybe!({ let pane = cx.view().clone(); - self.pinned_tab_count = self.pinned_tab_count.checked_sub(1).unwrap(); + self.pinned_tab_count = self.pinned_tab_count.checked_sub(1)?; let destination_index = self.pinned_tab_count; let id = self.item_for_index(ix)?.item_id(); diff --git a/crates/workspace/src/persistence/model.rs b/crates/workspace/src/persistence/model.rs index a2510b8bec..7a368ee441 100644 --- a/crates/workspace/src/persistence/model.rs +++ b/crates/workspace/src/persistence/model.rs @@ -473,7 +473,7 @@ impl SerializedPane { })?; } pane.update(cx, |pane, _| { - pane.set_pinned_count(self.pinned_count); + pane.set_pinned_count(self.pinned_count.min(items.len())); })?; anyhow::Ok(items) From b88daae67b4c6af1f80b7c7c091f50d313410d84 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 2 Dec 2024 15:01:18 -0500 Subject: [PATCH 239/886] assistant2: Add support for using tools provided by context servers (#21418) This PR adds support to Assistant 2 for using tools provided by context servers. As part of this I introduced a new `ThreadStore`. Release Notes: - N/A --------- Co-authored-by: Cole --- Cargo.lock | 3 + crates/assistant2/Cargo.toml | 3 + crates/assistant2/src/assistant.rs | 1 + crates/assistant2/src/assistant_panel.rs | 20 +++- crates/assistant2/src/thread_store.rs | 114 +++++++++++++++++++++++ 5 files changed, 139 insertions(+), 2 deletions(-) create mode 100644 crates/assistant2/src/thread_store.rs diff --git a/Cargo.lock b/Cargo.lock index e3bdc89f5f..0594b5c9b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -458,12 +458,15 @@ dependencies = [ "assistant_tool", "collections", "command_palette_hooks", + "context_server", "editor", "feature_flags", "futures 0.3.31", "gpui", "language_model", "language_model_selector", + "log", + "project", "proto", "serde", "serde_json", diff --git a/crates/assistant2/Cargo.toml b/crates/assistant2/Cargo.toml index ca563b05c8..ff49801c46 100644 --- a/crates/assistant2/Cargo.toml +++ b/crates/assistant2/Cargo.toml @@ -17,12 +17,15 @@ anyhow.workspace = true assistant_tool.workspace = true collections.workspace = true command_palette_hooks.workspace = true +context_server.workspace = true editor.workspace = true feature_flags.workspace = true futures.workspace = true gpui.workspace = true language_model.workspace = true language_model_selector.workspace = true +log.workspace = true +project.workspace = true proto.workspace = true settings.workspace = true serde.workspace = true diff --git a/crates/assistant2/src/assistant.rs b/crates/assistant2/src/assistant.rs index 1b33e27928..8ef4a1d9dc 100644 --- a/crates/assistant2/src/assistant.rs +++ b/crates/assistant2/src/assistant.rs @@ -1,6 +1,7 @@ mod assistant_panel; mod message_editor; mod thread; +mod thread_store; use command_palette_hooks::CommandPaletteFilter; use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt}; diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index bf457d6c71..7d8405dc78 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -14,6 +14,7 @@ use workspace::Workspace; use crate::message_editor::MessageEditor; use crate::thread::{Message, Thread, ThreadEvent}; +use crate::thread_store::ThreadStore; use crate::{NewThread, ToggleFocus, ToggleModelSelector}; pub fn init(cx: &mut AppContext) { @@ -29,6 +30,8 @@ pub fn init(cx: &mut AppContext) { pub struct AssistantPanel { workspace: WeakView, + #[allow(unused)] + thread_store: Model, thread: Model, message_editor: View, tools: Arc, @@ -42,13 +45,25 @@ impl AssistantPanel { ) -> Task>> { cx.spawn(|mut cx| async move { let tools = Arc::new(ToolWorkingSet::default()); + let thread_store = workspace + .update(&mut cx, |workspace, cx| { + let project = workspace.project().clone(); + ThreadStore::new(project, tools.clone(), cx) + })? + .await?; + workspace.update(&mut cx, |workspace, cx| { - cx.new_view(|cx| Self::new(workspace, tools, cx)) + cx.new_view(|cx| Self::new(workspace, thread_store, tools, cx)) }) }) } - fn new(workspace: &Workspace, tools: Arc, cx: &mut ViewContext) -> Self { + fn new( + workspace: &Workspace, + thread_store: Model, + tools: Arc, + cx: &mut ViewContext, + ) -> Self { let thread = cx.new_model(|cx| Thread::new(tools.clone(), cx)); let subscriptions = vec![ cx.observe(&thread, |_, _, cx| cx.notify()), @@ -57,6 +72,7 @@ impl AssistantPanel { Self { workspace: workspace.weak_handle(), + thread_store, thread: thread.clone(), message_editor: cx.new_view(|cx| MessageEditor::new(thread, cx)), tools, diff --git a/crates/assistant2/src/thread_store.rs b/crates/assistant2/src/thread_store.rs new file mode 100644 index 0000000000..99f90eace8 --- /dev/null +++ b/crates/assistant2/src/thread_store.rs @@ -0,0 +1,114 @@ +use std::sync::Arc; + +use anyhow::Result; +use assistant_tool::{ToolId, ToolWorkingSet}; +use collections::HashMap; +use context_server::manager::ContextServerManager; +use context_server::{ContextServerFactoryRegistry, ContextServerTool}; +use gpui::{prelude::*, AppContext, Model, ModelContext, Task}; +use project::Project; +use util::ResultExt as _; + +pub struct ThreadStore { + #[allow(unused)] + project: Model, + tools: Arc, + context_server_manager: Model, + context_server_tool_ids: HashMap, Vec>, +} + +impl ThreadStore { + pub fn new( + project: Model, + tools: Arc, + cx: &mut AppContext, + ) -> Task>> { + cx.spawn(|mut cx| async move { + let this = cx.new_model(|cx: &mut ModelContext| { + let context_server_factory_registry = + ContextServerFactoryRegistry::default_global(cx); + let context_server_manager = cx.new_model(|cx| { + ContextServerManager::new(context_server_factory_registry, project.clone(), cx) + }); + + let this = Self { + project, + tools, + context_server_manager, + context_server_tool_ids: HashMap::default(), + }; + this.register_context_server_handlers(cx); + + this + })?; + + Ok(this) + }) + } + + fn register_context_server_handlers(&self, cx: &mut ModelContext) { + cx.subscribe( + &self.context_server_manager.clone(), + Self::handle_context_server_event, + ) + .detach(); + } + + fn handle_context_server_event( + &mut self, + context_server_manager: Model, + event: &context_server::manager::Event, + cx: &mut ModelContext, + ) { + let tool_working_set = self.tools.clone(); + match event { + context_server::manager::Event::ServerStarted { server_id } => { + if let Some(server) = context_server_manager.read(cx).get_server(server_id) { + let context_server_manager = context_server_manager.clone(); + cx.spawn({ + let server = server.clone(); + let server_id = server_id.clone(); + |this, mut cx| async move { + let Some(protocol) = server.client() else { + return; + }; + + if protocol.capable(context_server::protocol::ServerCapability::Tools) { + if let Some(tools) = protocol.list_tools().await.log_err() { + let tool_ids = tools + .tools + .into_iter() + .map(|tool| { + log::info!( + "registering context server tool: {:?}", + tool.name + ); + tool_working_set.insert(Arc::new( + ContextServerTool::new( + context_server_manager.clone(), + server.id(), + tool, + ), + )) + }) + .collect::>(); + + this.update(&mut cx, |this, _cx| { + this.context_server_tool_ids.insert(server_id, tool_ids); + }) + .log_err(); + } + } + } + }) + .detach(); + } + } + context_server::manager::Event::ServerStopped { server_id } => { + if let Some(tool_ids) = self.context_server_tool_ids.remove(server_id) { + tool_working_set.remove(&tool_ids); + } + } + } + } +} From 59dc6cf523678f7a2ce0883fd2258c4a1af838c1 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 2 Dec 2024 21:03:31 +0100 Subject: [PATCH 240/886] toolchains: Run listing tasks on background thread (#21414) Potentially fixes #21404 This is a speculative fix, as while I was trying to repro this issue I've noticed that introducing artificial delays in ToolchainLister::list could impact apps responsiveness. These delays were essentially there to stimulate PET taking a while to find venvs. Release Notes: - Improved app responsiveness in environments with multiple Python virtual environments --- crates/language/src/toolchain.rs | 2 +- crates/languages/src/python.rs | 2 +- crates/project/src/toolchain_store.rs | 14 ++++++++------ 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/crates/language/src/toolchain.rs b/crates/language/src/toolchain.rs index fe8936db08..13703d81a7 100644 --- a/crates/language/src/toolchain.rs +++ b/crates/language/src/toolchain.rs @@ -24,7 +24,7 @@ pub struct Toolchain { pub as_json: serde_json::Value, } -#[async_trait(?Send)] +#[async_trait] pub trait ToolchainLister: Send + Sync { async fn list( &self, diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 8736a12942..ec7ddde61d 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -536,7 +536,7 @@ fn env_priority(kind: Option) -> usize { } } -#[async_trait(?Send)] +#[async_trait] impl ToolchainLister for PythonToolchainProvider { async fn list( &self, diff --git a/crates/project/src/toolchain_store.rs b/crates/project/src/toolchain_store.rs index 4d4c32d745..71228d96a4 100644 --- a/crates/project/src/toolchain_store.rs +++ b/crates/project/src/toolchain_store.rs @@ -311,12 +311,14 @@ impl LocalToolchainStore { }) .ok()? .await; - let language = registry.language_for_name(&language_name.0).await.ok()?; - let toolchains = language - .toolchain_lister()? - .list(root.to_path_buf(), project_env) - .await; - Some(toolchains) + + cx.background_executor() + .spawn(async move { + let language = registry.language_for_name(&language_name.0).await.ok()?; + let toolchains = language.toolchain_lister()?; + Some(toolchains.list(root.to_path_buf(), project_env).await) + }) + .await }) } pub(crate) fn active_toolchain( From 72afe684b8248a8662bb731694e79d014cca2169 Mon Sep 17 00:00:00 2001 From: yoleuh Date: Mon, 2 Dec 2024 16:48:20 -0500 Subject: [PATCH 241/886] assistant: Use a smaller icon for the "New Chat" button (#21425) Assistant new chat icon is slightly larger than editor pane new icon. Changes: Adds `IconSize::Small` to assistant default size new chat icon, not really noticeable, but matches the new icon in editor pane, and the assistant dropdown menu that have icon size small. |old|new| |---|---| |![image](https://github.com/user-attachments/assets/cbef5054-a465-4957-9409-b4a73e703363)|![image](https://github.com/user-attachments/assets/baee66ea-76d6-43b4-a4b9-ead34991ff85)| Release Notes: - N/A --- crates/assistant/src/assistant_panel.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 79e026cb51..109c9c3237 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -450,6 +450,7 @@ impl AssistantPanel { .gap(DynamicSpacing::Base02.rems(cx)) .child( IconButton::new("new-chat", IconName::Plus) + .icon_size(IconSize::Small) .on_click( cx.listener(|_, _, cx| { cx.dispatch_action(NewContext.boxed_clone()) From f3140f54d8458980417f0208849ce9254f0f54e4 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 2 Dec 2024 16:54:46 -0500 Subject: [PATCH 242/886] assistant2: Wire up error messages (#21426) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR wires up the error messages for Assistant 2 so that they are shown to the user: Screenshot 2024-12-02 at 4 28 02 PM Screenshot 2024-12-02 at 4 29 09 PM Screenshot 2024-12-02 at 4 32 49 PM @danilo-leal I kept the existing UX from Assistant 1, as I didn't see any errors in the design prototype, but we can revisit if another approach would work better. Release Notes: - N/A --- Cargo.lock | 2 + crates/assistant2/Cargo.toml | 2 + crates/assistant2/src/assistant_panel.rs | 160 +++++++++++++++++++- crates/assistant2/src/thread.rs | 56 ++++--- crates/language_model/src/language_model.rs | 2 +- 5 files changed, 194 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0594b5c9b5..7504b8491b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -456,6 +456,7 @@ version = "0.1.0" dependencies = [ "anyhow", "assistant_tool", + "client", "collections", "command_palette_hooks", "context_server", @@ -465,6 +466,7 @@ dependencies = [ "gpui", "language_model", "language_model_selector", + "language_models", "log", "project", "proto", diff --git a/crates/assistant2/Cargo.toml b/crates/assistant2/Cargo.toml index ff49801c46..20e8dfbc9a 100644 --- a/crates/assistant2/Cargo.toml +++ b/crates/assistant2/Cargo.toml @@ -15,6 +15,7 @@ doctest = false [dependencies] anyhow.workspace = true assistant_tool.workspace = true +client.workspace = true collections.workspace = true command_palette_hooks.workspace = true context_server.workspace = true @@ -24,6 +25,7 @@ futures.workspace = true gpui.workspace = true language_model.workspace = true language_model_selector.workspace = true +language_models.workspace = true log.workspace = true project.workspace = true proto.workspace = true diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index 7d8405dc78..4e6b6ef227 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -2,9 +2,11 @@ use std::sync::Arc; use anyhow::Result; use assistant_tool::ToolWorkingSet; +use client::zed_urls; use gpui::{ - prelude::*, px, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, - FocusableView, Model, Pixels, Subscription, Task, View, ViewContext, WeakView, WindowContext, + prelude::*, px, Action, AnyElement, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, + FocusableView, FontWeight, Model, Pixels, Subscription, Task, View, ViewContext, WeakView, + WindowContext, }; use language_model::{LanguageModelRegistry, Role}; use language_model_selector::LanguageModelSelector; @@ -13,7 +15,7 @@ use workspace::dock::{DockPosition, Panel, PanelEvent}; use workspace::Workspace; use crate::message_editor::MessageEditor; -use crate::thread::{Message, Thread, ThreadEvent}; +use crate::thread::{Message, Thread, ThreadError, ThreadEvent}; use crate::thread_store::ThreadStore; use crate::{NewThread, ToggleFocus, ToggleModelSelector}; @@ -35,6 +37,7 @@ pub struct AssistantPanel { thread: Model, message_editor: View, tools: Arc, + last_error: Option, _subscriptions: Vec, } @@ -76,6 +79,7 @@ impl AssistantPanel { thread: thread.clone(), message_editor: cx.new_view(|cx| MessageEditor::new(thread, cx)), tools, + last_error: None, _subscriptions: subscriptions, } } @@ -102,6 +106,9 @@ impl AssistantPanel { cx: &mut ViewContext, ) { match event { + ThreadEvent::ShowError(error) => { + self.last_error = Some(error.clone()); + } ThreadEvent::StreamedCompletion => {} ThreadEvent::UsePendingTools => { let pending_tool_uses = self @@ -320,6 +327,152 @@ impl AssistantPanel { ) .child(v_flex().p_1p5().child(Label::new(message.text.clone()))) } + + fn render_last_error(&self, cx: &mut ViewContext) -> Option { + let last_error = self.last_error.as_ref()?; + + Some( + div() + .absolute() + .right_3() + .bottom_12() + .max_w_96() + .py_2() + .px_3() + .elevation_2(cx) + .occlude() + .child(match last_error { + ThreadError::PaymentRequired => self.render_payment_required_error(cx), + ThreadError::MaxMonthlySpendReached => { + self.render_max_monthly_spend_reached_error(cx) + } + ThreadError::Message(error_message) => { + self.render_error_message(error_message, cx) + } + }) + .into_any(), + ) + } + + fn render_payment_required_error(&self, cx: &mut ViewContext) -> AnyElement { + const ERROR_MESSAGE: &str = "Free tier exceeded. Subscribe and add payment to continue using Zed LLMs. You'll be billed at cost for tokens used."; + + v_flex() + .gap_0p5() + .child( + h_flex() + .gap_1p5() + .items_center() + .child(Icon::new(IconName::XCircle).color(Color::Error)) + .child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)), + ) + .child( + div() + .id("error-message") + .max_h_24() + .overflow_y_scroll() + .child(Label::new(ERROR_MESSAGE)), + ) + .child( + h_flex() + .justify_end() + .mt_1() + .child(Button::new("subscribe", "Subscribe").on_click(cx.listener( + |this, _, cx| { + this.last_error = None; + cx.open_url(&zed_urls::account_url(cx)); + cx.notify(); + }, + ))) + .child(Button::new("dismiss", "Dismiss").on_click(cx.listener( + |this, _, cx| { + this.last_error = None; + cx.notify(); + }, + ))), + ) + .into_any() + } + + fn render_max_monthly_spend_reached_error(&self, cx: &mut ViewContext) -> AnyElement { + const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs."; + + v_flex() + .gap_0p5() + .child( + h_flex() + .gap_1p5() + .items_center() + .child(Icon::new(IconName::XCircle).color(Color::Error)) + .child(Label::new("Max Monthly Spend Reached").weight(FontWeight::MEDIUM)), + ) + .child( + div() + .id("error-message") + .max_h_24() + .overflow_y_scroll() + .child(Label::new(ERROR_MESSAGE)), + ) + .child( + h_flex() + .justify_end() + .mt_1() + .child( + Button::new("subscribe", "Update Monthly Spend Limit").on_click( + cx.listener(|this, _, cx| { + this.last_error = None; + cx.open_url(&zed_urls::account_url(cx)); + cx.notify(); + }), + ), + ) + .child(Button::new("dismiss", "Dismiss").on_click(cx.listener( + |this, _, cx| { + this.last_error = None; + cx.notify(); + }, + ))), + ) + .into_any() + } + + fn render_error_message( + &self, + error_message: &SharedString, + cx: &mut ViewContext, + ) -> AnyElement { + v_flex() + .gap_0p5() + .child( + h_flex() + .gap_1p5() + .items_center() + .child(Icon::new(IconName::XCircle).color(Color::Error)) + .child( + Label::new("Error interacting with language model") + .weight(FontWeight::MEDIUM), + ), + ) + .child( + div() + .id("error-message") + .max_h_32() + .overflow_y_scroll() + .child(Label::new(error_message.clone())), + ) + .child( + h_flex() + .justify_end() + .mt_1() + .child(Button::new("dismiss", "Dismiss").on_click(cx.listener( + |this, _, cx| { + this.last_error = None; + cx.notify(); + }, + ))), + ) + .into_any() + } } impl Render for AssistantPanel { @@ -354,5 +507,6 @@ impl Render for AssistantPanel { .border_color(cx.theme().colors().border_variant) .child(self.message_editor.clone()), ) + .children(self.render_last_error(cx)) } } diff --git a/crates/assistant2/src/thread.rs b/crates/assistant2/src/thread.rs index 0d2aab6905..a5ab415a4d 100644 --- a/crates/assistant2/src/thread.rs +++ b/crates/assistant2/src/thread.rs @@ -5,12 +5,13 @@ use assistant_tool::ToolWorkingSet; use collections::HashMap; use futures::future::Shared; use futures::{FutureExt as _, StreamExt as _}; -use gpui::{AppContext, EventEmitter, ModelContext, Task}; +use gpui::{AppContext, EventEmitter, ModelContext, SharedString, Task}; use language_model::{ LanguageModel, LanguageModelCompletionEvent, LanguageModelRequest, LanguageModelRequestMessage, LanguageModelToolResult, LanguageModelToolUse, LanguageModelToolUseId, MessageContent, Role, StopReason, }; +use language_models::provider::cloud::{MaxMonthlySpendReachedError, PaymentRequiredError}; use serde::{Deserialize, Serialize}; use util::post_inc; @@ -210,29 +211,28 @@ impl Thread { let result = stream_completion.await; thread - .update(&mut cx, |_thread, cx| { - let error_message = if let Some(error) = result.as_ref().err() { - let error_message = error - .chain() - .map(|err| err.to_string()) - .collect::>() - .join("\n"); - Some(error_message) - } else { - None - }; - - if let Some(error_message) = error_message { - eprintln!("Completion failed: {error_message:?}"); - } - - if let Ok(stop_reason) = result { - match stop_reason { - StopReason::ToolUse => { - cx.emit(ThreadEvent::UsePendingTools); - } - StopReason::EndTurn => {} - StopReason::MaxTokens => {} + .update(&mut cx, |_thread, cx| match result.as_ref() { + Ok(stop_reason) => match stop_reason { + StopReason::ToolUse => { + cx.emit(ThreadEvent::UsePendingTools); + } + StopReason::EndTurn => {} + StopReason::MaxTokens => {} + }, + Err(error) => { + if error.is::() { + cx.emit(ThreadEvent::ShowError(ThreadError::PaymentRequired)); + } else if error.is::() { + cx.emit(ThreadEvent::ShowError(ThreadError::MaxMonthlySpendReached)); + } else { + let error_message = error + .chain() + .map(|err| err.to_string()) + .collect::>() + .join("\n"); + cx.emit(ThreadEvent::ShowError(ThreadError::Message( + SharedString::from(error_message.clone()), + ))); } } }) @@ -305,8 +305,16 @@ impl Thread { } } +#[derive(Debug, Clone)] +pub enum ThreadError { + PaymentRequired, + MaxMonthlySpendReached, + Message(SharedString), +} + #[derive(Debug, Clone)] pub enum ThreadEvent { + ShowError(ThreadError), StreamedCompletion, UsePendingTools, ToolFinished { diff --git a/crates/language_model/src/language_model.rs b/crates/language_model/src/language_model.rs index 3c5a00bd85..83f0b50321 100644 --- a/crates/language_model/src/language_model.rs +++ b/crates/language_model/src/language_model.rs @@ -55,7 +55,7 @@ pub enum LanguageModelCompletionEvent { StartMessage { message_id: String }, } -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum StopReason { EndTurn, From 7c994cd4a5434fea92998f676462a9e6d6c46d2d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 2 Dec 2024 15:00:04 -0800 Subject: [PATCH 243/886] Add AutoIndent action and '=' vim operator (#21427) Release Notes: - vim: Added the `=` operator, for auto-indent Co-authored-by: Conrad --- assets/keymaps/vim.json | 10 +- crates/editor/src/actions.rs | 1 + crates/editor/src/editor.rs | 19 ++ crates/editor/src/editor_tests.rs | 100 +++++- crates/editor/src/element.rs | 1 + crates/editor/src/inlay_hint_cache.rs | 11 +- .../src/test/editor_lsp_test_context.rs | 82 ++--- crates/language/src/buffer.rs | 56 +++- crates/multi_buffer/src/multi_buffer.rs | 292 +++++++++++------- crates/vim/src/indent.rs | 85 ++++- crates/vim/src/normal.rs | 6 + crates/vim/src/state.rs | 3 + crates/vim/src/vim.rs | 1 + 13 files changed, 481 insertions(+), 186 deletions(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index a69e97401d..b2ef7f2c18 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -55,10 +55,10 @@ "n": "vim::MoveToNextMatch", "shift-n": "vim::MoveToPrevMatch", "%": "vim::Matching", - "] }": ["vim::UnmatchedForward", { "char": "}" } ], - "[ {": ["vim::UnmatchedBackward", { "char": "{" } ], - "] )": ["vim::UnmatchedForward", { "char": ")" } ], - "[ (": ["vim::UnmatchedBackward", { "char": "(" } ], + "] }": ["vim::UnmatchedForward", { "char": "}" }], + "[ {": ["vim::UnmatchedBackward", { "char": "{" }], + "] )": ["vim::UnmatchedForward", { "char": ")" }], + "[ (": ["vim::UnmatchedBackward", { "char": "(" }], "f": ["vim::PushOperator", { "FindForward": { "before": false } }], "t": ["vim::PushOperator", { "FindForward": { "before": true } }], "shift-f": ["vim::PushOperator", { "FindBackward": { "after": false } }], @@ -209,6 +209,7 @@ "shift-s": "vim::SubstituteLine", ">": ["vim::PushOperator", "Indent"], "<": ["vim::PushOperator", "Outdent"], + "=": ["vim::PushOperator", "AutoIndent"], "g u": ["vim::PushOperator", "Lowercase"], "g shift-u": ["vim::PushOperator", "Uppercase"], "g ~": ["vim::PushOperator", "OppositeCase"], @@ -275,6 +276,7 @@ "ctrl-[": ["vim::SwitchMode", "Normal"], ">": "vim::Indent", "<": "vim::Outdent", + "=": "vim::AutoIndent", "i": ["vim::PushOperator", { "Object": { "around": false } }], "a": ["vim::PushOperator", { "Object": { "around": true } }], "g c": "vim::ToggleComments", diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 5b11b18bc2..a67dd55055 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -303,6 +303,7 @@ gpui::actions!( OpenPermalinkToLine, OpenUrl, Outdent, + AutoIndent, PageDown, PageUp, Paste, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 51a90a9206..82b27d6f22 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -6297,6 +6297,25 @@ impl Editor { }); } + pub fn autoindent(&mut self, _: &AutoIndent, cx: &mut ViewContext) { + if self.read_only(cx) { + return; + } + let selections = self + .selections + .all::(cx) + .into_iter() + .map(|s| s.range()); + + self.transact(cx, |this, cx| { + this.buffer.update(cx, |buffer, cx| { + buffer.autoindent_ranges(selections, cx); + }); + let selections = this.selections.all::(cx); + this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections)); + }); + } + pub fn delete_line(&mut self, _: &DeleteLine, cx: &mut ViewContext) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let selections = self.selections.all::(cx); diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index b49b3fa33b..5134b512ff 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -34,6 +34,7 @@ use serde_json::{self, json}; use std::sync::atomic::AtomicUsize; use std::sync::atomic::{self, AtomicBool}; use std::{cell::RefCell, future::Future, rc::Rc, time::Instant}; +use test::editor_lsp_test_context::rust_lang; use unindent::Unindent; use util::{ assert_set_eq, @@ -5458,7 +5459,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { } #[gpui::test] -async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) { +async fn test_autoindent(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); let language = Arc::new( @@ -5520,6 +5521,89 @@ async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) { }); } +#[gpui::test] +async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + { + let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await; + cx.set_state(indoc! {" + impl A { + + fn b() {} + + «fn c() { + + }ˇ» + } + "}); + + cx.update_editor(|editor, cx| { + editor.autoindent(&Default::default(), cx); + }); + + cx.assert_editor_state(indoc! {" + impl A { + + fn b() {} + + «fn c() { + + }ˇ» + } + "}); + } + + { + let mut cx = EditorTestContext::new_multibuffer( + cx, + [indoc! { " + impl A { + « + // a + fn b(){} + » + « + } + fn c(){} + » + "}], + ); + + let buffer = cx.update_editor(|editor, cx| { + let buffer = editor.buffer().update(cx, |buffer, _| { + buffer.all_buffers().iter().next().unwrap().clone() + }); + buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx)); + buffer + }); + + cx.run_until_parked(); + cx.update_editor(|editor, cx| { + editor.select_all(&Default::default(), cx); + editor.autoindent(&Default::default(), cx) + }); + cx.run_until_parked(); + + cx.update(|cx| { + pretty_assertions::assert_eq!( + buffer.read(cx).text(), + indoc! { " + impl A { + + // a + fn b(){} + + + } + fn c(){} + + " } + ) + }); + } +} + #[gpui::test] async fn test_autoclose_and_auto_surround_pairs(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); @@ -13933,20 +14017,6 @@ pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsC update_test_language_settings(cx, f); } -pub(crate) fn rust_lang() -> Arc { - Arc::new(Language::new( - LanguageConfig { - name: "Rust".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - ..Default::default() - }, - Some(tree_sitter_rust::LANGUAGE.into()), - )) -} - #[track_caller] fn assert_hunk_revert( not_reverted_text_with_selections: &str, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 7f4bc3fb77..975f1b8bf0 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -189,6 +189,7 @@ impl EditorElement { register_action(view, cx, Editor::tab_prev); register_action(view, cx, Editor::indent); register_action(view, cx, Editor::outdent); + register_action(view, cx, Editor::autoindent); register_action(view, cx, Editor::delete_line); register_action(view, cx, Editor::join_lines); register_action(view, cx, Editor::sort_lines_case_sensitive); diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 877f02eefe..8b2358c6b4 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -1258,6 +1258,7 @@ pub mod tests { use crate::{ scroll::{scroll_amount::ScrollAmount, Autoscroll}, + test::editor_lsp_test_context::rust_lang, ExcerptRange, }; use futures::StreamExt; @@ -2274,7 +2275,7 @@ pub mod tests { let project = Project::test(fs, ["/a".as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); - language_registry.add(crate::editor_tests::rust_lang()); + language_registry.add(rust_lang()); let mut fake_servers = language_registry.register_fake_lsp( "Rust", FakeLspAdapter { @@ -2570,7 +2571,7 @@ pub mod tests { let project = Project::test(fs, ["/a".as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); - let language = crate::editor_tests::rust_lang(); + let language = rust_lang(); language_registry.add(language); let mut fake_servers = language_registry.register_fake_lsp( "Rust", @@ -2922,7 +2923,7 @@ pub mod tests { let project = Project::test(fs, ["/a".as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); - language_registry.add(crate::editor_tests::rust_lang()); + language_registry.add(rust_lang()); let mut fake_servers = language_registry.register_fake_lsp( "Rust", FakeLspAdapter { @@ -3153,7 +3154,7 @@ pub mod tests { let project = Project::test(fs, ["/a".as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); - language_registry.add(crate::editor_tests::rust_lang()); + language_registry.add(rust_lang()); let mut fake_servers = language_registry.register_fake_lsp( "Rust", FakeLspAdapter { @@ -3396,7 +3397,7 @@ pub mod tests { let project = Project::test(fs, ["/a".as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); - language_registry.add(crate::editor_tests::rust_lang()); + language_registry.add(rust_lang()); let mut fake_servers = language_registry.register_fake_lsp( "Rust", FakeLspAdapter { diff --git a/crates/editor/src/test/editor_lsp_test_context.rs b/crates/editor/src/test/editor_lsp_test_context.rs index 0384ed065b..b43d78bc99 100644 --- a/crates/editor/src/test/editor_lsp_test_context.rs +++ b/crates/editor/src/test/editor_lsp_test_context.rs @@ -31,6 +31,47 @@ pub struct EditorLspTestContext { pub buffer_lsp_url: lsp::Url, } +pub(crate) fn rust_lang() -> Arc { + let language = Language::new( + LanguageConfig { + name: "Rust".into(), + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + line_comments: vec!["// ".into(), "/// ".into(), "//! ".into()], + ..Default::default() + }, + Some(tree_sitter_rust::LANGUAGE.into()), + ) + .with_queries(LanguageQueries { + indents: Some(Cow::from(indoc! {r#" + [ + ((where_clause) _ @end) + (field_expression) + (call_expression) + (assignment_expression) + (let_declaration) + (let_chain) + (await_expression) + ] @indent + + (_ "[" "]" @end) @indent + (_ "<" ">" @end) @indent + (_ "{" "}" @end) @indent + (_ "(" ")" @end) @indent"#})), + brackets: Some(Cow::from(indoc! {r#" + ("(" @open ")" @close) + ("[" @open "]" @close) + ("{" @open "}" @close) + ("<" @open ">" @close) + ("\"" @open "\"" @close) + (closure_parameters "|" @open "|" @close)"#})), + ..Default::default() + }) + .expect("Could not parse queries"); + Arc::new(language) +} impl EditorLspTestContext { pub async fn new( language: Language, @@ -119,46 +160,7 @@ impl EditorLspTestContext { capabilities: lsp::ServerCapabilities, cx: &mut gpui::TestAppContext, ) -> EditorLspTestContext { - let language = Language::new( - LanguageConfig { - name: "Rust".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - line_comments: vec!["// ".into(), "/// ".into(), "//! ".into()], - ..Default::default() - }, - Some(tree_sitter_rust::LANGUAGE.into()), - ) - .with_queries(LanguageQueries { - indents: Some(Cow::from(indoc! {r#" - [ - ((where_clause) _ @end) - (field_expression) - (call_expression) - (assignment_expression) - (let_declaration) - (let_chain) - (await_expression) - ] @indent - - (_ "[" "]" @end) @indent - (_ "<" ">" @end) @indent - (_ "{" "}" @end) @indent - (_ "(" ")" @end) @indent"#})), - brackets: Some(Cow::from(indoc! {r#" - ("(" @open ")" @close) - ("[" @open "]" @close) - ("{" @open "}" @close) - ("<" @open ">" @close) - ("\"" @open "\"" @close) - (closure_parameters "|" @open "|" @close)"#})), - ..Default::default() - }) - .expect("Could not parse queries"); - - Self::new(language, capabilities, cx).await + Self::new(Arc::into_inner(rust_lang()).unwrap(), capabilities, cx).await } pub async fn new_typescript( diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 2479eafd7a..a03357c1d4 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -467,6 +467,7 @@ struct AutoindentRequest { before_edit: BufferSnapshot, entries: Vec, is_block_mode: bool, + ignore_empty_lines: bool, } #[derive(Debug, Clone)] @@ -1381,7 +1382,7 @@ impl Buffer { let autoindent_requests = self.autoindent_requests.clone(); Some(async move { - let mut indent_sizes = BTreeMap::new(); + let mut indent_sizes = BTreeMap::::new(); for request in autoindent_requests { // Resolve each edited range to its row in the current buffer and in the // buffer before this batch of edits. @@ -1475,10 +1476,12 @@ impl Buffer { let suggested_indent = indent_sizes .get(&suggestion.basis_row) .copied() + .map(|e| e.0) .unwrap_or_else(|| { snapshot.indent_size_for_line(suggestion.basis_row) }) .with_delta(suggestion.delta, language_indent_size); + if old_suggestions.get(&new_row).map_or( true, |(old_indentation, was_within_error)| { @@ -1486,7 +1489,10 @@ impl Buffer { && (!suggestion.within_error || *was_within_error) }, ) { - indent_sizes.insert(new_row, suggested_indent); + indent_sizes.insert( + new_row, + (suggested_indent, request.ignore_empty_lines), + ); } } } @@ -1494,10 +1500,12 @@ impl Buffer { if let (true, Some(original_indent_column)) = (request.is_block_mode, original_indent_column) { - let new_indent = indent_sizes - .get(&row_range.start) - .copied() - .unwrap_or_else(|| snapshot.indent_size_for_line(row_range.start)); + let new_indent = + if let Some((indent, _)) = indent_sizes.get(&row_range.start) { + *indent + } else { + snapshot.indent_size_for_line(row_range.start) + }; let delta = new_indent.len as i64 - original_indent_column as i64; if delta != 0 { for row in row_range.skip(1) { @@ -1512,7 +1520,7 @@ impl Buffer { Ordering::Equal => {} } } - size + (size, request.ignore_empty_lines) }); } } @@ -1523,6 +1531,15 @@ impl Buffer { } indent_sizes + .into_iter() + .filter_map(|(row, (indent, ignore_empty_lines))| { + if ignore_empty_lines && snapshot.line_len(row) == 0 { + None + } else { + Some((row, indent)) + } + }) + .collect() }) } @@ -2067,6 +2084,7 @@ impl Buffer { before_edit, entries, is_block_mode: matches!(mode, AutoindentMode::Block { .. }), + ignore_empty_lines: false, })); } @@ -2094,6 +2112,30 @@ impl Buffer { cx.notify(); } + pub fn autoindent_ranges(&mut self, ranges: I, cx: &mut ModelContext) + where + I: IntoIterator>, + T: ToOffset + Copy, + { + let before_edit = self.snapshot(); + let entries = ranges + .into_iter() + .map(|range| AutoindentRequestEntry { + range: before_edit.anchor_before(range.start)..before_edit.anchor_after(range.end), + first_line_is_new: true, + indent_size: before_edit.language_indent_size_at(range.start, cx), + original_indent_column: None, + }) + .collect(); + self.autoindent_requests.push(Arc::new(AutoindentRequest { + before_edit, + entries, + is_block_mode: false, + ignore_empty_lines: true, + })); + self.request_autoindent(cx); + } + // Inserts newlines at the given position to create an empty line, returning the start of the new line. // You can also request the insertion of empty lines above and below the line starting at the returned point. pub fn insert_empty_line( diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index b6ba702b4e..f1434b6d59 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -325,6 +325,13 @@ struct ExcerptBytes<'a> { reversed: bool, } +struct BufferEdit { + range: Range, + new_text: Arc, + is_insertion: bool, + original_indent_column: u32, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ExpandExcerptDirection { Up, @@ -525,57 +532,146 @@ impl MultiBuffer { pub fn edit( &self, edits: I, - mut autoindent_mode: Option, + autoindent_mode: Option, cx: &mut ModelContext, ) where I: IntoIterator, T)>, S: ToOffset, T: Into>, { - if self.read_only() { - return; - } - if self.buffers.borrow().is_empty() { - return; - } - let snapshot = self.read(cx); - let edits = edits.into_iter().map(|(range, new_text)| { - let mut range = range.start.to_offset(&snapshot)..range.end.to_offset(&snapshot); - if range.start > range.end { - mem::swap(&mut range.start, &mut range.end); + let edits = edits + .into_iter() + .map(|(range, new_text)| { + let mut range = range.start.to_offset(&snapshot)..range.end.to_offset(&snapshot); + if range.start > range.end { + mem::swap(&mut range.start, &mut range.end); + } + (range, new_text.into()) + }) + .collect::>(); + + return edit_internal(self, snapshot, edits, autoindent_mode, cx); + + // Non-generic part of edit, hoisted out to avoid blowing up LLVM IR. + fn edit_internal( + this: &MultiBuffer, + snapshot: Ref, + edits: Vec<(Range, Arc)>, + mut autoindent_mode: Option, + cx: &mut ModelContext, + ) { + if this.read_only() || this.buffers.borrow().is_empty() { + return; + } + + if let Some(buffer) = this.as_singleton() { + buffer.update(cx, |buffer, cx| { + buffer.edit(edits, autoindent_mode, cx); + }); + cx.emit(Event::ExcerptsEdited { + ids: this.excerpt_ids(), + }); + return; + } + + let original_indent_columns = match &mut autoindent_mode { + Some(AutoindentMode::Block { + original_indent_columns, + }) => mem::take(original_indent_columns), + _ => Default::default(), + }; + + let (buffer_edits, edited_excerpt_ids) = + this.convert_edits_to_buffer_edits(edits, &snapshot, &original_indent_columns); + drop(snapshot); + + for (buffer_id, mut edits) in buffer_edits { + edits.sort_unstable_by_key(|edit| edit.range.start); + this.buffers.borrow()[&buffer_id] + .buffer + .update(cx, |buffer, cx| { + let mut edits = edits.into_iter().peekable(); + let mut insertions = Vec::new(); + let mut original_indent_columns = Vec::new(); + let mut deletions = Vec::new(); + let empty_str: Arc = Arc::default(); + while let Some(BufferEdit { + mut range, + new_text, + mut is_insertion, + original_indent_column, + }) = edits.next() + { + while let Some(BufferEdit { + range: next_range, + is_insertion: next_is_insertion, + .. + }) = edits.peek() + { + if range.end >= next_range.start { + range.end = cmp::max(next_range.end, range.end); + is_insertion |= *next_is_insertion; + edits.next(); + } else { + break; + } + } + + if is_insertion { + original_indent_columns.push(original_indent_column); + insertions.push(( + buffer.anchor_before(range.start) + ..buffer.anchor_before(range.end), + new_text.clone(), + )); + } else if !range.is_empty() { + deletions.push(( + buffer.anchor_before(range.start) + ..buffer.anchor_before(range.end), + empty_str.clone(), + )); + } + } + + let deletion_autoindent_mode = + if let Some(AutoindentMode::Block { .. }) = autoindent_mode { + Some(AutoindentMode::Block { + original_indent_columns: Default::default(), + }) + } else { + autoindent_mode.clone() + }; + let insertion_autoindent_mode = + if let Some(AutoindentMode::Block { .. }) = autoindent_mode { + Some(AutoindentMode::Block { + original_indent_columns, + }) + } else { + autoindent_mode.clone() + }; + + buffer.edit(deletions, deletion_autoindent_mode, cx); + buffer.edit(insertions, insertion_autoindent_mode, cx); + }) } - (range, new_text) - }); - if let Some(buffer) = self.as_singleton() { - buffer.update(cx, |buffer, cx| { - buffer.edit(edits, autoindent_mode, cx); - }); cx.emit(Event::ExcerptsEdited { - ids: self.excerpt_ids(), + ids: edited_excerpt_ids, }); - return; } + } - let original_indent_columns = match &mut autoindent_mode { - Some(AutoindentMode::Block { - original_indent_columns, - }) => mem::take(original_indent_columns), - _ => Default::default(), - }; - - struct BufferEdit { - range: Range, - new_text: Arc, - is_insertion: bool, - original_indent_column: u32, - } + fn convert_edits_to_buffer_edits( + &self, + edits: Vec<(Range, Arc)>, + snapshot: &MultiBufferSnapshot, + original_indent_columns: &[u32], + ) -> (HashMap>, Vec) { let mut buffer_edits: HashMap> = Default::default(); let mut edited_excerpt_ids = Vec::new(); let mut cursor = snapshot.excerpts.cursor::(&()); - for (ix, (range, new_text)) in edits.enumerate() { - let new_text: Arc = new_text.into(); + for (ix, (range, new_text)) in edits.into_iter().enumerate() { let original_indent_column = original_indent_columns.get(ix).copied().unwrap_or(0); cursor.seek(&range.start, Bias::Right, &()); if cursor.item().is_none() && range.start == *cursor.start() { @@ -667,84 +763,71 @@ impl MultiBuffer { } } } + (buffer_edits, edited_excerpt_ids) + } - drop(cursor); - drop(snapshot); - // Non-generic part of edit, hoisted out to avoid blowing up LLVM IR. - fn tail( + pub fn autoindent_ranges(&self, ranges: I, cx: &mut ModelContext) + where + I: IntoIterator>, + S: ToOffset, + { + let snapshot = self.read(cx); + let empty = Arc::::from(""); + let edits = ranges + .into_iter() + .map(|range| { + let mut range = range.start.to_offset(&snapshot)..range.end.to_offset(&snapshot); + if range.start > range.end { + mem::swap(&mut range.start, &mut range.end); + } + (range, empty.clone()) + }) + .collect::>(); + + return autoindent_ranges_internal(self, snapshot, edits, cx); + + fn autoindent_ranges_internal( this: &MultiBuffer, - buffer_edits: HashMap>, - autoindent_mode: Option, - edited_excerpt_ids: Vec, + snapshot: Ref, + edits: Vec<(Range, Arc)>, cx: &mut ModelContext, ) { + if this.read_only() || this.buffers.borrow().is_empty() { + return; + } + + if let Some(buffer) = this.as_singleton() { + buffer.update(cx, |buffer, cx| { + buffer.autoindent_ranges(edits.into_iter().map(|e| e.0), cx); + }); + cx.emit(Event::ExcerptsEdited { + ids: this.excerpt_ids(), + }); + return; + } + + let (buffer_edits, edited_excerpt_ids) = + this.convert_edits_to_buffer_edits(edits, &snapshot, &[]); + drop(snapshot); + for (buffer_id, mut edits) in buffer_edits { edits.sort_unstable_by_key(|edit| edit.range.start); + + let mut ranges: Vec> = Vec::new(); + for edit in edits { + if let Some(last_range) = ranges.last_mut() { + if edit.range.start <= last_range.end { + last_range.end = last_range.end.max(edit.range.end); + continue; + } + } + ranges.push(edit.range); + } + this.buffers.borrow()[&buffer_id] .buffer .update(cx, |buffer, cx| { - let mut edits = edits.into_iter().peekable(); - let mut insertions = Vec::new(); - let mut original_indent_columns = Vec::new(); - let mut deletions = Vec::new(); - let empty_str: Arc = Arc::default(); - while let Some(BufferEdit { - mut range, - new_text, - mut is_insertion, - original_indent_column, - }) = edits.next() - { - while let Some(BufferEdit { - range: next_range, - is_insertion: next_is_insertion, - .. - }) = edits.peek() - { - if range.end >= next_range.start { - range.end = cmp::max(next_range.end, range.end); - is_insertion |= *next_is_insertion; - edits.next(); - } else { - break; - } - } - - if is_insertion { - original_indent_columns.push(original_indent_column); - insertions.push(( - buffer.anchor_before(range.start) - ..buffer.anchor_before(range.end), - new_text.clone(), - )); - } else if !range.is_empty() { - deletions.push(( - buffer.anchor_before(range.start) - ..buffer.anchor_before(range.end), - empty_str.clone(), - )); - } - } - - let deletion_autoindent_mode = - if let Some(AutoindentMode::Block { .. }) = autoindent_mode { - Some(AutoindentMode::Block { - original_indent_columns: Default::default(), - }) - } else { - autoindent_mode.clone() - }; - let insertion_autoindent_mode = - if let Some(AutoindentMode::Block { .. }) = autoindent_mode { - Some(AutoindentMode::Block { - original_indent_columns, - }) - } else { - autoindent_mode.clone() - }; - - buffer.edit(deletions, deletion_autoindent_mode, cx); - buffer.edit(insertions, insertion_autoindent_mode, cx); + buffer.autoindent_ranges(ranges, cx); }) } @@ -752,7 +835,6 @@ impl MultiBuffer { ids: edited_excerpt_ids, }); } - tail(self, buffer_edits, autoindent_mode, edited_excerpt_ids, cx); } // Inserts newlines at the given position to create an empty line, returning the start of the new line. diff --git a/crates/vim/src/indent.rs b/crates/vim/src/indent.rs index 8e4f27271b..6d5ce78f5c 100644 --- a/crates/vim/src/indent.rs +++ b/crates/vim/src/indent.rs @@ -9,9 +9,10 @@ use ui::ViewContext; pub(crate) enum IndentDirection { In, Out, + Auto, } -actions!(vim, [Indent, Outdent,]); +actions!(vim, [Indent, Outdent, AutoIndent]); pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext) { Vim::action(editor, cx, |vim, _: &Indent, cx| { @@ -49,6 +50,24 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext) { vim.switch_mode(Mode::Normal, true, cx) } }); + + Vim::action(editor, cx, |vim, _: &AutoIndent, cx| { + vim.record_current_action(cx); + let count = Vim::take_count(cx).unwrap_or(1); + vim.store_visual_marks(cx); + vim.update_editor(cx, |vim, editor, cx| { + editor.transact(cx, |editor, cx| { + let original_positions = vim.save_selection_starts(editor, cx); + for _ in 0..count { + editor.autoindent(&Default::default(), cx); + } + vim.restore_selection_cursors(editor, cx, original_positions); + }); + }); + if vim.mode.is_visual() { + vim.switch_mode(Mode::Normal, true, cx) + } + }); } impl Vim { @@ -71,10 +90,10 @@ impl Vim { motion.expand_selection(map, selection, times, false, &text_layout_details); }); }); - if dir == IndentDirection::In { - editor.indent(&Default::default(), cx); - } else { - editor.outdent(&Default::default(), cx); + match dir { + IndentDirection::In => editor.indent(&Default::default(), cx), + IndentDirection::Out => editor.outdent(&Default::default(), cx), + IndentDirection::Auto => editor.autoindent(&Default::default(), cx), } editor.change_selections(None, cx, |s| { s.move_with(|map, selection| { @@ -104,10 +123,10 @@ impl Vim { object.expand_selection(map, selection, around); }); }); - if dir == IndentDirection::In { - editor.indent(&Default::default(), cx); - } else { - editor.outdent(&Default::default(), cx); + match dir { + IndentDirection::In => editor.indent(&Default::default(), cx), + IndentDirection::Out => editor.outdent(&Default::default(), cx), + IndentDirection::Auto => editor.autoindent(&Default::default(), cx), } editor.change_selections(None, cx, |s| { s.move_with(|map, selection| { @@ -122,7 +141,11 @@ impl Vim { #[cfg(test)] mod test { - use crate::test::NeovimBackedTestContext; + use crate::{ + state::Mode, + test::{NeovimBackedTestContext, VimTestContext}, + }; + use indoc::indoc; #[gpui::test] async fn test_indent_gv(cx: &mut gpui::TestAppContext) { @@ -135,4 +158,46 @@ mod test { .await .assert_eq("« hello\n ˇ» world\n"); } + + #[gpui::test] + async fn test_autoindent_op(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + cx.set_state( + indoc!( + " + fn a() { + b(); + c(); + + d(); + ˇe(); + f(); + + g(); + } + " + ), + Mode::Normal, + ); + + cx.simulate_keystrokes("= a p"); + cx.assert_state( + indoc!( + " + fn a() { + b(); + c(); + + d(); + ˇe(); + f(); + + g(); + } + " + ), + Mode::Normal, + ); + } } diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 24e8e7bed4..bde3c12027 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -170,6 +170,9 @@ impl Vim { Some(Operator::Indent) => self.indent_motion(motion, times, IndentDirection::In, cx), Some(Operator::Rewrap) => self.rewrap_motion(motion, times, cx), Some(Operator::Outdent) => self.indent_motion(motion, times, IndentDirection::Out, cx), + Some(Operator::AutoIndent) => { + self.indent_motion(motion, times, IndentDirection::Auto, cx) + } Some(Operator::Lowercase) => { self.change_case_motion(motion, times, CaseTarget::Lowercase, cx) } @@ -202,6 +205,9 @@ impl Vim { Some(Operator::Outdent) => { self.indent_object(object, around, IndentDirection::Out, cx) } + Some(Operator::AutoIndent) => { + self.indent_object(object, around, IndentDirection::Auto, cx) + } Some(Operator::Rewrap) => self.rewrap_object(object, around, cx), Some(Operator::Lowercase) => { self.change_case_object(object, around, CaseTarget::Lowercase, cx) diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index 47742fb0c3..af187381ad 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -72,6 +72,7 @@ pub enum Operator { Jump { line: bool }, Indent, Outdent, + AutoIndent, Rewrap, Lowercase, Uppercase, @@ -465,6 +466,7 @@ impl Operator { Operator::Jump { line: true } => "'", Operator::Jump { line: false } => "`", Operator::Indent => ">", + Operator::AutoIndent => "eq", Operator::Rewrap => "gq", Operator::Outdent => "<", Operator::Uppercase => "gU", @@ -510,6 +512,7 @@ impl Operator { | Operator::Rewrap | Operator::Indent | Operator::Outdent + | Operator::AutoIndent | Operator::Lowercase | Operator::Uppercase | Operator::Object { .. } diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index a1820eafbb..db0a765170 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -470,6 +470,7 @@ impl Vim { | Operator::Replace | Operator::Indent | Operator::Outdent + | Operator::AutoIndent | Operator::Lowercase | Operator::Uppercase | Operator::OppositeCase From 579bc8f01597dadf784f93696de2a2d1d3de2981 Mon Sep 17 00:00:00 2001 From: Kyle Kelley Date: Mon, 2 Dec 2024 15:22:03 -0800 Subject: [PATCH 244/886] Upgrade repl dependencies (#21431) Bump dependencies for jupyter packages. cc @maxdeviant Release Notes: - N/A --- Cargo.lock | 170 +++++------------------ Cargo.toml | 8 +- crates/repl/src/kernels/native_kernel.rs | 9 +- crates/repl/src/outputs.rs | 8 +- 4 files changed, 48 insertions(+), 147 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7504b8491b..d21006ee55 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -930,20 +930,6 @@ version = "4.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" -[[package]] -name = "async-tls" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfeefd0ca297cbbb3bd34fd6b228401c2a5177038257afd751bc29f0a2da4795" -dependencies = [ - "futures-core", - "futures-io", - "rustls 0.20.9", - "rustls-pemfile 1.0.4", - "webpki", - "webpki-roots 0.22.6", -] - [[package]] name = "async-tls" version = "0.13.0" @@ -968,21 +954,6 @@ dependencies = [ "syn 2.0.87", ] -[[package]] -name = "async-tungstenite" -version = "0.22.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce01ac37fdc85f10a43c43bc582cbd566720357011578a935761075f898baf58" -dependencies = [ - "async-std", - "async-tls 0.12.0", - "futures-io", - "futures-util", - "log", - "pin-project-lite", - "tungstenite 0.19.0", -] - [[package]] name = "async-tungstenite" version = "0.28.0" @@ -990,7 +961,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90e661b6cb0a6eb34d02c520b052daa3aa9ac0cc02495c9d066bbce13ead132b" dependencies = [ "async-std", - "async-tls 0.13.0", + "async-tls", "futures-io", "futures-util", "log", @@ -1160,7 +1131,7 @@ dependencies = [ "fastrand 2.2.0", "hex", "http 0.2.12", - "ring 0.17.8", + "ring", "time", "tokio", "tracing", @@ -1350,7 +1321,7 @@ dependencies = [ "once_cell", "p256", "percent-encoding", - "ring 0.17.8", + "ring", "sha2", "subtle", "time", @@ -2507,7 +2478,7 @@ dependencies = [ "anyhow", "async-native-tls", "async-recursion 0.3.2", - "async-tungstenite 0.28.0", + "async-tungstenite", "chrono", "clock", "cocoa 0.26.0", @@ -2639,7 +2610,7 @@ dependencies = [ "assistant_tool", "async-stripe", "async-trait", - "async-tungstenite 0.28.0", + "async-tungstenite", "audio", "aws-config", "aws-sdk-kinesis", @@ -4540,7 +4511,7 @@ dependencies = [ "futures-core", "futures-sink", "nanorand", - "spin 0.9.8", + "spin", ] [[package]] @@ -6453,7 +6424,7 @@ dependencies = [ "base64 0.21.7", "js-sys", "pem", - "ring 0.17.8", + "ring", "serde", "serde_json", "simple_asn1", @@ -6461,47 +6432,31 @@ dependencies = [ [[package]] name = "jupyter-protocol" -version = "0.3.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d4d496ac890e14efc12c5289818b3c39e3026a7bb02d5576b011e1a062d4bcc" +checksum = "503458f8125fd9047ed0a9d95d7a93adc5eaf8bce48757c6d401e09f71ad3407" dependencies = [ "anyhow", "async-trait", "bytes 1.8.0", "chrono", "futures 0.3.31", - "jupyter-serde", - "rand 0.8.5", "serde", "serde_json", "uuid", ] -[[package]] -name = "jupyter-serde" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32aa595c3912167b7eafcaa822b767ad1fa9605a18127fc9ac741241b796410e" -dependencies = [ - "anyhow", - "serde", - "serde_json", - "thiserror 1.0.69", - "uuid", -] - [[package]] name = "jupyter-websocket-client" -version = "0.5.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5850894210a3f033ff730d6f956b0335db38573ce7bb61c6abbf69dcbe284ba7" +checksum = "58d9afa5bc6eeafb78f710a2efc585f69099f8b6a99dc7eb826581e3773a6e31" dependencies = [ "anyhow", "async-trait", - "async-tungstenite 0.22.2", + "async-tungstenite", "futures 0.3.31", "jupyter-protocol", - "jupyter-serde", "serde", "serde_json", "url", @@ -6817,7 +6772,7 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" dependencies = [ - "spin 0.9.8", + "spin", ] [[package]] @@ -7539,13 +7494,13 @@ dependencies = [ [[package]] name = "nbformat" -version = "0.7.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa6827a3881aa100bb2241cd2633b3c79474dbc93704f1f2cf5cc85064cda4be" +checksum = "19835ad46507d80d9671e10a1c7c335655f4f3033aeb066fe025f14e070c2e66" dependencies = [ "anyhow", "chrono", - "jupyter-serde", + "jupyter-protocol", "serde", "serde_json", "thiserror 1.0.69", @@ -9571,7 +9526,7 @@ dependencies = [ "bytes 1.8.0", "getrandom 0.2.15", "rand 0.8.5", - "ring 0.17.8", + "ring", "rustc-hash 2.0.0", "rustls 0.23.16", "rustls-pki-types", @@ -10214,21 +10169,6 @@ dependencies = [ "util", ] -[[package]] -name = "ring" -version = "0.16.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" -dependencies = [ - "cc", - "libc", - "once_cell", - "spin 0.5.2", - "untrusted 0.7.1", - "web-sys", - "winapi", -] - [[package]] name = "ring" version = "0.17.8" @@ -10239,8 +10179,8 @@ dependencies = [ "cfg-if", "getrandom 0.2.15", "libc", - "spin 0.9.8", - "untrusted 0.9.0", + "spin", + "untrusted", "windows-sys 0.52.0", ] @@ -10333,7 +10273,7 @@ name = "rpc" version = "0.1.0" dependencies = [ "anyhow", - "async-tungstenite 0.28.0", + "async-tungstenite", "base64 0.22.1", "chrono", "collections", @@ -10375,9 +10315,9 @@ dependencies = [ [[package]] name = "runtimelib" -version = "0.22.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3a8ab675beb5cf25c28f9c6ddb8f47bcf73b43872797e6ab6157865f44d1e19" +checksum = "445ff0ee3d5c832cdd27efadd004a741423db1f91bd1de593a14b21211ea084c" dependencies = [ "anyhow", "async-dispatcher", @@ -10390,8 +10330,7 @@ dependencies = [ "futures 0.3.31", "glob", "jupyter-protocol", - "jupyter-serde", - "ring 0.17.8", + "ring", "serde", "serde_json", "shellexpand 3.1.0", @@ -10518,18 +10457,6 @@ dependencies = [ "rustix 0.38.40", ] -[[package]] -name = "rustls" -version = "0.20.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99" -dependencies = [ - "log", - "ring 0.16.20", - "sct", - "webpki", -] - [[package]] name = "rustls" version = "0.21.12" @@ -10537,7 +10464,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ "log", - "ring 0.17.8", + "ring", "rustls-webpki 0.101.7", "sct", ] @@ -10549,7 +10476,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eee87ff5d9b36712a58574e12e9f0ea80f915a5b0ac518d322b24a465617925e" dependencies = [ "once_cell", - "ring 0.17.8", + "ring", "rustls-pki-types", "rustls-webpki 0.102.8", "subtle", @@ -10614,8 +10541,8 @@ version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ - "ring 0.17.8", - "untrusted 0.9.0", + "ring", + "untrusted", ] [[package]] @@ -10624,9 +10551,9 @@ version = "0.102.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" dependencies = [ - "ring 0.17.8", + "ring", "rustls-pki-types", - "untrusted 0.9.0", + "untrusted", ] [[package]] @@ -10740,8 +10667,8 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ - "ring 0.17.8", - "untrusted 0.9.0", + "ring", + "untrusted", ] [[package]] @@ -11503,12 +11430,6 @@ dependencies = [ "smallvec", ] -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - [[package]] name = "spin" version = "0.9.8" @@ -13389,25 +13310,6 @@ version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8" -[[package]] -name = "tungstenite" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15fba1a6d6bb030745759a9a2a588bfe8490fc8b4751a277db3a0be1c9ebbf67" -dependencies = [ - "byteorder", - "bytes 1.8.0", - "data-encoding", - "http 0.2.12", - "httparse", - "log", - "rand 0.8.5", - "sha1", - "thiserror 1.0.69", - "url", - "utf-8", -] - [[package]] name = "tungstenite" version = "0.20.1" @@ -13619,12 +13521,6 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1766d682d402817b5ac4490b3c3002d91dfa0d22812f341609f97b08757359c" -[[package]] -name = "untrusted" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" - [[package]] name = "untrusted" version = "0.9.0" @@ -14535,8 +14431,8 @@ version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" dependencies = [ - "ring 0.17.8", - "untrusted 0.9.0", + "ring", + "untrusted", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index b50b6d9f9d..0465545990 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -388,14 +388,14 @@ indexmap = { version = "1.6.2", features = ["serde"] } indoc = "2" itertools = "0.13.0" jsonwebtoken = "9.3" -jupyter-protocol = { version = "0.3.0" } -jupyter-websocket-client = { version = "0.5.0" } +jupyter-protocol = { version = "0.5.0" } +jupyter-websocket-client = { version = "0.8.0" } libc = "0.2" linkify = "0.10.0" log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] } markup5ever_rcdom = "0.3.0" nanoid = "0.4" -nbformat = { version = "0.7.0" } +nbformat = { version = "0.9.0" } nix = "0.29" num-format = "0.4.4" once_cell = "1.19.0" @@ -429,7 +429,7 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "fd110f "stream", ] } rsa = "0.9.6" -runtimelib = { version = "0.22.0", default-features = false, features = [ +runtimelib = { version = "0.24.0", default-features = false, features = [ "async-dispatcher-runtime", ] } rustc-demangle = "0.1.23" diff --git a/crates/repl/src/kernels/native_kernel.rs b/crates/repl/src/kernels/native_kernel.rs index 974a721ac5..2d796e12c6 100644 --- a/crates/repl/src/kernels/native_kernel.rs +++ b/crates/repl/src/kernels/native_kernel.rs @@ -6,9 +6,12 @@ use futures::{ AsyncBufReadExt as _, SinkExt as _, }; use gpui::{EntityId, Task, View, WindowContext}; -use jupyter_protocol::{JupyterKernelspec, JupyterMessage, JupyterMessageContent, KernelInfoReply}; +use jupyter_protocol::{ + connection_info::{ConnectionInfo, Transport}, + ExecutionState, JupyterKernelspec, JupyterMessage, JupyterMessageContent, KernelInfoReply, +}; use project::Fs; -use runtimelib::{dirs, ConnectionInfo, ExecutionState}; +use runtimelib::dirs; use smol::{net::TcpListener, process::Command}; use std::{ env, @@ -119,7 +122,7 @@ impl NativeRunningKernel { let ports = peek_ports(ip).await?; let connection_info = ConnectionInfo { - transport: "tcp".to_string(), + transport: Transport::TCP, ip: ip.to_string(), stdin_port: ports[0], control_port: ports[1], diff --git a/crates/repl/src/outputs.rs b/crates/repl/src/outputs.rs index b705a15568..a1335f2a0d 100644 --- a/crates/repl/src/outputs.rs +++ b/crates/repl/src/outputs.rs @@ -334,9 +334,11 @@ impl ExecutionView { result.transient.as_ref().and_then(|t| t.display_id.clone()), cx, ), - JupyterMessageContent::DisplayData(result) => { - Output::new(&result.data, result.transient.display_id.clone(), cx) - } + JupyterMessageContent::DisplayData(result) => Output::new( + &result.data, + result.transient.as_ref().and_then(|t| t.display_id.clone()), + cx, + ), JupyterMessageContent::StreamContent(result) => { // Previous stream data will combine together, handling colors, carriage returns, etc if let Some(new_terminal) = self.apply_terminal_text(&result.text, cx) { From f4dbcb67143a12d55735cd5811ab8601a022b1e1 Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Mon, 2 Dec 2024 16:27:29 -0700 Subject: [PATCH 245/886] Use explicit sort order instead of comparison impls for gpui prims (#21430) Found this while looking into adding support for the Surface primitive on Linux, for rendering video shares. In that case it would be expensive to compare images for equality. `Eq` and `PartialEq` were being required but not used here due to use of `Ord` and `PartialOrd`. Release Notes: - N/A --- crates/gpui/src/scene.rs | 129 +++++---------------------------------- 1 file changed, 16 insertions(+), 113 deletions(-) diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index 9787ec5d87..418be6af22 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -128,13 +128,15 @@ impl Scene { } pub fn finish(&mut self) { - self.shadows.sort(); - self.quads.sort(); - self.paths.sort(); - self.underlines.sort(); - self.monochrome_sprites.sort(); - self.polychrome_sprites.sort(); - self.surfaces.sort(); + self.shadows.sort_by_key(|shadow| shadow.order); + self.quads.sort_by_key(|quad| quad.order); + self.paths.sort_by_key(|path| path.order); + self.underlines.sort_by_key(|underline| underline.order); + self.monochrome_sprites + .sort_by_key(|sprite| (sprite.order, sprite.tile.tile_id)); + self.polychrome_sprites + .sort_by_key(|sprite| (sprite.order, sprite.tile.tile_id)); + self.surfaces.sort_by_key(|surface| surface.order); } #[cfg_attr( @@ -196,7 +198,7 @@ pub(crate) enum PaintOperation { EndLayer, } -#[derive(Clone, Ord, PartialOrd, Eq, PartialEq)] +#[derive(Clone)] pub(crate) enum Primitive { Shadow(Shadow), Quad(Quad), @@ -449,7 +451,7 @@ pub(crate) enum PrimitiveBatch<'a> { Surfaces(&'a [PaintSurface]), } -#[derive(Default, Debug, Clone, Eq, PartialEq)] +#[derive(Default, Debug, Clone)] #[repr(C)] pub(crate) struct Quad { pub order: DrawOrder, @@ -462,25 +464,13 @@ pub(crate) struct Quad { pub border_widths: Edges, } -impl Ord for Quad { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.order.cmp(&other.order) - } -} - -impl PartialOrd for Quad { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - impl From for Primitive { fn from(quad: Quad) -> Self { Primitive::Quad(quad) } } -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Clone)] #[repr(C)] pub(crate) struct Underline { pub order: DrawOrder, @@ -492,25 +482,13 @@ pub(crate) struct Underline { pub wavy: bool, } -impl Ord for Underline { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.order.cmp(&other.order) - } -} - -impl PartialOrd for Underline { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - impl From for Primitive { fn from(underline: Underline) -> Self { Primitive::Underline(underline) } } -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Clone)] #[repr(C)] pub(crate) struct Shadow { pub order: DrawOrder, @@ -521,18 +499,6 @@ pub(crate) struct Shadow { pub color: Hsla, } -impl Ord for Shadow { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.order.cmp(&other.order) - } -} - -impl PartialOrd for Shadow { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - impl From for Primitive { fn from(shadow: Shadow) -> Self { Primitive::Shadow(shadow) @@ -642,7 +608,7 @@ impl Default for TransformationMatrix { } } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug)] #[repr(C)] pub(crate) struct MonochromeSprite { pub order: DrawOrder, @@ -654,28 +620,13 @@ pub(crate) struct MonochromeSprite { pub transformation: TransformationMatrix, } -impl Ord for MonochromeSprite { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - match self.order.cmp(&other.order) { - std::cmp::Ordering::Equal => self.tile.tile_id.cmp(&other.tile.tile_id), - order => order, - } - } -} - -impl PartialOrd for MonochromeSprite { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - impl From for Primitive { fn from(sprite: MonochromeSprite) -> Self { Primitive::MonochromeSprite(sprite) } } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] #[repr(C)] pub(crate) struct PolychromeSprite { pub order: DrawOrder, @@ -687,22 +638,6 @@ pub(crate) struct PolychromeSprite { pub corner_radii: Corners, pub tile: AtlasTile, } -impl Eq for PolychromeSprite {} - -impl Ord for PolychromeSprite { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - match self.order.cmp(&other.order) { - std::cmp::Ordering::Equal => self.tile.tile_id.cmp(&other.tile.tile_id), - order => order, - } - } -} - -impl PartialOrd for PolychromeSprite { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} impl From for Primitive { fn from(sprite: PolychromeSprite) -> Self { @@ -710,7 +645,7 @@ impl From for Primitive { } } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug)] pub(crate) struct PaintSurface { pub order: DrawOrder, pub bounds: Bounds, @@ -719,18 +654,6 @@ pub(crate) struct PaintSurface { pub image_buffer: media::core_video::CVImageBuffer, } -impl Ord for PaintSurface { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.order.cmp(&other.order) - } -} - -impl PartialOrd for PaintSurface { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - impl From for Primitive { fn from(surface: PaintSurface) -> Self { Primitive::Surface(surface) @@ -859,26 +782,6 @@ impl Path { } } -impl Eq for Path {} - -impl PartialEq for Path { - fn eq(&self, other: &Self) -> bool { - self.order == other.order - } -} - -impl Ord for Path { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.order.cmp(&other.order) - } -} - -impl PartialOrd for Path { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - impl From> for Primitive { fn from(path: Path) -> Self { Primitive::Path(path) From e1c509e0de487d5ed6f0ad66e62be2063654f888 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Mon, 2 Dec 2024 18:48:03 -0500 Subject: [PATCH 246/886] Check for vulnerable dependencies in CI (#21424) This PR adds GitHub's dependency review action to CI, to flag PRs that introduce new Cargo.lock entries for vulnerable crates according to the GHSA database. An alternative would be to run `cargo audit`, which checks against the RustSec database. The state of synchronization between these two databases seems a bit messy, but as far as I can tell GHSA has most recent RustSec advisories on file, while RustSec is missing a larger number of recent GHSA advisories. The dependency review action should be smart enough not to flag PRs because an untouched entry in Cargo.lock has a new advisory. I've turned off the "license check" functionality since we have a separate CI step for that. Release Notes: - N/A --- .github/workflows/ci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 49881e2e7c..33c85f74b9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -113,6 +113,11 @@ jobs: script/check-licenses script/generate-licenses /tmp/zed_licenses_output + - name: Check for new vulnerable dependencies + uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4 + with: + license-check: false + - name: Run tests uses: ./.github/actions/run_tests From b53b2c03761d65647100400706670a0fe2d813ab Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Mon, 2 Dec 2024 19:39:18 -0500 Subject: [PATCH 247/886] Run dependency review for pull requests only (#21432) This was an oversight in the original PR, dependency-review-action won't work properly for `push` events ([example](https://github.com/zed-industries/zed/actions/runs/12130053580/job/33819624076)). Release Notes: - N/A --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 33c85f74b9..602808f1b5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -114,6 +114,7 @@ jobs: script/generate-licenses /tmp/zed_licenses_output - name: Check for new vulnerable dependencies + if: github.event_name == 'pull_request' uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4 with: license-check: false From 2b143784da1adfb82462076b54ba327159996a79 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Tue, 3 Dec 2024 00:40:46 -0300 Subject: [PATCH 248/886] Improve audio files icon (#21441) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It took me a couple of minutes of staring at this speaker icon to figure out it was a speaker! I even researched whether the `.wav` file type had a specific icon, given I thought it was a specific triangle of sorts 😅 I'm sensing audio waves, at this size, will be easier to parse. Release Notes: - N/A --- assets/icons/file_icons/audio.svg | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/assets/icons/file_icons/audio.svg b/assets/icons/file_icons/audio.svg index 5152efb874..672f736c95 100644 --- a/assets/icons/file_icons/audio.svg +++ b/assets/icons/file_icons/audio.svg @@ -1,4 +1,8 @@ - - + + + + + + From a8c7e610211de13d730a99f648ba6517a9f0a0f5 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Tue, 3 Dec 2024 12:45:15 +0800 Subject: [PATCH 249/886] Fix AI Context menu text wrapping causing overlap (#21438) Closes https://github.com/zed-industries/zed/issues/20678 | Before | After | | --- | --- | | SCR-20241203-jreb | SCR-20241203-jwhe | Release Notes: - Fixed AI Context menu text wrapping causing overlap. Also cc #21409 @WeetHet @osiewicz to use `Label`, this PR has been fixed `Label` to ensure `whitespace_nowrap` when use `single_line`. --------- Co-authored-by: Danilo Leal --- crates/assistant/src/slash_command_picker.rs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/crates/assistant/src/slash_command_picker.rs b/crates/assistant/src/slash_command_picker.rs index 8e797d6184..215888540a 100644 --- a/crates/assistant/src/slash_command_picker.rs +++ b/crates/assistant/src/slash_command_picker.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use gpui::{AnyElement, DismissEvent, SharedString, Task, WeakView}; use picker::{Picker, PickerDelegate, PickerEditorPosition}; -use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger}; +use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger, Tooltip}; use crate::assistant_panel::ContextEditor; use crate::SlashCommandWorkingSet; @@ -177,11 +177,17 @@ impl PickerDelegate for SlashCommandDelegate { .inset(true) .spacing(ListItemSpacing::Dense) .selected(selected) + .tooltip({ + let description = info.description.clone(); + move |cx| cx.new_view(|_| Tooltip::new(description.clone())).into() + }) .child( v_flex() .group(format!("command-entry-label-{ix}")) .w_full() + .py_0p5() .min_w(px(250.)) + .max_w(px(400.)) .child( h_flex() .gap_1p5() @@ -192,7 +198,7 @@ impl PickerDelegate for SlashCommandDelegate { { label.push_str(&args); } - Label::new(label).size(LabelSize::Small) + Label::new(label).single_line().size(LabelSize::Small) })) .children(info.args.clone().filter(|_| !selected).map( |args| { @@ -200,6 +206,7 @@ impl PickerDelegate for SlashCommandDelegate { .font_buffer(cx) .child( Label::new(args) + .single_line() .size(LabelSize::Small) .color(Color::Muted), ) @@ -210,9 +217,11 @@ impl PickerDelegate for SlashCommandDelegate { )), ) .child( - Label::new(info.description.clone()) - .size(LabelSize::Small) - .color(Color::Muted), + div().overflow_hidden().text_ellipsis().child( + Label::new(info.description.clone()) + .size(LabelSize::Small) + .color(Color::Muted), + ), ), ), ), From a76cd778c4eaf6af69f68f31190563836e25fb89 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Tue, 3 Dec 2024 10:07:59 -0300 Subject: [PATCH 250/886] Disable hunk diff arrow buttons when there's only one hunk (#21437) Closes https://github.com/zed-industries/zed/issues/20817 | One hunk | Multiple hunks | |--------|--------| | Screenshot 2024-12-03 at 09 42 49 | Screenshot 2024-12-02 at 23 36 38 | Release Notes: - Fixed showing prev/next hunk navigation buttons when there is only one hunk --- crates/editor/src/hunk_diff.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/editor/src/hunk_diff.rs b/crates/editor/src/hunk_diff.rs index 27bb8ac557..3da005cd2c 100644 --- a/crates/editor/src/hunk_diff.rs +++ b/crates/editor/src/hunk_diff.rs @@ -399,6 +399,12 @@ impl Editor { } } + fn has_multiple_hunks(&self, cx: &AppContext) -> bool { + let snapshot = self.buffer.read(cx).snapshot(cx); + let mut hunks = snapshot.git_diff_hunks_in_range(MultiBufferRow::MIN..MultiBufferRow::MAX); + hunks.nth(1).is_some() + } + fn hunk_header_block( &self, hunk: &HoveredHunk, @@ -428,6 +434,7 @@ impl Editor { render: Arc::new({ let editor = cx.view().clone(); let hunk = hunk.clone(); + let has_multiple_hunks = self.has_multiple_hunks(cx); move |cx| { let hunk_controls_menu_handle = @@ -471,6 +478,7 @@ impl Editor { IconButton::new("next-hunk", IconName::ArrowDown) .shape(IconButtonShape::Square) .icon_size(IconSize::Small) + .disabled(!has_multiple_hunks) .tooltip({ let focus_handle = editor.focus_handle(cx); move |cx| { @@ -499,6 +507,7 @@ impl Editor { IconButton::new("prev-hunk", IconName::ArrowUp) .shape(IconButtonShape::Square) .icon_size(IconSize::Small) + .disabled(!has_multiple_hunks) .tooltip({ let focus_handle = editor.focus_handle(cx); move |cx| { From 1270ef3ea543064d87ea4556ebf1ef46553b79dc Mon Sep 17 00:00:00 2001 From: Sebastian Nickels Date: Tue, 3 Dec 2024 16:24:30 +0100 Subject: [PATCH 251/886] Enable toolchain venv in new terminals (#21388) Fixes part of issue #7808 > This venv should be the one we automatically activate when opening new terminals, if the detect_venv setting is on. Release Notes: - Selected Python toolchains (virtual environments) are now automatically activated in new terminals. --------- Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> --- crates/project/src/terminals.rs | 344 ++++++++++++--------- crates/terminal/src/terminal_settings.rs | 2 +- crates/terminal_view/src/persistence.rs | 56 ++-- crates/terminal_view/src/terminal_panel.rs | 281 ++++++++++------- crates/terminal_view/src/terminal_view.rs | 65 ++-- 5 files changed, 441 insertions(+), 307 deletions(-) diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index 111516c82d..34ef4d8a82 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -1,8 +1,9 @@ use crate::Project; -use anyhow::Context as _; +use anyhow::{Context as _, Result}; use collections::HashMap; -use gpui::{AnyWindowHandle, AppContext, Context, Entity, Model, ModelContext, WeakModel}; +use gpui::{AnyWindowHandle, AppContext, Context, Entity, Model, ModelContext, Task, WeakModel}; use itertools::Itertools; +use language::LanguageName; use settings::{Settings, SettingsLocation}; use smol::channel::bounded; use std::{ @@ -10,10 +11,11 @@ use std::{ env::{self}, iter, path::{Path, PathBuf}, + sync::Arc, }; use task::{Shell, SpawnInTerminal}; use terminal::{ - terminal_settings::{self, TerminalSettings}, + terminal_settings::{self, TerminalSettings, VenvSettings}, TaskState, TaskStatus, Terminal, TerminalBuilder, }; use util::ResultExt; @@ -42,7 +44,7 @@ pub struct SshCommand { } impl Project { - pub fn active_project_directory(&self, cx: &AppContext) -> Option { + pub fn active_project_directory(&self, cx: &AppContext) -> Option> { let worktree = self .active_entry() .and_then(|entry_id| self.worktree_for_entry(entry_id, cx)) @@ -53,7 +55,7 @@ impl Project { worktree .root_entry() .filter(|entry| entry.is_dir()) - .map(|_| worktree.abs_path().to_path_buf()) + .map(|_| worktree.abs_path().clone()) }); worktree } @@ -87,12 +89,12 @@ impl Project { kind: TerminalKind, window: AnyWindowHandle, cx: &mut ModelContext, - ) -> anyhow::Result> { - let path = match &kind { - TerminalKind::Shell(path) => path.as_ref().map(|path| path.to_path_buf()), + ) -> Task>> { + let path: Option> = match &kind { + TerminalKind::Shell(path) => path.as_ref().map(|path| Arc::from(path.as_ref())), TerminalKind::Task(spawn_task) => { if let Some(cwd) = &spawn_task.cwd { - Some(cwd.clone()) + Some(Arc::from(cwd.as_ref())) } else { self.active_project_directory(cx) } @@ -109,7 +111,7 @@ impl Project { }); } } - let settings = TerminalSettings::get(settings_location, cx); + let settings = TerminalSettings::get(settings_location, cx).clone(); let (completion_tx, completion_rx) = bounded(1); @@ -128,160 +130,206 @@ impl Project { } else { None }; - let python_venv_directory = path - .as_ref() - .and_then(|path| self.python_venv_directory(path, settings, cx)); - let mut python_venv_activate_command = None; - let (spawn_task, shell) = match kind { - TerminalKind::Shell(_) => { - if let Some(python_venv_directory) = python_venv_directory { - python_venv_activate_command = - self.python_activate_command(&python_venv_directory, settings); - } + cx.spawn(move |this, mut cx| async move { + let python_venv_directory = if let Some(path) = path.clone() { + this.update(&mut cx, |this, cx| { + this.python_venv_directory(path, settings.detect_venv.clone(), cx) + })? + .await + } else { + None + }; + let mut python_venv_activate_command = None; - match &ssh_details { - Some((host, ssh_command)) => { - log::debug!("Connecting to a remote server: {ssh_command:?}"); - - // Alacritty sets its terminfo to `alacritty`, this requiring hosts to have it installed - // to properly display colors. - // We do not have the luxury of assuming the host has it installed, - // so we set it to a default that does not break the highlighting via ssh. - env.entry("TERM".to_string()) - .or_insert_with(|| "xterm-256color".to_string()); - - let (program, args) = - wrap_for_ssh(ssh_command, None, path.as_deref(), env, None); - env = HashMap::default(); - ( - None, - Shell::WithArguments { - program, - args, - title_override: Some(format!("{} — Terminal", host).into()), - }, - ) + let (spawn_task, shell) = match kind { + TerminalKind::Shell(_) => { + if let Some(python_venv_directory) = python_venv_directory { + python_venv_activate_command = this + .update(&mut cx, |this, _| { + this.python_activate_command( + &python_venv_directory, + &settings.detect_venv, + ) + }) + .ok() + .flatten(); } - None => (None, settings.shell.clone()), - } - } - TerminalKind::Task(spawn_task) => { - let task_state = Some(TaskState { - id: spawn_task.id, - full_label: spawn_task.full_label, - label: spawn_task.label, - command_label: spawn_task.command_label, - hide: spawn_task.hide, - status: TaskStatus::Running, - show_summary: spawn_task.show_summary, - show_command: spawn_task.show_command, - completion_rx, - }); - env.extend(spawn_task.env); + match &ssh_details { + Some((host, ssh_command)) => { + log::debug!("Connecting to a remote server: {ssh_command:?}"); - if let Some(venv_path) = &python_venv_directory { - env.insert( - "VIRTUAL_ENV".to_string(), - venv_path.to_string_lossy().to_string(), - ); - } + // Alacritty sets its terminfo to `alacritty`, this requiring hosts to have it installed + // to properly display colors. + // We do not have the luxury of assuming the host has it installed, + // so we set it to a default that does not break the highlighting via ssh. + env.entry("TERM".to_string()) + .or_insert_with(|| "xterm-256color".to_string()); - match &ssh_details { - Some((host, ssh_command)) => { - log::debug!("Connecting to a remote server: {ssh_command:?}"); - env.entry("TERM".to_string()) - .or_insert_with(|| "xterm-256color".to_string()); - let (program, args) = wrap_for_ssh( - ssh_command, - Some((&spawn_task.command, &spawn_task.args)), - path.as_deref(), - env, - python_venv_directory, - ); - env = HashMap::default(); - ( - task_state, - Shell::WithArguments { - program, - args, - title_override: Some(format!("{} — Terminal", host).into()), - }, - ) - } - None => { - if let Some(venv_path) = &python_venv_directory { - add_environment_path(&mut env, &venv_path.join("bin")).log_err(); + let (program, args) = + wrap_for_ssh(ssh_command, None, path.as_deref(), env, None); + env = HashMap::default(); + ( + Option::::None, + Shell::WithArguments { + program, + args, + title_override: Some(format!("{} — Terminal", host).into()), + }, + ) } - - ( - task_state, - Shell::WithArguments { - program: spawn_task.command, - args: spawn_task.args, - title_override: None, - }, - ) + None => (None, settings.shell.clone()), } } - } - }; + TerminalKind::Task(spawn_task) => { + let task_state = Some(TaskState { + id: spawn_task.id, + full_label: spawn_task.full_label, + label: spawn_task.label, + command_label: spawn_task.command_label, + hide: spawn_task.hide, + status: TaskStatus::Running, + show_summary: spawn_task.show_summary, + show_command: spawn_task.show_command, + completion_rx, + }); - let terminal = TerminalBuilder::new( - local_path, - spawn_task, - shell, - env, - settings.cursor_shape.unwrap_or_default(), - settings.alternate_scroll, - settings.max_scroll_history_lines, - ssh_details.is_some(), - window, - completion_tx, - cx, - ) - .map(|builder| { - let terminal_handle = cx.new_model(|cx| builder.subscribe(cx)); + env.extend(spawn_task.env); - self.terminals - .local_handles - .push(terminal_handle.downgrade()); + if let Some(venv_path) = &python_venv_directory { + env.insert( + "VIRTUAL_ENV".to_string(), + venv_path.to_string_lossy().to_string(), + ); + } - let id = terminal_handle.entity_id(); - cx.observe_release(&terminal_handle, move |project, _terminal, cx| { - let handles = &mut project.terminals.local_handles; + match &ssh_details { + Some((host, ssh_command)) => { + log::debug!("Connecting to a remote server: {ssh_command:?}"); + env.entry("TERM".to_string()) + .or_insert_with(|| "xterm-256color".to_string()); + let (program, args) = wrap_for_ssh( + ssh_command, + Some((&spawn_task.command, &spawn_task.args)), + path.as_deref(), + env, + python_venv_directory, + ); + env = HashMap::default(); + ( + task_state, + Shell::WithArguments { + program, + args, + title_override: Some(format!("{} — Terminal", host).into()), + }, + ) + } + None => { + if let Some(venv_path) = &python_venv_directory { + add_environment_path(&mut env, &venv_path.join("bin")).log_err(); + } - if let Some(index) = handles - .iter() - .position(|terminal| terminal.entity_id() == id) - { - handles.remove(index); - cx.notify(); + ( + task_state, + Shell::WithArguments { + program: spawn_task.command, + args: spawn_task.args, + title_override: None, + }, + ) + } + } } - }) - .detach(); + }; + let terminal = this.update(&mut cx, |this, cx| { + TerminalBuilder::new( + local_path.map(|path| path.to_path_buf()), + spawn_task, + shell, + env, + settings.cursor_shape.unwrap_or_default(), + settings.alternate_scroll, + settings.max_scroll_history_lines, + ssh_details.is_some(), + window, + completion_tx, + cx, + ) + .map(|builder| { + let terminal_handle = cx.new_model(|cx| builder.subscribe(cx)); - if let Some(activate_command) = python_venv_activate_command { - self.activate_python_virtual_environment(activate_command, &terminal_handle, cx); - } - terminal_handle - }); + this.terminals + .local_handles + .push(terminal_handle.downgrade()); - terminal + let id = terminal_handle.entity_id(); + cx.observe_release(&terminal_handle, move |project, _terminal, cx| { + let handles = &mut project.terminals.local_handles; + + if let Some(index) = handles + .iter() + .position(|terminal| terminal.entity_id() == id) + { + handles.remove(index); + cx.notify(); + } + }) + .detach(); + + if let Some(activate_command) = python_venv_activate_command { + this.activate_python_virtual_environment( + activate_command, + &terminal_handle, + cx, + ); + } + terminal_handle + }) + })?; + + terminal + }) } - pub fn python_venv_directory( + fn python_venv_directory( &self, - abs_path: &Path, - settings: &TerminalSettings, - cx: &AppContext, - ) -> Option { - let venv_settings = settings.detect_venv.as_option()?; - if let Some(path) = self.find_venv_in_worktree(abs_path, &venv_settings, cx) { - return Some(path); - } - self.find_venv_on_filesystem(abs_path, &venv_settings, cx) + abs_path: Arc, + venv_settings: VenvSettings, + cx: &ModelContext, + ) -> Task> { + cx.spawn(move |this, mut cx| async move { + if let Some((worktree, _)) = this + .update(&mut cx, |this, cx| this.find_worktree(&abs_path, cx)) + .ok()? + { + let toolchain = this + .update(&mut cx, |this, cx| { + this.active_toolchain( + worktree.read(cx).id(), + LanguageName::new("Python"), + cx, + ) + }) + .ok()? + .await; + + if let Some(toolchain) = toolchain { + let toolchain_path = Path::new(toolchain.path.as_ref()); + return Some(toolchain_path.parent()?.parent()?.to_path_buf()); + } + } + let venv_settings = venv_settings.as_option()?; + this.update(&mut cx, move |this, cx| { + if let Some(path) = this.find_venv_in_worktree(&abs_path, &venv_settings, cx) { + return Some(path); + } + this.find_venv_on_filesystem(&abs_path, &venv_settings, cx) + }) + .ok() + .flatten() + }) } fn find_venv_in_worktree( @@ -337,9 +385,9 @@ impl Project { fn python_activate_command( &self, venv_base_directory: &Path, - settings: &TerminalSettings, + venv_settings: &VenvSettings, ) -> Option { - let venv_settings = settings.detect_venv.as_option()?; + let venv_settings = venv_settings.as_option()?; let activate_keyword = match venv_settings.activate_script { terminal_settings::ActivateScript::Default => match std::env::consts::OS { "windows" => ".", @@ -441,7 +489,7 @@ pub fn wrap_for_ssh( (program, args) } -fn add_environment_path(env: &mut HashMap, new_path: &Path) -> anyhow::Result<()> { +fn add_environment_path(env: &mut HashMap, new_path: &Path) -> Result<()> { let mut env_paths = vec![new_path.to_path_buf()]; if let Some(path) = env.get("PATH").or(env::var("PATH").ok().as_ref()) { let mut paths = std::env::split_paths(&path).collect::>(); diff --git a/crates/terminal/src/terminal_settings.rs b/crates/terminal/src/terminal_settings.rs index 842f00ad9f..760eb14b21 100644 --- a/crates/terminal/src/terminal_settings.rs +++ b/crates/terminal/src/terminal_settings.rs @@ -24,7 +24,7 @@ pub struct Toolbar { pub breadcrumbs: bool, } -#[derive(Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize)] pub struct TerminalSettings { pub shell: Shell, pub working_directory: WorkingDirectory, diff --git a/crates/terminal_view/src/persistence.rs b/crates/terminal_view/src/persistence.rs index dd430963d2..d410ef6d72 100644 --- a/crates/terminal_view/src/persistence.rs +++ b/crates/terminal_view/src/persistence.rs @@ -5,7 +5,7 @@ use futures::{stream::FuturesUnordered, StreamExt as _}; use gpui::{AsyncWindowContext, Axis, Model, Task, View, WeakView}; use project::{terminals::TerminalKind, Project}; use serde::{Deserialize, Serialize}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use ui::{Pixels, ViewContext, VisualContext as _, WindowContext}; use util::ResultExt as _; @@ -219,33 +219,39 @@ async fn deserialize_pane_group( }) .log_err()?; let active_item = serialized_pane.active_item; - pane.update(cx, |pane, cx| { - populate_pane_items(pane, new_items, active_item, cx); - // Avoid blank panes in splits - if pane.items_len() == 0 { - let working_directory = workspace - .update(cx, |workspace, cx| default_working_directory(workspace, cx)) - .ok() - .flatten(); - let kind = TerminalKind::Shell(working_directory); - let window = cx.window_handle(); - let terminal = project - .update(cx, |project, cx| project.create_terminal(kind, window, cx)) - .log_err()?; + + let terminal = pane + .update(cx, |pane, cx| { + populate_pane_items(pane, new_items, active_item, cx); + // Avoid blank panes in splits + if pane.items_len() == 0 { + let working_directory = workspace + .update(cx, |workspace, cx| default_working_directory(workspace, cx)) + .ok() + .flatten(); + let kind = TerminalKind::Shell( + working_directory.as_deref().map(Path::to_path_buf), + ); + let window = cx.window_handle(); + let terminal = project + .update(cx, |project, cx| project.create_terminal(kind, window, cx)); + Some(Some(terminal)) + } else { + Some(None) + } + }) + .ok() + .flatten()?; + if let Some(terminal) = terminal { + let terminal = terminal.await.ok()?; + pane.update(cx, |pane, cx| { let terminal_view = Box::new(cx.new_view(|cx| { - TerminalView::new( - terminal.clone(), - workspace.clone(), - Some(workspace_id), - cx, - ) + TerminalView::new(terminal, workspace.clone(), Some(workspace_id), cx) })); pane.add_item(terminal_view, true, false, None, cx); - } - Some(()) - }) - .ok() - .flatten()?; + }) + .ok()?; + } Some((Member::Pane(pane.clone()), active.then_some(pane))) } } diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index b3804354c4..bbe25b8a92 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -318,10 +318,19 @@ impl TerminalPanel { } } pane::Event::Split(direction) => { - let Some(new_pane) = self.new_pane_with_cloned_active_terminal(cx) else { - return; - }; - self.center.split(&pane, &new_pane, *direction).log_err(); + let new_pane = self.new_pane_with_cloned_active_terminal(cx); + let pane = pane.clone(); + let direction = *direction; + cx.spawn(move |this, mut cx| async move { + let Some(new_pane) = new_pane.await else { + return; + }; + this.update(&mut cx, |this, _| { + this.center.split(&pane, &new_pane, direction).log_err(); + }) + .ok(); + }) + .detach(); } pane::Event::Focus => { self.active_pane = pane.clone(); @@ -334,8 +343,12 @@ impl TerminalPanel { fn new_pane_with_cloned_active_terminal( &mut self, cx: &mut ViewContext, - ) -> Option> { - let workspace = self.workspace.clone().upgrade()?; + ) -> Task>> { + let Some(workspace) = self.workspace.clone().upgrade() else { + return Task::ready(None); + }; + let database_id = workspace.read(cx).database_id(); + let weak_workspace = self.workspace.clone(); let project = workspace.read(cx).project().clone(); let working_directory = self .active_pane @@ -352,21 +365,37 @@ impl TerminalPanel { .or_else(|| default_working_directory(workspace.read(cx), cx)); let kind = TerminalKind::Shell(working_directory); let window = cx.window_handle(); - let terminal = project - .update(cx, |project, cx| project.create_terminal(kind, window, cx)) - .log_err()?; - let database_id = workspace.read(cx).database_id(); - let terminal_view = Box::new(cx.new_view(|cx| { - TerminalView::new(terminal.clone(), self.workspace.clone(), database_id, cx) - })); - let pane = new_terminal_pane(self.workspace.clone(), project, cx); - self.apply_tab_bar_buttons(&pane, cx); - pane.update(cx, |pane, cx| { - pane.add_item(terminal_view, true, true, None, cx); - }); - cx.focus_view(&pane); + cx.spawn(move |this, mut cx| async move { + let terminal = project + .update(&mut cx, |project, cx| { + project.create_terminal(kind, window, cx) + }) + .log_err()? + .await + .log_err()?; - Some(pane) + let terminal_view = Box::new( + cx.new_view(|cx| { + TerminalView::new(terminal.clone(), weak_workspace.clone(), database_id, cx) + }) + .ok()?, + ); + let pane = this + .update(&mut cx, |this, cx| { + let pane = new_terminal_pane(weak_workspace, project, cx); + this.apply_tab_bar_buttons(&pane, cx); + pane + }) + .ok()?; + + pane.update(&mut cx, |pane, cx| { + pane.add_item(terminal_view, true, true, None, cx); + }) + .ok()?; + cx.focus_view(&pane).ok()?; + + Some(pane) + }) } pub fn open_terminal( @@ -489,43 +518,58 @@ impl TerminalPanel { .last() .expect("covered no terminals case above") .clone(); - if allow_concurrent_runs { - debug_assert!( - !use_new_terminal, - "Should have handled 'allow_concurrent_runs && use_new_terminal' case above" - ); - self.replace_terminal( - spawn_task, - task_pane, - existing_item_index, - existing_terminal, - cx, - ); - } else { - self.deferred_tasks.insert( - spawn_in_terminal.id.clone(), - cx.spawn(|terminal_panel, mut cx| async move { - wait_for_terminals_tasks(terminals_for_task, &mut cx).await; - terminal_panel - .update(&mut cx, |terminal_panel, cx| { - if use_new_terminal { - terminal_panel - .spawn_in_new_terminal(spawn_task, cx) - .detach_and_log_err(cx); - } else { - terminal_panel.replace_terminal( - spawn_task, - task_pane, - existing_item_index, - existing_terminal, - cx, - ); - } - }) - .ok(); - }), - ); - } + let id = spawn_in_terminal.id.clone(); + cx.spawn(move |this, mut cx| async move { + if allow_concurrent_runs { + debug_assert!( + !use_new_terminal, + "Should have handled 'allow_concurrent_runs && use_new_terminal' case above" + ); + this.update(&mut cx, |this, cx| { + this.replace_terminal( + spawn_task, + task_pane, + existing_item_index, + existing_terminal, + cx, + ) + })? + .await; + } else { + this.update(&mut cx, |this, cx| { + this.deferred_tasks.insert( + id, + cx.spawn(|terminal_panel, mut cx| async move { + wait_for_terminals_tasks(terminals_for_task, &mut cx).await; + let Ok(Some(new_terminal_task)) = + terminal_panel.update(&mut cx, |terminal_panel, cx| { + if use_new_terminal { + terminal_panel + .spawn_in_new_terminal(spawn_task, cx) + .detach_and_log_err(cx); + None + } else { + Some(terminal_panel.replace_terminal( + spawn_task, + task_pane, + existing_item_index, + existing_terminal, + cx, + )) + } + }) + else { + return; + }; + new_terminal_task.await; + }), + ); + }) + .ok(); + } + anyhow::Result::<_, anyhow::Error>::Ok(()) + }) + .detach() } pub fn spawn_in_new_terminal( @@ -611,11 +655,14 @@ impl TerminalPanel { cx.spawn(|terminal_panel, mut cx| async move { let pane = terminal_panel.update(&mut cx, |this, _| this.active_pane.clone())?; + let project = workspace.update(&mut cx, |workspace, _| workspace.project().clone())?; + let window = cx.window_handle(); + let terminal = project + .update(&mut cx, |project, cx| { + project.create_terminal(kind, window, cx) + })? + .await?; let result = workspace.update(&mut cx, |workspace, cx| { - let window = cx.window_handle(); - let terminal = workspace - .project() - .update(cx, |project, cx| project.create_terminal(kind, window, cx))?; let terminal_view = Box::new(cx.new_view(|cx| { TerminalView::new( terminal.clone(), @@ -695,48 +742,64 @@ impl TerminalPanel { terminal_item_index: usize, terminal_to_replace: View, cx: &mut ViewContext<'_, Self>, - ) -> Option<()> { - let project = self - .workspace - .update(cx, |workspace, _| workspace.project().clone()) - .ok()?; - + ) -> Task> { let reveal = spawn_task.reveal; let window = cx.window_handle(); - let new_terminal = project.update(cx, |project, cx| { - project - .create_terminal(TerminalKind::Task(spawn_task), window, cx) - .log_err() - })?; - terminal_to_replace.update(cx, |terminal_to_replace, cx| { - terminal_to_replace.set_terminal(new_terminal, cx); - }); - - match reveal { - RevealStrategy::Always => { - self.activate_terminal_view(&task_pane, terminal_item_index, true, cx); - let task_workspace = self.workspace.clone(); - cx.spawn(|_, mut cx| async move { - task_workspace - .update(&mut cx, |workspace, cx| workspace.focus_panel::(cx)) + let task_workspace = self.workspace.clone(); + cx.spawn(move |this, mut cx| async move { + let project = this + .update(&mut cx, |this, cx| { + this.workspace + .update(cx, |workspace, _| workspace.project().clone()) .ok() }) - .detach(); - } - RevealStrategy::NoFocus => { - self.activate_terminal_view(&task_pane, terminal_item_index, false, cx); - let task_workspace = self.workspace.clone(); - cx.spawn(|_, mut cx| async move { - task_workspace - .update(&mut cx, |workspace, cx| workspace.open_panel::(cx)) - .ok() + .ok() + .flatten()?; + let new_terminal = project + .update(&mut cx, |project, cx| { + project.create_terminal(TerminalKind::Task(spawn_task), window, cx) }) - .detach(); - } - RevealStrategy::Never => {} - } + .ok()? + .await + .log_err()?; + terminal_to_replace + .update(&mut cx, |terminal_to_replace, cx| { + terminal_to_replace.set_terminal(new_terminal, cx); + }) + .ok()?; - Some(()) + match reveal { + RevealStrategy::Always => { + this.update(&mut cx, |this, cx| { + this.activate_terminal_view(&task_pane, terminal_item_index, true, cx) + }) + .ok()?; + + cx.spawn(|mut cx| async move { + task_workspace + .update(&mut cx, |workspace, cx| workspace.focus_panel::(cx)) + .ok() + }) + .detach(); + } + RevealStrategy::NoFocus => { + this.update(&mut cx, |this, cx| { + this.activate_terminal_view(&task_pane, terminal_item_index, false, cx) + }) + .ok()?; + + cx.spawn(|mut cx| async move { + task_workspace + .update(&mut cx, |workspace, cx| workspace.open_panel::(cx)) + .ok() + }) + .detach(); + } + RevealStrategy::Never => {} + } + + Some(()) + }) } fn has_no_terminals(&self, cx: &WindowContext) -> bool { @@ -998,18 +1061,18 @@ impl Render for TerminalPanel { if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) { cx.focus_view(&pane); } else { - if let Some(new_pane) = - terminal_panel.new_pane_with_cloned_active_terminal(cx) - { - terminal_panel - .center - .split( - &terminal_panel.active_pane, - &new_pane, - SplitDirection::Right, - ) - .log_err(); - } + let new_pane = terminal_panel.new_pane_with_cloned_active_terminal(cx); + cx.spawn(|this, mut cx| async move { + if let Some(new_pane) = new_pane.await { + this.update(&mut cx, |this, _| { + this.center + .split(&this.active_pane, &new_pane, SplitDirection::Right) + .log_err(); + }) + .ok(); + } + }) + .detach(); } })) .on_action(cx.listener( diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 44e97122b8..7a83e530fe 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -136,24 +136,36 @@ impl TerminalView { let working_directory = default_working_directory(workspace, cx); let window = cx.window_handle(); - let terminal = workspace - .project() - .update(cx, |project, cx| { - project.create_terminal(TerminalKind::Shell(working_directory), window, cx) - }) - .notify_err(workspace, cx); + let project = workspace.project().downgrade(); + cx.spawn(move |workspace, mut cx| async move { + let terminal = project + .update(&mut cx, |project, cx| { + project.create_terminal(TerminalKind::Shell(working_directory), window, cx) + }) + .ok()? + .await; + let terminal = workspace + .update(&mut cx, |workspace, cx| terminal.notify_err(workspace, cx)) + .ok() + .flatten()?; - if let Some(terminal) = terminal { - let view = cx.new_view(|cx| { - TerminalView::new( - terminal, - workspace.weak_handle(), - workspace.database_id(), - cx, - ) - }); - workspace.add_item_to_active_pane(Box::new(view), None, true, cx); - } + workspace + .update(&mut cx, |workspace, cx| { + let view = cx.new_view(|cx| { + TerminalView::new( + terminal, + workspace.weak_handle(), + workspace.database_id(), + cx, + ) + }); + workspace.add_item_to_active_pane(Box::new(view), None, true, cx); + }) + .ok(); + + Some(()) + }) + .detach() } pub fn new( @@ -1231,9 +1243,11 @@ impl SerializableItem for TerminalView { .ok() .flatten(); - let terminal = project.update(&mut cx, |project, cx| { - project.create_terminal(TerminalKind::Shell(cwd), window, cx) - })??; + let terminal = project + .update(&mut cx, |project, cx| { + project.create_terminal(TerminalKind::Shell(cwd), window, cx) + })? + .await?; cx.update(|cx| { cx.new_view(|cx| TerminalView::new(terminal, workspace, Some(workspace_id), cx)) }) @@ -1362,11 +1376,14 @@ impl SearchableItem for TerminalView { ///Gets the working directory for the given workspace, respecting the user's settings. /// None implies "~" on whichever machine we end up on. -pub fn default_working_directory(workspace: &Workspace, cx: &AppContext) -> Option { +pub(crate) fn default_working_directory(workspace: &Workspace, cx: &AppContext) -> Option { match &TerminalSettings::get_global(cx).working_directory { - WorkingDirectory::CurrentProjectDirectory => { - workspace.project().read(cx).active_project_directory(cx) - } + WorkingDirectory::CurrentProjectDirectory => workspace + .project() + .read(cx) + .active_project_directory(cx) + .as_deref() + .map(Path::to_path_buf), WorkingDirectory::FirstProjectDirectory => first_project_directory(workspace, cx), WorkingDirectory::AlwaysHome => None, WorkingDirectory::Always { directory } => { From a0f2c0799ebdfdac2c45e0b288016ff29d14fa0e Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 3 Dec 2024 17:27:59 +0200 Subject: [PATCH 252/886] Debounce diagnostics status bar updates (#21463) Closes https://github.com/zed-industries/zed/pull/20797 Release Notes: - Fixed diagnostics status bar flashing when typing --- crates/diagnostics/src/items.rs | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs index 495987c516..f102be37fd 100644 --- a/crates/diagnostics/src/items.rs +++ b/crates/diagnostics/src/items.rs @@ -1,7 +1,9 @@ +use std::time::Duration; + use editor::Editor; use gpui::{ - EventEmitter, IntoElement, ParentElement, Render, Styled, Subscription, View, ViewContext, - WeakView, + EventEmitter, IntoElement, ParentElement, Render, Styled, Subscription, Task, View, + ViewContext, WeakView, }; use language::Diagnostic; use ui::{h_flex, prelude::*, Button, ButtonLike, Color, Icon, IconName, Label, Tooltip}; @@ -15,6 +17,7 @@ pub struct DiagnosticIndicator { workspace: WeakView, current_diagnostic: Option, _observe_active_editor: Option, + diagnostics_update: Task<()>, } impl Render for DiagnosticIndicator { @@ -126,6 +129,7 @@ impl DiagnosticIndicator { workspace: workspace.weak_handle(), current_diagnostic: None, _observe_active_editor: None, + diagnostics_update: Task::ready(()), } } @@ -149,8 +153,17 @@ impl DiagnosticIndicator { .min_by_key(|entry| (entry.diagnostic.severity, entry.range.len())) .map(|entry| entry.diagnostic); if new_diagnostic != self.current_diagnostic { - self.current_diagnostic = new_diagnostic; - cx.notify(); + self.diagnostics_update = cx.spawn(|diagnostics_indicator, mut cx| async move { + cx.background_executor() + .timer(Duration::from_millis(50)) + .await; + diagnostics_indicator + .update(&mut cx, |diagnostics_indicator, cx| { + diagnostics_indicator.current_diagnostic = new_diagnostic; + cx.notify(); + }) + .ok(); + }); } } } From a464474df017dd42f554b401d5775c1b1b1c26a2 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 3 Dec 2024 18:41:36 +0200 Subject: [PATCH 253/886] Properly handle opening of file-less excerpts (#21465) Follow-up of https://github.com/zed-industries/zed/pull/20491 and https://github.com/zed-industries/zed/pull/20469 Closes https://github.com/zed-industries/zed/issues/21369 Release Notes: - Fixed file-less excerpts always opening instead of activating --- crates/editor/src/editor.rs | 37 +++++++++++++++++++++++++++++-- crates/editor/src/editor_tests.rs | 2 +- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 82b27d6f22..1e47eb46a8 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -12833,8 +12833,41 @@ impl Editor { }; for (buffer, (ranges, scroll_offset)) in new_selections_by_buffer { - let editor = - workspace.open_project_item::(pane.clone(), buffer, true, true, cx); + let editor = buffer + .read(cx) + .file() + .is_none() + .then(|| { + // Handle file-less buffers separately: those are not really the project items, so won't have a paroject path or entity id, + // so `workspace.open_project_item` will never find them, always opening a new editor. + // Instead, we try to activate the existing editor in the pane first. + let (editor, pane_item_index) = + pane.read(cx).items().enumerate().find_map(|(i, item)| { + let editor = item.downcast::()?; + let singleton_buffer = + editor.read(cx).buffer().read(cx).as_singleton()?; + if singleton_buffer == buffer { + Some((editor, i)) + } else { + None + } + })?; + pane.update(cx, |pane, cx| { + pane.activate_item(pane_item_index, true, true, cx) + }); + Some(editor) + }) + .flatten() + .unwrap_or_else(|| { + workspace.open_project_item::( + pane.clone(), + buffer, + true, + true, + cx, + ) + }); + editor.update(cx, |editor, cx| { let autoscroll = match scroll_offset { Some(scroll_offset) => Autoscroll::top_relative(scroll_offset as usize), diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 5134b512ff..044e2765ed 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -11805,7 +11805,7 @@ async fn test_mutlibuffer_in_navigation_history(cx: &mut gpui::TestAppContext) { multi_buffer_editor.update(cx, |editor, cx| { editor.change_selections(Some(Autoscroll::Next), cx, |s| { - s.select_ranges(Some(60..70)) + s.select_ranges(Some(70..70)) }); editor.open_excerpts(&OpenExcerpts, cx); }); From 2dd5138988ada1b57983b5948c7e082150df50c6 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Tue, 3 Dec 2024 16:54:06 +0000 Subject: [PATCH 254/886] docs: Add anchor links for language-specific settings (#21469) --- docs/src/configuring-zed.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index e71266a01f..d4f8c40dbd 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -1335,19 +1335,19 @@ To override settings for a language, add an entry for that languages name to the The following settings can be overridden for each specific language: -- `enable_language_server` -- `ensure_final_newline_on_save` -- `format_on_save` -- `formatter` -- `hard_tabs` -- `preferred_line_length` -- `remove_trailing_whitespace_on_save` -- `show_inline_completions` -- `show_whitespaces` -- `soft_wrap` -- `tab_size` -- `use_autoclose` -- `always_treat_brackets_as_autoclosed` +- [`enable_language_server`](#enable-language-server) +- [`ensure_final_newline_on_save`](#ensure-final-newline-on-save) +- [`format_on_save`](#format-on-save) +- [`formatter`](#formatter) +- [`hard_tabs`](#hard-tabs) +- [`preferred_line_length`](#preferred-line-length) +- [`remove_trailing_whitespace_on_save`](#remove-trailing-whitespace-on-save) +- [`show_inline_completions`](#show-inline-completions) +- [`show_whitespaces`](#show-whitespaces) +- [`soft_wrap`](#soft-wrap) +- [`tab_size`](#tab-size) +- [`use_autoclose`](#use-autoclose) +- [`always_treat_brackets_as_autoclosed`](#always-treat-brackets-as-autoclosed) These values take in the same options as the root-level settings with the same name. From c443307c19f71fef32b05721b03e32db91b1dd34 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 3 Dec 2024 09:26:19 -0800 Subject: [PATCH 255/886] Fix ctrl-alt-X shortcuts (#21473) The macOS input handler assumes that you want to insert control sequences when you type ctrl-alt-X (you probably don't...). Release Notes: - (nightly only) fix ctrl-alt-X shortcuts --- crates/gpui/src/platform/mac/window.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 12a332e9bc..9266f81f74 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -1253,7 +1253,10 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent: // otherwise we only send to the input handler if we don't have a matching binding. // The input handler may call `do_command_by_selector` if it doesn't know how to handle // a key. If it does so, it will return YES so we won't send the key twice. - if is_composing || event.keystroke.key_char.is_none() { + // We also do this for non-printing keys (like arrow keys and escape) as the IME menu + // may need them even if there is no marked text; + // however we skip keys with control or the input handler adds control-characters to the buffer. + if is_composing || (event.keystroke.key_char.is_none() && !event.keystroke.modifiers.control) { { let mut lock = window_state.as_ref().lock(); lock.keystroke_for_do_command = Some(event.keystroke.clone()); From 75c9dc179bb3db89915666baf56e5362761cd97c Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 3 Dec 2024 09:37:01 -0800 Subject: [PATCH 256/886] Add textobjects queries (#20924) Co-Authored-By: Max Release Notes: - vim: Added motions `[[`, `[]`, `]]`, `][` for navigating by section, `[m`, `]m`, `[M`, `]M` for navigating by method, and `[*`, `]*`, `[/`, `]/` for comments. These currently only work for languages built in to Zed, as they are powered by new tree-sitter queries. - vim: Added new text objects: `ic`, `ac` for inside/around classes, `if`,`af` for functions/methods, and `g c` for comments. These currently only work for languages built in to Zed, as they are powered by new tree-sitter queries. --------- Co-authored-by: Max --- Cargo.lock | 12 +- assets/keymaps/vim.json | 19 +- crates/language/src/buffer.rs | 69 +++- crates/language/src/buffer_tests.rs | 48 +++ crates/language/src/language.rs | 69 +++- crates/language/src/language_registry.rs | 2 + crates/language/src/syntax_map.rs | 31 ++ crates/languages/src/bash/textobjects.scm | 7 + crates/languages/src/c/textobjects.scm | 25 ++ crates/languages/src/cpp/textobjects.scm | 31 ++ crates/languages/src/css/textobjects.scm | 30 ++ crates/languages/src/go/textobjects.scm | 25 ++ .../languages/src/javascript/textobjects.scm | 51 +++ crates/languages/src/json/textobjects.scm | 1 + crates/languages/src/jsonc/textobjects.scm | 1 + crates/languages/src/markdown/textobjects.scm | 3 + crates/languages/src/python/textobjects.scm | 7 + crates/languages/src/rust/outline.scm | 6 +- crates/languages/src/rust/textobjects.scm | 51 +++ crates/languages/src/tsx/textobjects.scm | 79 ++++ .../languages/src/typescript/textobjects.scm | 79 ++++ crates/languages/src/yaml/textobjects.scm | 1 + crates/multi_buffer/src/multi_buffer.rs | 44 +++ crates/vim/src/motion.rs | 348 ++++++++++++++++++ crates/vim/src/object.rs | 112 +++++- crates/vim/src/visual.rs | 2 +- docs/src/extensions/languages.md | 39 ++ docs/src/vim.md | 39 +- 28 files changed, 1205 insertions(+), 26 deletions(-) create mode 100644 crates/languages/src/bash/textobjects.scm create mode 100644 crates/languages/src/c/textobjects.scm create mode 100644 crates/languages/src/cpp/textobjects.scm create mode 100644 crates/languages/src/css/textobjects.scm create mode 100644 crates/languages/src/go/textobjects.scm create mode 100644 crates/languages/src/javascript/textobjects.scm create mode 100644 crates/languages/src/json/textobjects.scm create mode 100644 crates/languages/src/jsonc/textobjects.scm create mode 100644 crates/languages/src/markdown/textobjects.scm create mode 100644 crates/languages/src/python/textobjects.scm create mode 100644 crates/languages/src/rust/textobjects.scm create mode 100644 crates/languages/src/tsx/textobjects.scm create mode 100644 crates/languages/src/typescript/textobjects.scm create mode 100644 crates/languages/src/yaml/textobjects.scm diff --git a/Cargo.lock b/Cargo.lock index d21006ee55..1bd064ca4c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3416,9 +3416,9 @@ dependencies = [ [[package]] name = "ctor" -version = "0.2.9" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" dependencies = [ "quote", "syn 2.0.87", @@ -6789,9 +6789,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "libc" -version = "0.2.164" +version = "0.2.162" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f" +checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398" [[package]] name = "libdbus-sys" @@ -10956,9 +10956,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.133" +version = "1.0.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" dependencies = [ "indexmap 2.6.0", "itoa", diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index b2ef7f2c18..5f5933ef63 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -33,6 +33,18 @@ "(": "vim::SentenceBackward", ")": "vim::SentenceForward", "|": "vim::GoToColumn", + "] ]": "vim::NextSectionStart", + "] [": "vim::NextSectionEnd", + "[ [": "vim::PreviousSectionStart", + "[ ]": "vim::PreviousSectionEnd", + "] m": "vim::NextMethodStart", + "] M": "vim::NextMethodEnd", + "[ m": "vim::PreviousMethodStart", + "[ M": "vim::PreviousMethodEnd", + "[ *": "vim::PreviousComment", + "[ /": "vim::PreviousComment", + "] *": "vim::NextComment", + "] /": "vim::NextComment", // Word motions "w": "vim::NextWordStart", "e": "vim::NextWordEnd", @@ -360,7 +372,8 @@ "bindings": { "escape": "vim::ClearOperators", "ctrl-c": "vim::ClearOperators", - "ctrl-[": "vim::ClearOperators" + "ctrl-[": "vim::ClearOperators", + "g c": "vim::Comment" } }, { @@ -389,7 +402,9 @@ ">": "vim::AngleBrackets", "a": "vim::Argument", "i": "vim::IndentObj", - "shift-i": ["vim::IndentObj", { "includeBelow": true }] + "shift-i": ["vim::IndentObj", { "includeBelow": true }], + "f": "vim::Method", + "c": "vim::Class" } }, { diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index a03357c1d4..f3b6cb51ad 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -14,7 +14,8 @@ use crate::{ SyntaxMapMatches, SyntaxSnapshot, ToTreeSitterPoint, }, task_context::RunnableRange, - LanguageScope, Outline, OutlineConfig, RunnableCapture, RunnableTag, + LanguageScope, Outline, OutlineConfig, RunnableCapture, RunnableTag, TextObject, + TreeSitterOptions, }; use anyhow::{anyhow, Context, Result}; use async_watch as watch; @@ -3412,6 +3413,72 @@ impl BufferSnapshot { }) } + pub fn text_object_ranges( + &self, + range: Range, + options: TreeSitterOptions, + ) -> impl Iterator, TextObject)> + '_ { + let range = range.start.to_offset(self).saturating_sub(1) + ..self.len().min(range.end.to_offset(self) + 1); + + let mut matches = + self.syntax + .matches_with_options(range.clone(), &self.text, options, |grammar| { + grammar.text_object_config.as_ref().map(|c| &c.query) + }); + + let configs = matches + .grammars() + .iter() + .map(|grammar| grammar.text_object_config.as_ref()) + .collect::>(); + + let mut captures = Vec::<(Range, TextObject)>::new(); + + iter::from_fn(move || loop { + while let Some(capture) = captures.pop() { + if capture.0.overlaps(&range) { + return Some(capture); + } + } + + let mat = matches.peek()?; + + let Some(config) = configs[mat.grammar_index].as_ref() else { + matches.advance(); + continue; + }; + + for capture in mat.captures { + let Some(ix) = config + .text_objects_by_capture_ix + .binary_search_by_key(&capture.index, |e| e.0) + .ok() + else { + continue; + }; + let text_object = config.text_objects_by_capture_ix[ix].1; + let byte_range = capture.node.byte_range(); + + let mut found = false; + for (range, existing) in captures.iter_mut() { + if existing == &text_object { + range.start = range.start.min(byte_range.start); + range.end = range.end.max(byte_range.end); + found = true; + break; + } + } + + if !found { + captures.push((byte_range, text_object)); + } + } + + matches.advance(); + }) + } + /// Returns enclosing bracket ranges containing the given range pub fn enclosing_bracket_ranges( &self, diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index a33a21cb0f..3eab3aaed7 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -20,6 +20,7 @@ use std::{ sync::LazyLock, time::{Duration, Instant}, }; +use syntax_map::TreeSitterOptions; use text::network::Network; use text::{BufferId, LineEnding, LineIndent}; use text::{Point, ToPoint}; @@ -915,6 +916,39 @@ async fn test_symbols_containing(cx: &mut gpui::TestAppContext) { } } +#[gpui::test] +fn test_text_objects(cx: &mut AppContext) { + let (text, ranges) = marked_text_ranges( + indoc! {r#" + impl Hello { + fn say() -> u8 { return /* ˇhi */ 1 } + }"# + }, + false, + ); + + let buffer = + cx.new_model(|cx| Buffer::local(text.clone(), cx).with_language(Arc::new(rust_lang()), cx)); + let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot()); + + let matches = snapshot + .text_object_ranges(ranges[0].clone(), TreeSitterOptions::default()) + .map(|(range, text_object)| (&text[range], text_object)) + .collect::>(); + + assert_eq!( + matches, + &[ + ("/* hi */", TextObject::AroundComment), + ("return /* hi */ 1", TextObject::InsideFunction), + ( + "fn say() -> u8 { return /* hi */ 1 }", + TextObject::AroundFunction + ), + ], + ) +} + #[gpui::test] fn test_enclosing_bracket_ranges(cx: &mut AppContext) { let mut assert = |selection_text, range_markers| { @@ -3182,6 +3216,20 @@ fn rust_lang() -> Language { "#, ) .unwrap() + .with_text_object_query( + r#" + (function_item + body: (_ + "{" + (_)* @function.inside + "}" )) @function.around + + (line_comment)+ @comment.around + + (block_comment) @comment.around + "#, + ) + .unwrap() .with_outline_query( r#" (line_comment) @annotation diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index e9590448f8..e0cd392131 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -78,7 +78,7 @@ pub use language_registry::{ }; pub use lsp::LanguageServerId; pub use outline::*; -pub use syntax_map::{OwnedSyntaxLayer, SyntaxLayer}; +pub use syntax_map::{OwnedSyntaxLayer, SyntaxLayer, TreeSitterOptions}; pub use text::{AnchorRangeExt, LineEnding}; pub use tree_sitter::{Node, Parser, Tree, TreeCursor}; @@ -848,6 +848,7 @@ pub struct Grammar { pub(crate) runnable_config: Option, pub(crate) indents_config: Option, pub outline_config: Option, + pub text_object_config: Option, pub embedding_config: Option, pub(crate) injection_config: Option, pub(crate) override_config: Option, @@ -873,6 +874,44 @@ pub struct OutlineConfig { pub annotation_capture_ix: Option, } +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum TextObject { + InsideFunction, + AroundFunction, + InsideClass, + AroundClass, + InsideComment, + AroundComment, +} + +impl TextObject { + pub fn from_capture_name(name: &str) -> Option { + match name { + "function.inside" => Some(TextObject::InsideFunction), + "function.around" => Some(TextObject::AroundFunction), + "class.inside" => Some(TextObject::InsideClass), + "class.around" => Some(TextObject::AroundClass), + "comment.inside" => Some(TextObject::InsideComment), + "comment.around" => Some(TextObject::AroundComment), + _ => None, + } + } + + pub fn around(&self) -> Option { + match self { + TextObject::InsideFunction => Some(TextObject::AroundFunction), + TextObject::InsideClass => Some(TextObject::AroundClass), + TextObject::InsideComment => Some(TextObject::AroundComment), + _ => None, + } + } +} + +pub struct TextObjectConfig { + pub query: Query, + pub text_objects_by_capture_ix: Vec<(u32, TextObject)>, +} + #[derive(Debug)] pub struct EmbeddingConfig { pub query: Query, @@ -950,6 +989,7 @@ impl Language { highlights_query: None, brackets_config: None, outline_config: None, + text_object_config: None, embedding_config: None, indents_config: None, injection_config: None, @@ -1020,7 +1060,12 @@ impl Language { if let Some(query) = queries.runnables { self = self .with_runnable_query(query.as_ref()) - .context("Error loading tests query")?; + .context("Error loading runnables query")?; + } + if let Some(query) = queries.text_objects { + self = self + .with_text_object_query(query.as_ref()) + .context("Error loading textobject query")?; } Ok(self) } @@ -1097,6 +1142,26 @@ impl Language { Ok(self) } + pub fn with_text_object_query(mut self, source: &str) -> Result { + let grammar = self + .grammar_mut() + .ok_or_else(|| anyhow!("cannot mutate grammar"))?; + let query = Query::new(&grammar.ts_language, source)?; + + let mut text_objects_by_capture_ix = Vec::new(); + for (ix, name) in query.capture_names().iter().enumerate() { + if let Some(text_object) = TextObject::from_capture_name(name) { + text_objects_by_capture_ix.push((ix as u32, text_object)); + } + } + + grammar.text_object_config = Some(TextObjectConfig { + query, + text_objects_by_capture_ix, + }); + Ok(self) + } + pub fn with_embedding_query(mut self, source: &str) -> Result { let grammar = self .grammar_mut() diff --git a/crates/language/src/language_registry.rs b/crates/language/src/language_registry.rs index e5f7815351..794ab0784e 100644 --- a/crates/language/src/language_registry.rs +++ b/crates/language/src/language_registry.rs @@ -181,6 +181,7 @@ pub const QUERY_FILENAME_PREFIXES: &[( ("overrides", |q| &mut q.overrides), ("redactions", |q| &mut q.redactions), ("runnables", |q| &mut q.runnables), + ("textobjects", |q| &mut q.text_objects), ]; /// Tree-sitter language queries for a given language. @@ -195,6 +196,7 @@ pub struct LanguageQueries { pub overrides: Option>, pub redactions: Option>, pub runnables: Option>, + pub text_objects: Option>, } #[derive(Clone, Default)] diff --git a/crates/language/src/syntax_map.rs b/crates/language/src/syntax_map.rs index 1208925542..76c6dc75e3 100644 --- a/crates/language/src/syntax_map.rs +++ b/crates/language/src/syntax_map.rs @@ -814,6 +814,23 @@ impl SyntaxSnapshot { buffer.as_rope(), self.layers_for_range(range, buffer, true), query, + TreeSitterOptions::default(), + ) + } + + pub fn matches_with_options<'a>( + &'a self, + range: Range, + buffer: &'a BufferSnapshot, + options: TreeSitterOptions, + query: fn(&Grammar) -> Option<&Query>, + ) -> SyntaxMapMatches<'a> { + SyntaxMapMatches::new( + range.clone(), + buffer.as_rope(), + self.layers_for_range(range, buffer, true), + query, + options, ) } @@ -1001,12 +1018,25 @@ impl<'a> SyntaxMapCaptures<'a> { } } +#[derive(Default)] +pub struct TreeSitterOptions { + max_start_depth: Option, +} +impl TreeSitterOptions { + pub fn max_start_depth(max_start_depth: u32) -> Self { + Self { + max_start_depth: Some(max_start_depth), + } + } +} + impl<'a> SyntaxMapMatches<'a> { fn new( range: Range, text: &'a Rope, layers: impl Iterator>, query: fn(&Grammar) -> Option<&Query>, + options: TreeSitterOptions, ) -> Self { let mut result = Self::default(); for layer in layers { @@ -1027,6 +1057,7 @@ impl<'a> SyntaxMapMatches<'a> { query_cursor.deref_mut(), ) }; + cursor.set_max_start_depth(options.max_start_depth); cursor.set_byte_range(range.clone()); let matches = cursor.matches(query, layer.node(), TextProvider(text)); diff --git a/crates/languages/src/bash/textobjects.scm b/crates/languages/src/bash/textobjects.scm new file mode 100644 index 0000000000..cca2f7d9e9 --- /dev/null +++ b/crates/languages/src/bash/textobjects.scm @@ -0,0 +1,7 @@ +(function_definition + body: (_ + "{" + (_)* @function.inside + "}" )) @function.around + +(comment) @comment.around diff --git a/crates/languages/src/c/textobjects.scm b/crates/languages/src/c/textobjects.scm new file mode 100644 index 0000000000..832dd62288 --- /dev/null +++ b/crates/languages/src/c/textobjects.scm @@ -0,0 +1,25 @@ +(declaration + declarator: (function_declarator)) @function.around + +(function_definition + body: (_ + "{" + (_)* @function.inside + "}" )) @function.around + +(preproc_function_def + value: (_) @function.inside) @function.around + +(comment) @comment.around + +(struct_specifier + body: (_ + "{" + (_)* @class.inside + "}")) @class.around + +(enum_specifier + body: (_ + "{" + [(_) ","?]* @class.inside + "}")) @class.around diff --git a/crates/languages/src/cpp/textobjects.scm b/crates/languages/src/cpp/textobjects.scm new file mode 100644 index 0000000000..11a27b8d58 --- /dev/null +++ b/crates/languages/src/cpp/textobjects.scm @@ -0,0 +1,31 @@ +(declaration + declarator: (function_declarator)) @function.around + +(function_definition + body: (_ + "{" + (_)* @function.inside + "}" )) @function.around + +(preproc_function_def + value: (_) @function.inside) @function.around + +(comment) @comment.around + +(struct_specifier + body: (_ + "{" + (_)* @class.inside + "}")) @class.around + +(enum_specifier + body: (_ + "{" + [(_) ","?]* @class.inside + "}")) @class.around + +(class_specifier + body: (_ + "{" + [(_) ":"? ";"?]* @class.inside + "}"?)) @class.around diff --git a/crates/languages/src/css/textobjects.scm b/crates/languages/src/css/textobjects.scm new file mode 100644 index 0000000000..c9c6207b85 --- /dev/null +++ b/crates/languages/src/css/textobjects.scm @@ -0,0 +1,30 @@ +(comment) @comment.around + +(rule_set + (block ( + "{" + (_)* @function.inside + "}" ))) @function.around +(keyframe_block + (block ( + "{" + (_)* @function.inside + "}" ))) @function.around + +(media_statement + (block ( + "{" + (_)* @class.inside + "}" ))) @class.around + +(supports_statement + (block ( + "{" + (_)* @class.inside + "}" ))) @class.around + +(keyframes_statement + (keyframe_block_list ( + "{" + (_)* @class.inside + "}" ))) @class.around diff --git a/crates/languages/src/go/textobjects.scm b/crates/languages/src/go/textobjects.scm new file mode 100644 index 0000000000..eb4f3a0050 --- /dev/null +++ b/crates/languages/src/go/textobjects.scm @@ -0,0 +1,25 @@ +(function_declaration + body: (_ + "{" + (_)* @function.inside + "}")) @function.around + +(method_declaration + body: (_ + "{" + (_)* @function.inside + "}")) @function.around + +(type_declaration + (type_spec (struct_type (field_declaration_list ( + "{" + (_)* @class.inside + "}")?)))) @class.around + +(type_declaration + (type_spec (interface_type + (_)* @class.inside))) @class.around + +(type_declaration) @class.around + +(comment)+ @comment.around diff --git a/crates/languages/src/javascript/textobjects.scm b/crates/languages/src/javascript/textobjects.scm new file mode 100644 index 0000000000..1a273ddb50 --- /dev/null +++ b/crates/languages/src/javascript/textobjects.scm @@ -0,0 +1,51 @@ +(comment)+ @comment.around + +(function_declaration + body: (_ + "{" + (_)* @function.inside + "}")) @function.around + +(method_definition + body: (_ + "{" + (_)* @function.inside + "}")) @function.around + +(function_expression + body: (_ + "{" + (_)* @function.inside + "}")) @function.around + +(arrow_function + body: (statement_block + "{" + (_)* @function.inside + "}")) @function.around + +(arrow_function) @function.around + +(generator_function + body: (_ + "{" + (_)* @function.inside + "}")) @function.around + +(generator_function_declaration + body: (_ + "{" + (_)* @function.inside + "}")) @function.around + +(class_declaration + body: (_ + "{" + [(_) ";"?]* @class.inside + "}" )) @class.around + +(class + body: (_ + "{" + [(_) ";"?]* @class.inside + "}" )) @class.around diff --git a/crates/languages/src/json/textobjects.scm b/crates/languages/src/json/textobjects.scm new file mode 100644 index 0000000000..81fd20245b --- /dev/null +++ b/crates/languages/src/json/textobjects.scm @@ -0,0 +1 @@ +(comment)+ @comment.around diff --git a/crates/languages/src/jsonc/textobjects.scm b/crates/languages/src/jsonc/textobjects.scm new file mode 100644 index 0000000000..81fd20245b --- /dev/null +++ b/crates/languages/src/jsonc/textobjects.scm @@ -0,0 +1 @@ +(comment)+ @comment.around diff --git a/crates/languages/src/markdown/textobjects.scm b/crates/languages/src/markdown/textobjects.scm new file mode 100644 index 0000000000..e0f76c5365 --- /dev/null +++ b/crates/languages/src/markdown/textobjects.scm @@ -0,0 +1,3 @@ +(section + (atx_heading) + (_)* @class.inside) @class.around diff --git a/crates/languages/src/python/textobjects.scm b/crates/languages/src/python/textobjects.scm new file mode 100644 index 0000000000..abd28ab75a --- /dev/null +++ b/crates/languages/src/python/textobjects.scm @@ -0,0 +1,7 @@ +(comment)+ @comment.around + +(function_definition + body: (_) @function.inside) @function.around + +(class_definition + body: (_) @class.inside) @class.around diff --git a/crates/languages/src/rust/outline.scm b/crates/languages/src/rust/outline.scm index 3012995e2a..4299a01f19 100644 --- a/crates/languages/src/rust/outline.scm +++ b/crates/languages/src/rust/outline.scm @@ -15,11 +15,7 @@ (visibility_modifier)? @context name: (_) @name) @item -(impl_item - "impl" @context - trait: (_)? @name - "for"? @context - type: (_) @name +(function_item body: (_ "{" @open (_)* "}" @close)) @item (trait_item diff --git a/crates/languages/src/rust/textobjects.scm b/crates/languages/src/rust/textobjects.scm new file mode 100644 index 0000000000..4e7e7fa0cd --- /dev/null +++ b/crates/languages/src/rust/textobjects.scm @@ -0,0 +1,51 @@ +; functions +(function_signature_item) @function.around + +(function_item + body: (_ + "{" + (_)* @function.inside + "}" )) @function.around + +; classes +(struct_item + body: (_ + ["{" "("]? + [(_) ","?]* @class.inside + ["}" ")"]? )) @class.around + +(enum_item + body: (_ + "{" + [(_) ","?]* @class.inside + "}" )) @class.around + +(union_item + body: (_ + "{" + [(_) ","?]* @class.inside + "}" )) @class.around + +(trait_item + body: (_ + "{" + [(_) ","?]* @class.inside + "}" )) @class.around + +(impl_item + body: (_ + "{" + [(_) ","?]* @class.inside + "}" )) @class.around + +(mod_item + body: (_ + "{" + [(_) ","?]* @class.inside + "}" )) @class.around + +; comments + +(line_comment)+ @comment.around + +(block_comment) @comment.around diff --git a/crates/languages/src/tsx/textobjects.scm b/crates/languages/src/tsx/textobjects.scm new file mode 100644 index 0000000000..836fed35ba --- /dev/null +++ b/crates/languages/src/tsx/textobjects.scm @@ -0,0 +1,79 @@ +(comment)+ @comment.around + +(function_declaration + body: (_ + "{" + (_)* @function.inside + "}")) @function.around + +(method_definition + body: (_ + "{" + (_)* @function.inside + "}")) @function.around + +(function_expression + body: (_ + "{" + (_)* @function.inside + "}")) @function.around + +(arrow_function + body: (statement_block + "{" + (_)* @function.inside + "}")) @function.around + +(arrow_function) @function.around +(function_signature) @function.around + +(generator_function + body: (_ + "{" + (_)* @function.inside + "}")) @function.around + +(generator_function_declaration + body: (_ + "{" + (_)* @function.inside + "}")) @function.around + +(class_declaration + body: (_ + "{" + [(_) ";"?]* @class.inside + "}" )) @class.around + +(class + body: (_ + "{" + (_)* @class.inside + "}" )) @class.around + +(interface_declaration + body: (_ + "{" + [(_) ";"?]* @class.inside + "}" )) @class.around + +(enum_declaration + body: (_ + "{" + [(_) ","?]* @class.inside + "}" )) @class.around + +(ambient_declaration + (module + body: (_ + "{" + [(_) ";"?]* @class.inside + "}" ))) @class.around + +(internal_module + body: (_ + "{" + [(_) ";"?]* @class.inside + "}" )) @class.around + +(type_alias_declaration) @class.around diff --git a/crates/languages/src/typescript/textobjects.scm b/crates/languages/src/typescript/textobjects.scm new file mode 100644 index 0000000000..836fed35ba --- /dev/null +++ b/crates/languages/src/typescript/textobjects.scm @@ -0,0 +1,79 @@ +(comment)+ @comment.around + +(function_declaration + body: (_ + "{" + (_)* @function.inside + "}")) @function.around + +(method_definition + body: (_ + "{" + (_)* @function.inside + "}")) @function.around + +(function_expression + body: (_ + "{" + (_)* @function.inside + "}")) @function.around + +(arrow_function + body: (statement_block + "{" + (_)* @function.inside + "}")) @function.around + +(arrow_function) @function.around +(function_signature) @function.around + +(generator_function + body: (_ + "{" + (_)* @function.inside + "}")) @function.around + +(generator_function_declaration + body: (_ + "{" + (_)* @function.inside + "}")) @function.around + +(class_declaration + body: (_ + "{" + [(_) ";"?]* @class.inside + "}" )) @class.around + +(class + body: (_ + "{" + (_)* @class.inside + "}" )) @class.around + +(interface_declaration + body: (_ + "{" + [(_) ";"?]* @class.inside + "}" )) @class.around + +(enum_declaration + body: (_ + "{" + [(_) ","?]* @class.inside + "}" )) @class.around + +(ambient_declaration + (module + body: (_ + "{" + [(_) ";"?]* @class.inside + "}" ))) @class.around + +(internal_module + body: (_ + "{" + [(_) ";"?]* @class.inside + "}" )) @class.around + +(type_alias_declaration) @class.around diff --git a/crates/languages/src/yaml/textobjects.scm b/crates/languages/src/yaml/textobjects.scm new file mode 100644 index 0000000000..5262b7e232 --- /dev/null +++ b/crates/languages/src/yaml/textobjects.scm @@ -0,0 +1 @@ +(comment)+ @comment diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index f1434b6d59..461498d00d 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -3441,6 +3441,30 @@ impl MultiBufferSnapshot { }) } + pub fn excerpt_before(&self, id: ExcerptId) -> Option> { + let start_locator = self.excerpt_locator_for_id(id); + let mut cursor = self.excerpts.cursor::>(&()); + cursor.seek(&Some(start_locator), Bias::Left, &()); + cursor.prev(&()); + let excerpt = cursor.item()?; + Some(MultiBufferExcerpt { + excerpt, + excerpt_offset: 0, + }) + } + + pub fn excerpt_after(&self, id: ExcerptId) -> Option> { + let start_locator = self.excerpt_locator_for_id(id); + let mut cursor = self.excerpts.cursor::>(&()); + cursor.seek(&Some(start_locator), Bias::Left, &()); + cursor.next(&()); + let excerpt = cursor.item()?; + Some(MultiBufferExcerpt { + excerpt, + excerpt_offset: 0, + }) + } + pub fn excerpt_boundaries_in_range( &self, range: R, @@ -4689,6 +4713,26 @@ impl<'a> MultiBufferExcerpt<'a> { } } + pub fn id(&self) -> ExcerptId { + self.excerpt.id + } + + pub fn start_anchor(&self) -> Anchor { + Anchor { + buffer_id: Some(self.excerpt.buffer_id), + excerpt_id: self.excerpt.id, + text_anchor: self.excerpt.range.context.start, + } + } + + pub fn end_anchor(&self) -> Anchor { + Anchor { + buffer_id: Some(self.excerpt.buffer_id), + excerpt_id: self.excerpt.id, + text_anchor: self.excerpt.range.context.end, + } + } + pub fn buffer(&self) -> &'a BufferSnapshot { &self.excerpt.buffer } diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 9c770fb63f..eb6e8464a3 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -11,6 +11,7 @@ use language::{CharKind, Point, Selection, SelectionGoal}; use multi_buffer::MultiBufferRow; use serde::Deserialize; use std::ops::Range; +use workspace::searchable::Direction; use crate::{ normal::mark, @@ -104,6 +105,16 @@ pub enum Motion { WindowTop, WindowMiddle, WindowBottom, + NextSectionStart, + NextSectionEnd, + PreviousSectionStart, + PreviousSectionEnd, + NextMethodStart, + NextMethodEnd, + PreviousMethodStart, + PreviousMethodEnd, + NextComment, + PreviousComment, // we don't have a good way to run a search synchronously, so // we handle search motions by running the search async and then @@ -269,6 +280,16 @@ actions!( WindowTop, WindowMiddle, WindowBottom, + NextSectionStart, + NextSectionEnd, + PreviousSectionStart, + PreviousSectionEnd, + NextMethodStart, + NextMethodEnd, + PreviousMethodStart, + PreviousMethodEnd, + NextComment, + PreviousComment, ] ); @@ -454,6 +475,37 @@ pub fn register(editor: &mut Editor, cx: &mut ViewContext) { Vim::action(editor, cx, |vim, &WindowBottom, cx| { vim.motion(Motion::WindowBottom, cx) }); + + Vim::action(editor, cx, |vim, &PreviousSectionStart, cx| { + vim.motion(Motion::PreviousSectionStart, cx) + }); + Vim::action(editor, cx, |vim, &NextSectionStart, cx| { + vim.motion(Motion::NextSectionStart, cx) + }); + Vim::action(editor, cx, |vim, &PreviousSectionEnd, cx| { + vim.motion(Motion::PreviousSectionEnd, cx) + }); + Vim::action(editor, cx, |vim, &NextSectionEnd, cx| { + vim.motion(Motion::NextSectionEnd, cx) + }); + Vim::action(editor, cx, |vim, &PreviousMethodStart, cx| { + vim.motion(Motion::PreviousMethodStart, cx) + }); + Vim::action(editor, cx, |vim, &NextMethodStart, cx| { + vim.motion(Motion::NextMethodStart, cx) + }); + Vim::action(editor, cx, |vim, &PreviousMethodEnd, cx| { + vim.motion(Motion::PreviousMethodEnd, cx) + }); + Vim::action(editor, cx, |vim, &NextMethodEnd, cx| { + vim.motion(Motion::NextMethodEnd, cx) + }); + Vim::action(editor, cx, |vim, &NextComment, cx| { + vim.motion(Motion::NextComment, cx) + }); + Vim::action(editor, cx, |vim, &PreviousComment, cx| { + vim.motion(Motion::PreviousComment, cx) + }); } impl Vim { @@ -536,6 +588,16 @@ impl Motion { | WindowTop | WindowMiddle | WindowBottom + | NextSectionStart + | NextSectionEnd + | PreviousSectionStart + | PreviousSectionEnd + | NextMethodStart + | NextMethodEnd + | PreviousMethodStart + | PreviousMethodEnd + | NextComment + | PreviousComment | Jump { line: true, .. } => true, EndOfLine { .. } | Matching @@ -607,6 +669,16 @@ impl Motion { | NextLineStart | PreviousLineStart | ZedSearchResult { .. } + | NextSectionStart + | NextSectionEnd + | PreviousSectionStart + | PreviousSectionEnd + | NextMethodStart + | NextMethodEnd + | PreviousMethodStart + | PreviousMethodEnd + | NextComment + | PreviousComment | Jump { .. } => false, } } @@ -652,6 +724,16 @@ impl Motion { | FirstNonWhitespace { .. } | FindBackward { .. } | Jump { .. } + | NextSectionStart + | NextSectionEnd + | PreviousSectionStart + | PreviousSectionEnd + | NextMethodStart + | NextMethodEnd + | PreviousMethodStart + | PreviousMethodEnd + | NextComment + | PreviousComment | ZedSearchResult { .. } => false, RepeatFind { last_find: motion } | RepeatFindReversed { last_find: motion } => { motion.inclusive() @@ -867,6 +949,47 @@ impl Motion { return None; } } + NextSectionStart => ( + section_motion(map, point, times, Direction::Next, true), + SelectionGoal::None, + ), + NextSectionEnd => ( + section_motion(map, point, times, Direction::Next, false), + SelectionGoal::None, + ), + PreviousSectionStart => ( + section_motion(map, point, times, Direction::Prev, true), + SelectionGoal::None, + ), + PreviousSectionEnd => ( + section_motion(map, point, times, Direction::Prev, false), + SelectionGoal::None, + ), + + NextMethodStart => ( + method_motion(map, point, times, Direction::Next, true), + SelectionGoal::None, + ), + NextMethodEnd => ( + method_motion(map, point, times, Direction::Next, false), + SelectionGoal::None, + ), + PreviousMethodStart => ( + method_motion(map, point, times, Direction::Prev, true), + SelectionGoal::None, + ), + PreviousMethodEnd => ( + method_motion(map, point, times, Direction::Prev, false), + SelectionGoal::None, + ), + NextComment => ( + comment_motion(map, point, times, Direction::Next), + SelectionGoal::None, + ), + PreviousComment => ( + comment_motion(map, point, times, Direction::Prev), + SelectionGoal::None, + ), }; (new_point != point || infallible).then_some((new_point, goal)) @@ -2129,6 +2252,231 @@ fn window_bottom( } } +fn method_motion( + map: &DisplaySnapshot, + mut display_point: DisplayPoint, + times: usize, + direction: Direction, + is_start: bool, +) -> DisplayPoint { + let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else { + return display_point; + }; + + for _ in 0..times { + let point = map.display_point_to_point(display_point, Bias::Left); + let offset = point.to_offset(&map.buffer_snapshot); + let range = if direction == Direction::Prev { + 0..offset + } else { + offset..buffer.len() + }; + + let possibilities = buffer + .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(4)) + .filter_map(|(range, object)| { + if !matches!(object, language::TextObject::AroundFunction) { + return None; + } + + let relevant = if is_start { range.start } else { range.end }; + if direction == Direction::Prev && relevant < offset { + Some(relevant) + } else if direction == Direction::Next && relevant > offset + 1 { + Some(relevant) + } else { + None + } + }); + + let dest = if direction == Direction::Prev { + possibilities.max().unwrap_or(offset) + } else { + possibilities.min().unwrap_or(offset) + }; + let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left); + if new_point == display_point { + break; + } + display_point = new_point; + } + display_point +} + +fn comment_motion( + map: &DisplaySnapshot, + mut display_point: DisplayPoint, + times: usize, + direction: Direction, +) -> DisplayPoint { + let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else { + return display_point; + }; + + for _ in 0..times { + let point = map.display_point_to_point(display_point, Bias::Left); + let offset = point.to_offset(&map.buffer_snapshot); + let range = if direction == Direction::Prev { + 0..offset + } else { + offset..buffer.len() + }; + + let possibilities = buffer + .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(6)) + .filter_map(|(range, object)| { + if !matches!(object, language::TextObject::AroundComment) { + return None; + } + + let relevant = if direction == Direction::Prev { + range.start + } else { + range.end + }; + if direction == Direction::Prev && relevant < offset { + Some(relevant) + } else if direction == Direction::Next && relevant > offset + 1 { + Some(relevant) + } else { + None + } + }); + + let dest = if direction == Direction::Prev { + possibilities.max().unwrap_or(offset) + } else { + possibilities.min().unwrap_or(offset) + }; + let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left); + if new_point == display_point { + break; + } + display_point = new_point; + } + + display_point +} + +fn section_motion( + map: &DisplaySnapshot, + mut display_point: DisplayPoint, + times: usize, + direction: Direction, + is_start: bool, +) -> DisplayPoint { + if let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() { + for _ in 0..times { + let offset = map + .display_point_to_point(display_point, Bias::Left) + .to_offset(&map.buffer_snapshot); + let range = if direction == Direction::Prev { + 0..offset + } else { + offset..buffer.len() + }; + + // we set a max start depth here because we want a section to only be "top level" + // similar to vim's default of '{' in the first column. + // (and without it, ]] at the start of editor.rs is -very- slow) + let mut possibilities = buffer + .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(3)) + .filter(|(_, object)| { + matches!( + object, + language::TextObject::AroundClass | language::TextObject::AroundFunction + ) + }) + .collect::>(); + possibilities.sort_by_key(|(range_a, _)| range_a.start); + let mut prev_end = None; + let possibilities = possibilities.into_iter().filter_map(|(range, t)| { + if t == language::TextObject::AroundFunction + && prev_end.is_some_and(|prev_end| prev_end > range.start) + { + return None; + } + prev_end = Some(range.end); + + let relevant = if is_start { range.start } else { range.end }; + if direction == Direction::Prev && relevant < offset { + Some(relevant) + } else if direction == Direction::Next && relevant > offset + 1 { + Some(relevant) + } else { + None + } + }); + + let offset = if direction == Direction::Prev { + possibilities.max().unwrap_or(0) + } else { + possibilities.min().unwrap_or(buffer.len()) + }; + + let new_point = map.clip_point(offset.to_display_point(&map), Bias::Left); + if new_point == display_point { + break; + } + display_point = new_point; + } + return display_point; + }; + + for _ in 0..times { + let point = map.display_point_to_point(display_point, Bias::Left); + let Some(excerpt) = map.buffer_snapshot.excerpt_containing(point..point) else { + return display_point; + }; + let next_point = match (direction, is_start) { + (Direction::Prev, true) => { + let mut start = excerpt.start_anchor().to_display_point(&map); + if start >= display_point && start.row() > DisplayRow(0) { + let Some(excerpt) = map.buffer_snapshot.excerpt_before(excerpt.id()) else { + return display_point; + }; + start = excerpt.start_anchor().to_display_point(&map); + } + start + } + (Direction::Prev, false) => { + let mut start = excerpt.start_anchor().to_display_point(&map); + if start.row() > DisplayRow(0) { + *start.row_mut() -= 1; + } + map.clip_point(start, Bias::Left) + } + (Direction::Next, true) => { + let mut end = excerpt.end_anchor().to_display_point(&map); + *end.row_mut() += 1; + map.clip_point(end, Bias::Right) + } + (Direction::Next, false) => { + let mut end = excerpt.end_anchor().to_display_point(&map); + *end.column_mut() = 0; + if end <= display_point { + *end.row_mut() += 1; + let point_end = map.display_point_to_point(end, Bias::Right); + let Some(excerpt) = + map.buffer_snapshot.excerpt_containing(point_end..point_end) + else { + return display_point; + }; + end = excerpt.end_anchor().to_display_point(&map); + *end.column_mut() = 0; + } + end + } + }; + if next_point == display_point { + break; + } + display_point = next_point; + } + + display_point +} + #[cfg(test)] mod test { diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index 7ed97358ff..380acc896a 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -1,6 +1,10 @@ use std::ops::Range; -use crate::{motion::right, state::Mode, Vim}; +use crate::{ + motion::right, + state::{Mode, Operator}, + Vim, +}; use editor::{ display_map::{DisplaySnapshot, ToDisplayPoint}, movement::{self, FindRange}, @@ -10,7 +14,7 @@ use editor::{ use itertools::Itertools; use gpui::{actions, impl_actions, ViewContext}; -use language::{BufferSnapshot, CharKind, Point, Selection}; +use language::{BufferSnapshot, CharKind, Point, Selection, TextObject, TreeSitterOptions}; use multi_buffer::MultiBufferRow; use serde::Deserialize; @@ -30,6 +34,9 @@ pub enum Object { Argument, IndentObj { include_below: bool }, Tag, + Method, + Class, + Comment, } #[derive(Clone, Deserialize, PartialEq)] @@ -61,7 +68,10 @@ actions!( CurlyBrackets, AngleBrackets, Argument, - Tag + Tag, + Method, + Class, + Comment ] ); @@ -107,6 +117,18 @@ pub fn register(editor: &mut Editor, cx: &mut ViewContext) { Vim::action(editor, cx, |vim, _: &Argument, cx| { vim.object(Object::Argument, cx) }); + Vim::action(editor, cx, |vim, _: &Method, cx| { + vim.object(Object::Method, cx) + }); + Vim::action(editor, cx, |vim, _: &Class, cx| { + vim.object(Object::Class, cx) + }); + Vim::action(editor, cx, |vim, _: &Comment, cx| { + if !matches!(vim.active_operator(), Some(Operator::Object { .. })) { + vim.push_operator(Operator::Object { around: true }, cx); + } + vim.object(Object::Comment, cx) + }); Vim::action( editor, cx, @@ -144,6 +166,9 @@ impl Object { | Object::CurlyBrackets | Object::SquareBrackets | Object::Argument + | Object::Method + | Object::Class + | Object::Comment | Object::IndentObj { .. } => true, } } @@ -162,12 +187,15 @@ impl Object { | Object::Parentheses | Object::SquareBrackets | Object::Tag + | Object::Method + | Object::Class + | Object::Comment | Object::CurlyBrackets | Object::AngleBrackets => true, } } - pub fn target_visual_mode(self, current_mode: Mode) -> Mode { + pub fn target_visual_mode(self, current_mode: Mode, around: bool) -> Mode { match self { Object::Word { .. } | Object::Sentence @@ -186,8 +214,16 @@ impl Object { | Object::AngleBrackets | Object::VerticalBars | Object::Tag + | Object::Comment | Object::Argument | Object::IndentObj { .. } => Mode::Visual, + Object::Method | Object::Class => { + if around { + Mode::VisualLine + } else { + Mode::Visual + } + } Object::Paragraph => Mode::VisualLine, } } @@ -238,6 +274,33 @@ impl Object { Object::AngleBrackets => { surrounding_markers(map, relative_to, around, self.is_multiline(), '<', '>') } + Object::Method => text_object( + map, + relative_to, + if around { + TextObject::AroundFunction + } else { + TextObject::InsideFunction + }, + ), + Object::Comment => text_object( + map, + relative_to, + if around { + TextObject::AroundComment + } else { + TextObject::InsideComment + }, + ), + Object::Class => text_object( + map, + relative_to, + if around { + TextObject::AroundClass + } else { + TextObject::InsideClass + }, + ), Object::Argument => argument(map, relative_to, around), Object::IndentObj { include_below } => indent(map, relative_to, around, include_below), } @@ -441,6 +504,47 @@ fn around_next_word( Some(start..end) } +fn text_object( + map: &DisplaySnapshot, + relative_to: DisplayPoint, + target: TextObject, +) -> Option> { + let snapshot = &map.buffer_snapshot; + let offset = relative_to.to_offset(map, Bias::Left); + + let excerpt = snapshot.excerpt_containing(offset..offset)?; + let buffer = excerpt.buffer(); + + let mut matches: Vec> = buffer + .text_object_ranges(offset..offset, TreeSitterOptions::default()) + .filter_map(|(r, m)| if m == target { Some(r) } else { None }) + .collect(); + matches.sort_by_key(|r| (r.end - r.start)); + if let Some(range) = matches.first() { + return Some(range.start.to_display_point(map)..range.end.to_display_point(map)); + } + + let around = target.around()?; + let mut matches: Vec> = buffer + .text_object_ranges(offset..offset, TreeSitterOptions::default()) + .filter_map(|(r, m)| if m == around { Some(r) } else { None }) + .collect(); + matches.sort_by_key(|r| (r.end - r.start)); + let around_range = matches.first()?; + + let mut matches: Vec> = buffer + .text_object_ranges(around_range.clone(), TreeSitterOptions::default()) + .filter_map(|(r, m)| if m == target { Some(r) } else { None }) + .collect(); + matches.sort_by_key(|r| r.start); + if let Some(range) = matches.first() { + if !range.is_empty() { + return Some(range.start.to_display_point(map)..range.end.to_display_point(map)); + } + } + return Some(around_range.start.to_display_point(map)..around_range.end.to_display_point(map)); +} + fn argument( map: &DisplaySnapshot, relative_to: DisplayPoint, diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 813be6dda1..8d2b31a1de 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -308,7 +308,7 @@ impl Vim { if let Some(Operator::Object { around }) = self.active_operator() { self.pop_operator(cx); let current_mode = self.mode; - let target_mode = object.target_visual_mode(current_mode); + let target_mode = object.target_visual_mode(current_mode, around); if target_mode != current_mode { self.switch_mode(target_mode, true, cx); } diff --git a/docs/src/extensions/languages.md b/docs/src/extensions/languages.md index 0995ed97fd..fc2c42c74a 100644 --- a/docs/src/extensions/languages.md +++ b/docs/src/extensions/languages.md @@ -69,6 +69,7 @@ several features: - Syntax overrides - Text redactions - Runnable code detection +- Selecting classes, functions, etc. The following sections elaborate on how [Tree-sitter queries](https://tree-sitter.github.io/tree-sitter/using-parsers#query-syntax) enable these features in Zed, using [JSON syntax](https://www.json.org/json-en.html) as a guiding example. @@ -259,6 +260,44 @@ For example, in JavaScript, we also disable auto-closing of single quotes within (comment) @comment.inclusive ``` +### Text objects + +The `textobjects.scm` file defines rules for navigating by text objects. This was added in Zed v0.165 and is currently used only in Vim mode. + +Vim provides two levels of granularity for navigating around files. Section-by-section with `[]` etc., and method-by-method with `]m` etc. Even languages that don't support functions and classes can work well by defining similar concepts. For example CSS defines a rule-set as a method, and a media-query as a class. + +For languages with closures, these typically should not count as functions in Zed. This is best-effort however, as languages like Javascript do not syntactically differentiate syntactically between closures and top-level function declarations. + +For languages with declarations like C, provide queries that match `@class.around` or `@function.around`. The `if` and `ic` text objects will default to these if there is no inside. + +If you are not sure what to put in textobjects.scm, both [nvim-treesitter-textobjects](https://github.com/nvim-treesitter/nvim-treesitter-textobjects), and the [Helix editor](https://github.com/helix-editor/helix) have queries for many languages. You can refer to the Zed [built-in languages](https://github.com/zed-industries/zed/tree/main/crates/languages/src) to see how to adapt these. + +| Capture | Description | Vim mode | +| ---------------- | ----------------------------------------------------------------------- | ------------------------------------------------ | +| @function.around | An entire function definition or equivalent small section of a file. | `[m`, `]m`, `[M`,`]M` motions. `af` text object | +| @function.inside | The function body (the stuff within the braces). | `if` text object | +| @class.around | An entire class definition or equivalent large section of a file. | `[[`, `]]`, `[]`, `][` motions. `ac` text object | +| @class.inside | The contents of a class definition. | `ic` text object | +| @comment.around | An entire comment (e.g. all adjacent line comments, or a block comment) | `gc` text object | +| @comment.inside | The contents of a comment | `igc` text object (rarely supported) | + +For example: + +```scheme +; include only the content of the method in the function +(method_definition + body: (_ + "{" + (_)* @function.inside + "}")) @function.around + +; match function.around for declarations with no body +(function_signature_item) @function.around + +; join all adjacent comments into one +(comment)+ @comment.around +``` + ### Text redactions The `redactions.scm` file defines text redaction rules. When collaborating and sharing your screen, it makes sure that certain syntax nodes are rendered in a redacted mode to avoid them from leaking. diff --git a/docs/src/vim.md b/docs/src/vim.md index 254c5a0934..c0a7fed2e2 100644 --- a/docs/src/vim.md +++ b/docs/src/vim.md @@ -79,12 +79,41 @@ The following commands use the language server to help you navigate and refactor ### Treesitter -Treesitter is a powerful tool that Zed uses to understand the structure of your code. These commands help you navigate your code semantically. +Treesitter is a powerful tool that Zed uses to understand the structure of your code. Zed provides motions that change the current cursor position, and text objects that can be used as the target of actions. -| Command | Default Shortcut | -| ---------------------------- | ---------------- | -| Select a smaller syntax node | `] x` | -| Select a larger syntax node | `[ x` | +| Command | Default Shortcut | +| ------------------------------- | --------------------------- | +| Go to next/previous method | `] m` / `[ m` | +| Go to next/previous method end | `] M` / `[ M` | +| Go to next/previous section | `] ]` / `[ [` | +| Go to next/previous section end | `] [` / `[ ]` | +| Go to next/previous comment | `] /`, `] *` / `[ /`, `[ *` | +| Select a larger syntax node | `[ x` | +| Select a larger syntax node | `[ x` | + +| Text Objects | Default Shortcut | +| ---------------------------------------------------------- | ---------------- | +| Around a class, definition, etc. | `a c` | +| Inside a class, definition, etc. | `i c` | +| Around a function, method etc. | `a f` | +| Inside a function, method, etc. | `i f` | +| A comment | `g c` | +| An argument, or list item, etc. | `i a` | +| An argument, or list item, etc. (including trailing comma) | `a a` | +| Around an HTML-like tag | `i a` | +| Inside an HTML-like tag | `i a` | +| The current indent level, and one line before and after | `a I` | +| The current indent level, and one line before | `a i` | +| The current indent level | `i i` | + +Note that the definitions for the targets of the `[m` family of motions are the same as the +boundaries defined by `af`. The targets of the `[[` are the same as those defined by `ac`, though +if there are no classes, then functions are also used. Similarly `gc` is used to find `[ /`. `g c` + +The definition of functions, classes and comments is language dependent, and support can be added +to extensions by adding a [`textobjects.scm`]. The definition of arguments and tags operates at +the tree-sitter level, but looks for certain patterns in the parse tree and is not currently configurable +per language. ### Multi cursor From 41a973b13f4db40627f18cfe2b496ce2fe6cc7f5 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Tue, 3 Dec 2024 17:57:39 +0000 Subject: [PATCH 257/886] Publish theme json schema v0.2.0 (#21428) Fix theme json schema so `./script/import-themes print-schema` works again Update schema to reflect current structs ([diff](https://gist.github.com/notpeter/26e6d0939985f542e8492458442ac62a/revisions?diff=unified&w=)) https://zed.dev/schema/themes/v0.2.0.json --- assets/themes/andromeda/andromeda.json | 2 +- assets/themes/atelier/atelier.json | 2 +- assets/themes/ayu/ayu.json | 2 +- assets/themes/gruvbox/gruvbox.json | 2 +- assets/themes/one/one.json | 2 +- assets/themes/rose_pine/rose_pine.json | 2 +- assets/themes/sandcastle/sandcastle.json | 2 +- assets/themes/solarized/solarized.json | 2 +- assets/themes/summercamp/summercamp.json | 2 +- crates/theme_importer/src/main.rs | 78 +++++++++++++----------- docs/src/extensions/themes.md | 4 +- script/import-themes | 2 +- 12 files changed, 55 insertions(+), 47 deletions(-) diff --git a/assets/themes/andromeda/andromeda.json b/assets/themes/andromeda/andromeda.json index 532d013b36..633b5c308f 100644 --- a/assets/themes/andromeda/andromeda.json +++ b/assets/themes/andromeda/andromeda.json @@ -1,5 +1,5 @@ { - "$schema": "https://zed.dev/schema/themes/v0.1.0.json", + "$schema": "https://zed.dev/schema/themes/v0.2.0.json", "name": "Andromeda", "author": "Zed Industries", "themes": [ diff --git a/assets/themes/atelier/atelier.json b/assets/themes/atelier/atelier.json index 1bf4878b5a..f72e8e84ee 100644 --- a/assets/themes/atelier/atelier.json +++ b/assets/themes/atelier/atelier.json @@ -1,5 +1,5 @@ { - "$schema": "https://zed.dev/schema/themes/v0.1.0.json", + "$schema": "https://zed.dev/schema/themes/v0.2.0.json", "name": "Atelier", "author": "Zed Industries", "themes": [ diff --git a/assets/themes/ayu/ayu.json b/assets/themes/ayu/ayu.json index 00fb6deb91..d511ebf84a 100644 --- a/assets/themes/ayu/ayu.json +++ b/assets/themes/ayu/ayu.json @@ -1,5 +1,5 @@ { - "$schema": "https://zed.dev/schema/themes/v0.1.0.json", + "$schema": "https://zed.dev/schema/themes/v0.2.0.json", "name": "Ayu", "author": "Zed Industries", "themes": [ diff --git a/assets/themes/gruvbox/gruvbox.json b/assets/themes/gruvbox/gruvbox.json index a56ea7d046..908ce3a28a 100644 --- a/assets/themes/gruvbox/gruvbox.json +++ b/assets/themes/gruvbox/gruvbox.json @@ -1,5 +1,5 @@ { - "$schema": "https://zed.dev/schema/themes/v0.1.0.json", + "$schema": "https://zed.dev/schema/themes/v0.2.0.json", "name": "Gruvbox", "author": "Zed Industries", "themes": [ diff --git a/assets/themes/one/one.json b/assets/themes/one/one.json index 0519ead392..daa09f8995 100644 --- a/assets/themes/one/one.json +++ b/assets/themes/one/one.json @@ -1,5 +1,5 @@ { - "$schema": "https://zed.dev/schema/themes/v0.1.0.json", + "$schema": "https://zed.dev/schema/themes/v0.2.0.json", "name": "One", "author": "Zed Industries", "themes": [ diff --git a/assets/themes/rose_pine/rose_pine.json b/assets/themes/rose_pine/rose_pine.json index 5b66c5ed34..2ff97da117 100644 --- a/assets/themes/rose_pine/rose_pine.json +++ b/assets/themes/rose_pine/rose_pine.json @@ -1,5 +1,5 @@ { - "$schema": "https://zed.dev/schema/themes/v0.1.0.json", + "$schema": "https://zed.dev/schema/themes/v0.2.0.json", "name": "Rosé Pine", "author": "Zed Industries", "themes": [ diff --git a/assets/themes/sandcastle/sandcastle.json b/assets/themes/sandcastle/sandcastle.json index b5239b0a55..ba9e6f50fd 100644 --- a/assets/themes/sandcastle/sandcastle.json +++ b/assets/themes/sandcastle/sandcastle.json @@ -1,5 +1,5 @@ { - "$schema": "https://zed.dev/schema/themes/v0.1.0.json", + "$schema": "https://zed.dev/schema/themes/v0.2.0.json", "name": "Sandcastle", "author": "Zed Industries", "themes": [ diff --git a/assets/themes/solarized/solarized.json b/assets/themes/solarized/solarized.json index 7bd0c53f52..fe86793cdc 100644 --- a/assets/themes/solarized/solarized.json +++ b/assets/themes/solarized/solarized.json @@ -1,5 +1,5 @@ { - "$schema": "https://zed.dev/schema/themes/v0.1.0.json", + "$schema": "https://zed.dev/schema/themes/v0.2.0.json", "name": "Solarized", "author": "Zed Industries", "themes": [ diff --git a/assets/themes/summercamp/summercamp.json b/assets/themes/summercamp/summercamp.json index 84423a8600..c2206f9aab 100644 --- a/assets/themes/summercamp/summercamp.json +++ b/assets/themes/summercamp/summercamp.json @@ -1,5 +1,5 @@ { - "$schema": "https://zed.dev/schema/themes/v0.1.0.json", + "$schema": "https://zed.dev/schema/themes/v0.2.0.json", "name": "Summercamp", "author": "Zed Industries", "themes": [ diff --git a/crates/theme_importer/src/main.rs b/crates/theme_importer/src/main.rs index d92966ae24..db287956c5 100644 --- a/crates/theme_importer/src/main.rs +++ b/crates/theme_importer/src/main.rs @@ -19,6 +19,8 @@ use theme::{Appearance, AppearanceContent, ThemeFamilyContent}; use crate::vscode::VsCodeTheme; use crate::vscode::VsCodeThemeConverter; +const ZED_THEME_SCHEMA_URL: &str = "https://zed.dev/public/schema/themes/v0.2.0.json"; + #[derive(Debug, Deserialize)] struct FamilyMetadata { pub name: String, @@ -69,34 +71,53 @@ pub struct ThemeMetadata { #[derive(Parser)] #[command(author, version, about, long_about = None)] struct Args { - /// The path to the theme to import. - theme_path: PathBuf, - - /// Whether to warn when values are missing from the theme. - #[arg(long)] - warn_on_missing: bool, - - /// The path to write the output to. - #[arg(long, short)] - output: Option, - #[command(subcommand)] - command: Option, + command: Command, } -#[derive(Subcommand)] +#[derive(PartialEq, Subcommand)] enum Command { /// Prints the JSON schema for a theme. PrintSchema, + /// Converts a VSCode theme to Zed format [default] + Convert { + /// The path to the theme to import. + theme_path: PathBuf, + + /// Whether to warn when values are missing from the theme. + #[arg(long)] + warn_on_missing: bool, + + /// The path to write the output to. + #[arg(long, short)] + output: Option, + }, } fn main() -> Result<()> { let args = Args::parse(); + match args.command { + Command::PrintSchema => { + let theme_family_schema = schema_for!(ThemeFamilyContent); + println!( + "{}", + serde_json::to_string_pretty(&theme_family_schema).unwrap() + ); + Ok(()) + } + Command::Convert { + theme_path, + warn_on_missing, + output, + } => convert(theme_path, output, warn_on_missing), + } +} + +fn convert(theme_file_path: PathBuf, output: Option, warn_on_missing: bool) -> Result<()> { let log_config = { let mut config = simplelog::ConfigBuilder::new(); - - if !args.warn_on_missing { + if !warn_on_missing { config.add_filter_ignore_str("theme_printer"); } @@ -111,28 +132,11 @@ fn main() -> Result<()> { ) .expect("could not initialize logger"); - if let Some(command) = args.command { - match command { - Command::PrintSchema => { - let theme_family_schema = schema_for!(ThemeFamilyContent); - - println!( - "{}", - serde_json::to_string_pretty(&theme_family_schema).unwrap() - ); - - return Ok(()); - } - } - } - - let theme_file_path = args.theme_path; - let theme_file = match File::open(&theme_file_path) { Ok(file) => file, Err(err) => { log::info!("Failed to open file at path: {:?}", theme_file_path); - return Err(err)?; + return Err(err.into()); } }; @@ -148,10 +152,14 @@ fn main() -> Result<()> { let converter = VsCodeThemeConverter::new(vscode_theme, theme_metadata, IndexMap::new()); let theme = converter.convert()?; - + let mut theme = serde_json::to_value(theme).unwrap(); + theme.as_object_mut().unwrap().insert( + "$schema".to_string(), + serde_json::Value::String(ZED_THEME_SCHEMA_URL.to_string()), + ); let theme_json = serde_json::to_string_pretty(&theme).unwrap(); - if let Some(output) = args.output { + if let Some(output) = output { let mut file = File::create(output)?; file.write_all(theme_json.as_bytes())?; } else { diff --git a/docs/src/extensions/themes.md b/docs/src/extensions/themes.md index 4737a99a3e..ecdbdace59 100644 --- a/docs/src/extensions/themes.md +++ b/docs/src/extensions/themes.md @@ -2,13 +2,13 @@ The `themes` directory in an extension should contain one or more theme files. -Each theme file should adhere to the JSON schema specified at [`https://zed.dev/schema/themes/v0.1.0.json`](https://zed.dev/schema/themes/v0.1.0.json). +Each theme file should adhere to the JSON schema specified at [`https://zed.dev/schema/themes/v0.2.0.json`](https://zed.dev/schema/themes/v0.2.0.json). See [this blog post](https://zed.dev/blog/user-themes-now-in-preview) for more details about creating themes. ## Theme JSON Structure -The structure of a Zed theme is defined in the [Zed Theme JSON Schema](https://zed.dev/schema/themes/v0.1.0.json). +The structure of a Zed theme is defined in the [Zed Theme JSON Schema](https://zed.dev/schema/themes/v0.2.0.json). A Zed theme consists of a Theme Family object including: diff --git a/script/import-themes b/script/import-themes index ce9ce9ef12..8f07df2ef3 100755 --- a/script/import-themes +++ b/script/import-themes @@ -1,3 +1,3 @@ #!/bin/bash -cargo run -p theme_importer +cargo run -p theme_importer -- "$@" From afb253b406c40d1bb0a7ec2961be93523130e460 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Wed, 4 Dec 2024 02:03:53 +0800 Subject: [PATCH 258/886] ui: Ensure `Label` with `single_line` set does not wrap (#21444) Release Notes: - N/A --- Split from #21438, this change for make sure the `single_line` mode Label will not be wrap. --------- Co-authored-by: Marshall Bowers --- .../src/components/label/highlighted_label.rs | 5 +++++ crates/ui/src/components/label/label.rs | 20 ++++++------------- crates/ui/src/components/label/label_like.rs | 11 ++++++++++ 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/crates/ui/src/components/label/highlighted_label.rs b/crates/ui/src/components/label/highlighted_label.rs index f961713956..0e6cc26b18 100644 --- a/crates/ui/src/components/label/highlighted_label.rs +++ b/crates/ui/src/components/label/highlighted_label.rs @@ -65,6 +65,11 @@ impl LabelCommon for HighlightedLabel { self.base = self.base.underline(underline); self } + + fn single_line(mut self) -> Self { + self.base = self.base.single_line(); + self + } } pub fn highlight_ranges( diff --git a/crates/ui/src/components/label/label.rs b/crates/ui/src/components/label/label.rs index f655961841..1df33d2740 100644 --- a/crates/ui/src/components/label/label.rs +++ b/crates/ui/src/components/label/label.rs @@ -56,20 +56,6 @@ impl Label { single_line: false, } } - - /// Make the label display in a single line mode - /// - /// # Examples - /// - /// ``` - /// use ui::prelude::*; - /// - /// let my_label = Label::new("Hello, World!").single_line(); - /// ``` - pub fn single_line(mut self) -> Self { - self.single_line = true; - self - } } // Style methods. @@ -177,6 +163,12 @@ impl LabelCommon for Label { self.base = self.base.underline(underline); self } + + fn single_line(mut self) -> Self { + self.single_line = true; + self.base = self.base.single_line(); + self + } } impl RenderOnce for Label { diff --git a/crates/ui/src/components/label/label_like.rs b/crates/ui/src/components/label/label_like.rs index fd7303082a..b1c3240f5a 100644 --- a/crates/ui/src/components/label/label_like.rs +++ b/crates/ui/src/components/label/label_like.rs @@ -49,6 +49,9 @@ pub trait LabelCommon { /// Sets the alpha property of the label, overwriting the alpha value of the color. fn alpha(self, alpha: f32) -> Self; + + /// Sets the label to render as a single line. + fn single_line(self) -> Self; } #[derive(IntoElement)] @@ -63,6 +66,7 @@ pub struct LabelLike { children: SmallVec<[AnyElement; 2]>, alpha: Option, underline: bool, + single_line: bool, } impl Default for LabelLike { @@ -84,6 +88,7 @@ impl LabelLike { children: SmallVec::new(), alpha: None, underline: false, + single_line: false, } } } @@ -139,6 +144,11 @@ impl LabelCommon for LabelLike { self.alpha = Some(alpha); self } + + fn single_line(mut self) -> Self { + self.single_line = true; + self + } } impl ParentElement for LabelLike { @@ -178,6 +188,7 @@ impl RenderOnce for LabelLike { this }) .when(self.strikethrough, |this| this.line_through()) + .when(self.single_line, |this| this.whitespace_nowrap()) .text_color(color) .font_weight(self.weight.unwrap_or(settings.ui_font.weight)) .children(self.children) From 492ca219d34e56b4d4145545a6ab3d1a818f3a0e Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 3 Dec 2024 12:09:44 -0800 Subject: [PATCH 259/886] Fix panic in autoclosing (#21482) Closes #14961 Release Notes: - Fixed a panic when backspacing at the start of a buffer with `always_treat_brackets_as_autoclosed` enabled. --- crates/editor/src/editor.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 1e47eb46a8..88919f9295 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -4098,8 +4098,10 @@ impl Editor { if buffer.contains_str_at(selection.start, &pair.end) { let pair_start_len = pair.start.len(); - if buffer.contains_str_at(selection.start - pair_start_len, &pair.start) - { + if buffer.contains_str_at( + selection.start.saturating_sub(pair_start_len), + &pair.start, + ) { selection.start -= pair_start_len; selection.end += pair.end.len(); From b28287ce9137602957620b72aa6988a56b081de5 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 3 Dec 2024 12:09:53 -0800 Subject: [PATCH 260/886] Fix panic in remove_item (#21480) In #20742 we added a call to remove_item that retain an item index over an await point. This led to a race condition that could panic if another tab was removed during that time. (cc @mgsloan) This changes the API to make it harder to misuse. Release Notes: - Fixed a panic when closing tabs containing new unsaved files --- crates/workspace/src/pane.rs | 30 ++++++++++++++---------------- crates/workspace/src/workspace.rs | 8 ++++---- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index fe6b08fd4a..a2c63addd8 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -828,9 +828,10 @@ impl Pane { pub fn close_current_preview_item(&mut self, cx: &mut ViewContext) -> Option { let item_idx = self.preview_item_idx()?; + let id = self.preview_item_id()?; let prev_active_item_index = self.active_item_index; - self.remove_item(item_idx, false, false, cx); + self.remove_item(id, false, false, cx); self.active_item_index = prev_active_item_index; if item_idx < self.items.len() { @@ -1403,13 +1404,7 @@ impl Pane { // Remove the item from the pane. pane.update(&mut cx, |pane, cx| { - if let Some(item_ix) = pane - .items - .iter() - .position(|i| i.item_id() == item_to_close.item_id()) - { - pane.remove_item(item_ix, false, true, cx); - } + pane.remove_item(item_to_close.item_id(), false, true, cx); }) .ok(); } @@ -1421,11 +1416,14 @@ impl Pane { pub fn remove_item( &mut self, - item_index: usize, + item_id: EntityId, activate_pane: bool, close_pane_if_empty: bool, cx: &mut ViewContext, ) { + let Some(item_index) = self.index_for_item_id(item_id) else { + return; + }; self._remove_item(item_index, activate_pane, close_pane_if_empty, None, cx) } @@ -1615,7 +1613,9 @@ impl Pane { .await? } Ok(1) => { - pane.update(cx, |pane, cx| pane.remove_item(item_ix, false, false, cx))?; + pane.update(cx, |pane, cx| { + pane.remove_item(item.item_id(), false, false, cx) + })?; } _ => return Ok(false), } @@ -1709,9 +1709,7 @@ impl Pane { if let Some(abs_path) = abs_path.await.ok().flatten() { pane.update(cx, |pane, cx| { if let Some(item) = pane.item_for_path(abs_path.clone(), cx) { - if let Some(idx) = pane.index_for_item(&*item) { - pane.remove_item(idx, false, false, cx); - } + pane.remove_item(item.item_id(), false, false, cx); } item.save_as(project, abs_path, cx) @@ -1777,15 +1775,15 @@ impl Pane { entry_id: ProjectEntryId, cx: &mut ViewContext, ) -> Option<()> { - let (item_index_to_delete, item_id) = self.items().enumerate().find_map(|(i, item)| { + let item_id = self.items().find_map(|item| { if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] { - Some((i, item.item_id())) + Some(item.item_id()) } else { None } })?; - self.remove_item(item_index_to_delete, false, true, cx); + self.remove_item(item_id, false, true, cx); self.nav_history.remove_item(item_id); Some(()) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index a8681f22c5..c5de8822dc 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -3723,7 +3723,7 @@ impl Workspace { let mut new_item = task.await?; pane.update(cx, |pane, cx| { - let mut item_ix_to_remove = None; + let mut item_to_remove = None; for (ix, item) in pane.items().enumerate() { if let Some(item) = item.to_followable_item_handle(cx) { match new_item.dedup(item.as_ref(), cx) { @@ -3733,7 +3733,7 @@ impl Workspace { break; } Some(item::Dedup::ReplaceExisting) => { - item_ix_to_remove = Some(ix); + item_to_remove = Some((ix, item.item_id())); break; } None => {} @@ -3741,8 +3741,8 @@ impl Workspace { } } - if let Some(ix) = item_ix_to_remove { - pane.remove_item(ix, false, false, cx); + if let Some((ix, id)) = item_to_remove { + pane.remove_item(id, false, false, cx); pane.add_item(new_item.boxed_clone(), false, false, Some(ix), cx); } })?; From 731e6d31f6015827d1fcdebf59f298a4c16ff547 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 3 Dec 2024 12:10:02 -0800 Subject: [PATCH 261/886] Revert "macos: Add default keybind for ctrl-home / ctrl-end (#21007)" (#21476) This reverts commit 614b3b979b7373aaa6dee84dfbc824fce1a86ea8. This conflicts with the macOS `ctrl-fn-left/right` bindings for moving windows around (new in Sequoia). If you want these use: ``` { "context": "Editor", "bindings": { "ctrl-home": "editor::MoveToBeginning", "ctrl-end": "editor::MoveToEnd" } }, ``` Release Notes: - N/A --- assets/keymaps/default-macos.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index f3990cecee..71d997d2b1 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -93,8 +93,6 @@ "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 165d50ff5b1dbf02d60ba20d53f4a0a5ee7ff26e Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Tue, 3 Dec 2024 20:27:12 +0000 Subject: [PATCH 262/886] Add openbsd netcat to script/linux (#21478) - Follow-up to: https://github.com/zed-industries/zed/pull/20751 openbsd-netcat is required for interactive SSH Remoting prompts (password, passphrase, 2fa, etc). --- script/linux | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/script/linux b/script/linux index f1fe751154..7457b8de76 100755 --- a/script/linux +++ b/script/linux @@ -37,6 +37,7 @@ if [[ -n $apt ]]; then cmake clang jq + netcat-openbsd git curl gettext-base @@ -84,12 +85,14 @@ if [[ -n $dnf ]] || [[ -n $yum ]]; then tar ) # perl used for building openssl-sys crate. See: https://docs.rs/openssl/latest/openssl/ + # openbsd-netcat is unavailable in RHEL8/9 (and nmap-ncat doesn't support sockets) if grep -qP '^ID="(fedora)' /etc/os-release; then deps+=( perl-FindBin perl-IPC-Cmd perl-File-Compare perl-File-Copy + netcat mold ) elif grep -qP '^ID="(rhel|rocky|alma|centos|ol)' /etc/os-release; then @@ -120,7 +123,7 @@ if [[ -n $dnf ]] || [[ -n $yum ]]; then fi fi - $maysudo $pkg_cmd install -y "${deps[@]}" + $maysudo "$pkg_cmd" install -y "${deps[@]}" finalize exit 0 fi @@ -145,6 +148,7 @@ if [[ -n $zyp ]]; then libzstd-devel make mold + netcat-openbsd openssl-devel sqlite3-devel tar @@ -169,6 +173,7 @@ if [[ -n $pacman ]]; then wayland libgit2 libxkbcommon-x11 + openbsd-netcat openssl zstd pkgconf @@ -198,6 +203,7 @@ if [[ -n $xbps ]]; then libxcb-devel libxkbcommon-devel libzstd-devel + openbsd-netcat openssl-devel wayland-devel vulkan-loader @@ -222,6 +228,7 @@ if [[ -n $emerge ]]; then media-libs/alsa-lib media-libs/fontconfig media-libs/vulkan-loader + net-analyzer/openbsd-netcat x11-libs/libxcb x11-libs/libxkbcommon sys-devel/mold From 88b0d3c78eb2cc4f826e508fe458588a67de1f7e Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 3 Dec 2024 15:27:58 -0500 Subject: [PATCH 263/886] markdown: Make `cx` the last parameter to the constructor (#21487) I noticed that `Markdown::new` didn't have the `cx` as the final parameter, as is conventional. This PR fixes that. Release Notes: - N/A --- crates/editor/src/hover_popover.rs | 2 +- crates/markdown/examples/markdown.rs | 2 +- crates/markdown/examples/markdown_as_child.rs | 2 +- crates/markdown/src/markdown.rs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 006a42700b..c402132bf3 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -593,8 +593,8 @@ async fn parse_blocks( combined_text, markdown_style.clone(), Some(language_registry.clone()), - cx, fallback_language_name, + cx, ) }) .ok(); diff --git a/crates/markdown/examples/markdown.rs b/crates/markdown/examples/markdown.rs index 0514ebcf4e..26b4f83374 100644 --- a/crates/markdown/examples/markdown.rs +++ b/crates/markdown/examples/markdown.rs @@ -178,7 +178,7 @@ impl MarkdownExample { cx: &mut WindowContext, ) -> Self { let markdown = - cx.new_view(|cx| Markdown::new(text, style, Some(language_registry), cx, None)); + cx.new_view(|cx| Markdown::new(text, style, Some(language_registry), None, cx)); Self { markdown } } } diff --git a/crates/markdown/examples/markdown_as_child.rs b/crates/markdown/examples/markdown_as_child.rs index 3700e64364..a7be4d2891 100644 --- a/crates/markdown/examples/markdown_as_child.rs +++ b/crates/markdown/examples/markdown_as_child.rs @@ -87,7 +87,7 @@ pub fn main() { heading: Default::default(), }; let markdown = cx.new_view(|cx| { - Markdown::new(MARKDOWN_EXAMPLE.into(), markdown_style, None, cx, None) + Markdown::new(MARKDOWN_EXAMPLE.into(), markdown_style, None, None, cx) }); HelloWorld { markdown } diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index ff67c01a0e..39217b6930 100644 --- a/crates/markdown/src/markdown.rs +++ b/crates/markdown/src/markdown.rs @@ -71,8 +71,8 @@ impl Markdown { source: String, style: MarkdownStyle, language_registry: Option>, - cx: &ViewContext, fallback_code_block_language: Option, + cx: &ViewContext, ) -> Self { let focus_handle = cx.focus_handle(); let mut this = Self { From 463c99b503d0b679cd1859789e5a378fe2c0c783 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 3 Dec 2024 12:56:01 -0800 Subject: [PATCH 264/886] Fix script/get-released-version (#21489) Release Notes: - N/A --- script/get-released-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/get-released-version b/script/get-released-version index e1f4783f8a..357de7c240 100755 --- a/script/get-released-version +++ b/script/get-released-version @@ -18,4 +18,4 @@ case $channel in ;; esac -curl -s https://zed.dev/api/releases/latest?asset=Zed.dmg$query | jq -r .version +curl -s "https://zed.dev/api/releases/latest?asset=zed&os=macos&arch=aarch64$query" | jq -r .version From 1fccda7b8d1fc6d82befce6921a35538625f9e7a Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 3 Dec 2024 12:56:25 -0800 Subject: [PATCH 265/886] Add text objects to extensions (#21488) Release Notes: - Adds textobject support to erlang, haskell, lua, php, prisma, proto, toml, and zig --- Cargo.lock | 16 +++---- extensions/erlang/Cargo.toml | 2 +- extensions/erlang/extension.toml | 2 +- .../erlang/languages/erlang/textobjects.scm | 6 +++ extensions/haskell/Cargo.toml | 2 +- extensions/haskell/extension.toml | 2 +- .../haskell/languages/haskell/textobjects.scm | 12 +++++ extensions/lua/Cargo.toml | 2 +- extensions/lua/extension.toml | 2 +- extensions/lua/languages/lua/textobjects.scm | 7 +++ extensions/php/Cargo.toml | 2 +- extensions/php/extension.toml | 2 +- extensions/php/languages/php/textobjects.scm | 45 +++++++++++++++++++ extensions/prisma/Cargo.toml | 2 +- extensions/prisma/extension.toml | 2 +- .../prisma/languages/prisma/textobjects.scm | 25 +++++++++++ extensions/proto/Cargo.toml | 2 +- extensions/proto/extension.toml | 2 +- .../proto/languages/proto/textobjects.scm | 18 ++++++++ extensions/toml/Cargo.toml | 2 +- extensions/toml/extension.toml | 2 +- .../toml/languages/toml/textobjects.scm | 6 +++ extensions/zig/Cargo.toml | 2 +- extensions/zig/extension.toml | 2 +- extensions/zig/languages/zig/textobjects.scm | 27 +++++++++++ script/language-extension-version | 1 - 26 files changed, 170 insertions(+), 25 deletions(-) create mode 100644 extensions/erlang/languages/erlang/textobjects.scm create mode 100644 extensions/haskell/languages/haskell/textobjects.scm create mode 100644 extensions/lua/languages/lua/textobjects.scm create mode 100644 extensions/php/languages/php/textobjects.scm create mode 100644 extensions/prisma/languages/prisma/textobjects.scm create mode 100644 extensions/proto/languages/proto/textobjects.scm create mode 100644 extensions/toml/languages/toml/textobjects.scm create mode 100644 extensions/zig/languages/zig/textobjects.scm diff --git a/Cargo.lock b/Cargo.lock index 1bd064ca4c..6266df7d86 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15768,7 +15768,7 @@ dependencies = [ [[package]] name = "zed_erlang" -version = "0.1.0" +version = "0.1.1" dependencies = [ "zed_extension_api 0.1.0", ] @@ -15802,7 +15802,7 @@ dependencies = [ [[package]] name = "zed_haskell" -version = "0.1.1" +version = "0.1.2" dependencies = [ "zed_extension_api 0.1.0", ] @@ -15816,28 +15816,28 @@ dependencies = [ [[package]] name = "zed_lua" -version = "0.1.0" +version = "0.1.1" dependencies = [ "zed_extension_api 0.1.0", ] [[package]] name = "zed_php" -version = "0.2.2" +version = "0.2.3" dependencies = [ "zed_extension_api 0.1.0", ] [[package]] name = "zed_prisma" -version = "0.0.3" +version = "0.0.4" dependencies = [ "zed_extension_api 0.1.0", ] [[package]] name = "zed_proto" -version = "0.2.0" +version = "0.2.1" dependencies = [ "zed_extension_api 0.1.0", ] @@ -15880,7 +15880,7 @@ dependencies = [ [[package]] name = "zed_toml" -version = "0.1.1" +version = "0.1.2" dependencies = [ "zed_extension_api 0.1.0", ] @@ -15894,7 +15894,7 @@ dependencies = [ [[package]] name = "zed_zig" -version = "0.3.1" +version = "0.3.2" dependencies = [ "zed_extension_api 0.1.0", ] diff --git a/extensions/erlang/Cargo.toml b/extensions/erlang/Cargo.toml index 5067344896..ca354e0cbc 100644 --- a/extensions/erlang/Cargo.toml +++ b/extensions/erlang/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zed_erlang" -version = "0.1.0" +version = "0.1.1" edition = "2021" publish = false license = "Apache-2.0" diff --git a/extensions/erlang/extension.toml b/extensions/erlang/extension.toml index 8dd2628fd2..f6e903ccf9 100644 --- a/extensions/erlang/extension.toml +++ b/extensions/erlang/extension.toml @@ -1,7 +1,7 @@ id = "erlang" name = "Erlang" description = "Erlang support." -version = "0.1.0" +version = "0.1.1" schema_version = 1 authors = ["Dairon M ", "Fabian Bergström "] repository = "https://github.com/zed-industries/zed" diff --git a/extensions/erlang/languages/erlang/textobjects.scm b/extensions/erlang/languages/erlang/textobjects.scm new file mode 100644 index 0000000000..e802a2f362 --- /dev/null +++ b/extensions/erlang/languages/erlang/textobjects.scm @@ -0,0 +1,6 @@ +(function_clause + body: (_ "->" (_)* @function.inside)) @function.around + +(type_alias ty: (_) @class.inside) @class.around + +(comment)+ @comment.around diff --git a/extensions/haskell/Cargo.toml b/extensions/haskell/Cargo.toml index 0b69075a20..c106a0dd1b 100644 --- a/extensions/haskell/Cargo.toml +++ b/extensions/haskell/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zed_haskell" -version = "0.1.1" +version = "0.1.2" edition = "2021" publish = false license = "Apache-2.0" diff --git a/extensions/haskell/extension.toml b/extensions/haskell/extension.toml index 2ef30cb3d5..003687136e 100644 --- a/extensions/haskell/extension.toml +++ b/extensions/haskell/extension.toml @@ -1,7 +1,7 @@ id = "haskell" name = "Haskell" description = "Haskell support." -version = "0.1.1" +version = "0.1.2" schema_version = 1 authors = [ "Pocæus ", diff --git a/extensions/haskell/languages/haskell/textobjects.scm b/extensions/haskell/languages/haskell/textobjects.scm new file mode 100644 index 0000000000..4302397467 --- /dev/null +++ b/extensions/haskell/languages/haskell/textobjects.scm @@ -0,0 +1,12 @@ +(comment)+ @comment.around + +[ + (adt) + (type_alias) + (newtype) +] @class.around + +(record_fields "{" (_)* @class.inside "}") + +((signature)? (function)+) @function.around +(function rhs:(_) @function.inside) diff --git a/extensions/lua/Cargo.toml b/extensions/lua/Cargo.toml index f577ce1871..8eec6ed62f 100644 --- a/extensions/lua/Cargo.toml +++ b/extensions/lua/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zed_lua" -version = "0.1.0" +version = "0.1.1" edition = "2021" publish = false license = "Apache-2.0" diff --git a/extensions/lua/extension.toml b/extensions/lua/extension.toml index 82026f48ba..52120cdfa2 100644 --- a/extensions/lua/extension.toml +++ b/extensions/lua/extension.toml @@ -1,7 +1,7 @@ id = "lua" name = "Lua" description = "Lua support." -version = "0.1.0" +version = "0.1.1" schema_version = 1 authors = ["Max Brunsfeld "] repository = "https://github.com/zed-industries/zed" diff --git a/extensions/lua/languages/lua/textobjects.scm b/extensions/lua/languages/lua/textobjects.scm new file mode 100644 index 0000000000..1f8bf66059 --- /dev/null +++ b/extensions/lua/languages/lua/textobjects.scm @@ -0,0 +1,7 @@ +(function_definition + body: (_) @function.inside) @function.around + +(function_declaration + body: (_) @function.inside) @function.around + +(comment)+ @comment.around diff --git a/extensions/php/Cargo.toml b/extensions/php/Cargo.toml index a78c133e8e..8bf6a523f4 100644 --- a/extensions/php/Cargo.toml +++ b/extensions/php/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zed_php" -version = "0.2.2" +version = "0.2.3" edition = "2021" publish = false license = "Apache-2.0" diff --git a/extensions/php/extension.toml b/extensions/php/extension.toml index eec2fe5d39..a2bc1d921e 100644 --- a/extensions/php/extension.toml +++ b/extensions/php/extension.toml @@ -1,7 +1,7 @@ id = "php" name = "PHP" description = "PHP support." -version = "0.2.2" +version = "0.2.3" schema_version = 1 authors = ["Piotr Osiewicz "] repository = "https://github.com/zed-industries/zed" diff --git a/extensions/php/languages/php/textobjects.scm b/extensions/php/languages/php/textobjects.scm new file mode 100644 index 0000000000..d86a0c1252 --- /dev/null +++ b/extensions/php/languages/php/textobjects.scm @@ -0,0 +1,45 @@ +(function_definition + body: (_ + "{" + (_)* @function.inside + "}" )) @function.around + +(method_declaration + body: (_ + "{" + (_)* @function.inside + "}" )) @function.around + +(method_declaration) @function.around + +(class_declaration + body: (_ + "{" + (_)* @class.inside + "}")) @class.around + +(interface_declaration + body: (_ + "{" + (_)* @class.inside + "}")) @class.around + +(trait_declaration + body: (_ + "{" + (_)* @class.inside + "}")) @class.around + +(enum_declaration + body: (_ + "{" + (_)* @class.inside + "}")) @class.around + +(namespace_definition + body: (_ + "{" + (_)* @class.inside + "}")) @class.around + +(comment)+ @comment.around diff --git a/extensions/prisma/Cargo.toml b/extensions/prisma/Cargo.toml index e5a261266a..68256bd1cc 100644 --- a/extensions/prisma/Cargo.toml +++ b/extensions/prisma/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zed_prisma" -version = "0.0.3" +version = "0.0.4" edition = "2021" publish = false license = "Apache-2.0" diff --git a/extensions/prisma/extension.toml b/extensions/prisma/extension.toml index 449f990d2f..22b2bd9f2b 100644 --- a/extensions/prisma/extension.toml +++ b/extensions/prisma/extension.toml @@ -1,7 +1,7 @@ id = "prisma" name = "Prisma" description = "Prisma support." -version = "0.0.3" +version = "0.0.4" schema_version = 1 authors = ["Matthew Gramigna "] repository = "https://github.com/zed-industries/zed" diff --git a/extensions/prisma/languages/prisma/textobjects.scm b/extensions/prisma/languages/prisma/textobjects.scm new file mode 100644 index 0000000000..0158c90786 --- /dev/null +++ b/extensions/prisma/languages/prisma/textobjects.scm @@ -0,0 +1,25 @@ +(model_declaration + (statement_block + "{" + (_)* @class.inside + "}")) @class.around + +(datasource_declaration + (statement_block + "{" + (_)* @class.inside + "}")) @class.around + +(generator_declaration + (statement_block + "{" + (_)* @class.inside + "}")) @class.around + +(enum_declaration + (enum_block + "{" + (_)* @class.inside + "}")) @class.around + +(developer_comment)+ @comment.around diff --git a/extensions/proto/Cargo.toml b/extensions/proto/Cargo.toml index 215a09f896..03c9bc5626 100644 --- a/extensions/proto/Cargo.toml +++ b/extensions/proto/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zed_proto" -version = "0.2.0" +version = "0.2.1" edition = "2021" publish = false license = "Apache-2.0" diff --git a/extensions/proto/extension.toml b/extensions/proto/extension.toml index f26aee7dde..232602faf7 100644 --- a/extensions/proto/extension.toml +++ b/extensions/proto/extension.toml @@ -1,7 +1,7 @@ id = "proto" name = "Proto" description = "Protocol Buffers support." -version = "0.2.0" +version = "0.2.1" schema_version = 1 authors = ["Zed Industries "] repository = "https://github.com/zed-industries/zed" diff --git a/extensions/proto/languages/proto/textobjects.scm b/extensions/proto/languages/proto/textobjects.scm new file mode 100644 index 0000000000..90ea84282d --- /dev/null +++ b/extensions/proto/languages/proto/textobjects.scm @@ -0,0 +1,18 @@ +(message (message_body + "{" + (_)* @class.inside + "}")) @class.around +(enum (enum_body + "{" + (_)* @class.inside + "}")) @class.around +(service + "service" + (_) + "{" + (_)* @class.inside + "}") @class.around + +(rpc) @function.around + +(comment)+ @comment.around diff --git a/extensions/toml/Cargo.toml b/extensions/toml/Cargo.toml index 3aa7b69224..85d933e236 100644 --- a/extensions/toml/Cargo.toml +++ b/extensions/toml/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zed_toml" -version = "0.1.1" +version = "0.1.2" edition = "2021" publish = false license = "Apache-2.0" diff --git a/extensions/toml/extension.toml b/extensions/toml/extension.toml index 15db5c464d..a8b9250226 100644 --- a/extensions/toml/extension.toml +++ b/extensions/toml/extension.toml @@ -1,7 +1,7 @@ id = "toml" name = "TOML" description = "TOML support." -version = "0.1.1" +version = "0.1.2" schema_version = 1 authors = [ "Max Brunsfeld ", diff --git a/extensions/toml/languages/toml/textobjects.scm b/extensions/toml/languages/toml/textobjects.scm new file mode 100644 index 0000000000..f5b4856e27 --- /dev/null +++ b/extensions/toml/languages/toml/textobjects.scm @@ -0,0 +1,6 @@ +(comment)+ @comment +(table "[" (_) "]" + (_)* @class.inside) @class.around + +(table_array_element "[[" (_) "]]" + (_)* @class.inside) @class.around diff --git a/extensions/zig/Cargo.toml b/extensions/zig/Cargo.toml index 63f3c5c007..e29542d27e 100644 --- a/extensions/zig/Cargo.toml +++ b/extensions/zig/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zed_zig" -version = "0.3.1" +version = "0.3.2" edition = "2021" publish = false license = "Apache-2.0" diff --git a/extensions/zig/extension.toml b/extensions/zig/extension.toml index bcd4f58555..380300683b 100644 --- a/extensions/zig/extension.toml +++ b/extensions/zig/extension.toml @@ -1,7 +1,7 @@ id = "zig" name = "Zig" description = "Zig support." -version = "0.3.1" +version = "0.3.2" schema_version = 1 authors = ["Allan Calix "] repository = "https://github.com/zed-industries/zed" diff --git a/extensions/zig/languages/zig/textobjects.scm b/extensions/zig/languages/zig/textobjects.scm new file mode 100644 index 0000000000..b08df97ea9 --- /dev/null +++ b/extensions/zig/languages/zig/textobjects.scm @@ -0,0 +1,27 @@ +(function_declaration + body: (_ + "{" + (_)* @function.inside + "}")) @function.around + +(test_declaration + (block + "{" + (_)* @function.inside + "}")) @function.around + +(variable_declaration + (struct_declaration + "struct" + "{" + [(_) ","]* @class.inside + "}")) @class.around + +(variable_declaration + (enum_declaration + "enum" + "{" + (_)* @class.inside + "}")) @class.around + +(comment)+ @comment.around diff --git a/script/language-extension-version b/script/language-extension-version index fc5c448736..d547886087 100755 --- a/script/language-extension-version +++ b/script/language-extension-version @@ -26,4 +26,3 @@ fi sed -i '' -e "s/^version = \".*\"/version = \"$VERSION\"/" "$EXTENSION_TOML" sed -i '' -e "s/^version = \".*\"/version = \"$VERSION\"/" "$CARGO_TOML" -cargo check From db34f293006d46ddaf798fa095cb8d4dd6afafd2 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 3 Dec 2024 13:18:19 -0800 Subject: [PATCH 266/886] vim: Add == and fix = in the status bar (#21490) cc @maxbrunsfeld Release Notes: - vim: Add == --- assets/keymaps/vim.json | 7 +++++++ crates/vim/src/state.rs | 1 + 2 files changed, 8 insertions(+) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 5f5933ef63..3c2197afcc 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -489,6 +489,13 @@ "<": "vim::CurrentLine" } }, + { + "context": "vim_operator == eq", + "use_layout_keys": true, + "bindings": { + "=": "vim::CurrentLine" + } + }, { "context": "vim_operator == gc", "use_layout_keys": true, diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index af187381ad..f43de2cf6f 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -487,6 +487,7 @@ impl Operator { Operator::Literal { prefix: Some(prefix), } => format!("^V{prefix}"), + Operator::AutoIndent => "=".to_string(), _ => self.id().to_string(), } } From aca23da9714e0482be8bee1b56695ac3ff1d4cc6 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 3 Dec 2024 16:25:09 -0500 Subject: [PATCH 267/886] assistant2: Render messages in the thread using a `list` (#21491) This PR updates the rendering of the messages in the current thread to use a `gpui::list`. Release Notes: - N/A --- crates/assistant2/src/assistant_panel.rs | 80 ++++++++++++++---------- crates/assistant2/src/message_editor.rs | 2 +- crates/assistant2/src/thread.rs | 15 +++-- 3 files changed, 57 insertions(+), 40 deletions(-) diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index 4e6b6ef227..b4ac2731e0 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -4,9 +4,9 @@ use anyhow::Result; use assistant_tool::ToolWorkingSet; use client::zed_urls; use gpui::{ - prelude::*, px, Action, AnyElement, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, - FocusableView, FontWeight, Model, Pixels, Subscription, Task, View, ViewContext, WeakView, - WindowContext, + list, prelude::*, px, Action, AnyElement, AppContext, AsyncWindowContext, Empty, EventEmitter, + FocusHandle, FocusableView, FontWeight, ListAlignment, ListState, Model, Pixels, Subscription, + Task, View, ViewContext, WeakView, WindowContext, }; use language_model::{LanguageModelRegistry, Role}; use language_model_selector::LanguageModelSelector; @@ -15,7 +15,7 @@ use workspace::dock::{DockPosition, Panel, PanelEvent}; use workspace::Workspace; use crate::message_editor::MessageEditor; -use crate::thread::{Message, Thread, ThreadError, ThreadEvent}; +use crate::thread::{MessageId, Thread, ThreadError, ThreadEvent}; use crate::thread_store::ThreadStore; use crate::{NewThread, ToggleFocus, ToggleModelSelector}; @@ -35,6 +35,8 @@ pub struct AssistantPanel { #[allow(unused)] thread_store: Model, thread: Model, + thread_messages: Vec, + thread_list_state: ListState, message_editor: View, tools: Arc, last_error: Option, @@ -77,6 +79,14 @@ impl AssistantPanel { workspace: workspace.weak_handle(), thread_store, thread: thread.clone(), + thread_messages: Vec::new(), + thread_list_state: ListState::new(0, ListAlignment::Bottom, px(1024.), { + let this = cx.view().downgrade(); + move |ix, cx: &mut WindowContext| { + this.update(cx, |this, cx| this.render_message(ix, cx)) + .unwrap() + } + }), message_editor: cx.new_view(|cx| MessageEditor::new(thread, cx)), tools, last_error: None, @@ -110,6 +120,12 @@ impl AssistantPanel { self.last_error = Some(error.clone()); } ThreadEvent::StreamedCompletion => {} + ThreadEvent::MessageAdded(message_id) => { + let old_len = self.thread_messages.len(); + self.thread_messages.push(*message_id); + self.thread_list_state.splice(old_len..old_len, 1); + cx.notify(); + } ThreadEvent::UsePendingTools => { let pending_tool_uses = self .thread @@ -301,31 +317,42 @@ impl AssistantPanel { ) } - fn render_message(&self, message: Message, cx: &mut ViewContext) -> impl IntoElement { + fn render_message(&self, ix: usize, cx: &mut ViewContext) -> AnyElement { + let message_id = self.thread_messages[ix]; + let Some(message) = self.thread.read(cx).message(message_id) else { + return Empty.into_any(); + }; + let (role_icon, role_name) = match message.role { Role::User => (IconName::Person, "You"), Role::Assistant => (IconName::ZedAssistant, "Assistant"), Role::System => (IconName::Settings, "System"), }; - v_flex() - .border_1() - .border_color(cx.theme().colors().border_variant) - .rounded_md() + div() + .id(("message-container", ix)) + .p_2() .child( - h_flex() - .justify_between() - .p_1p5() - .border_b_1() + v_flex() + .border_1() .border_color(cx.theme().colors().border_variant) + .rounded_md() .child( h_flex() - .gap_2() - .child(Icon::new(role_icon).size(IconSize::Small)) - .child(Label::new(role_name).size(LabelSize::Small)), - ), + .justify_between() + .p_1p5() + .border_b_1() + .border_color(cx.theme().colors().border_variant) + .child( + h_flex() + .gap_2() + .child(Icon::new(role_icon).size(IconSize::Small)) + .child(Label::new(role_name).size(LabelSize::Small)), + ), + ) + .child(v_flex().p_1p5().child(Label::new(message.text.clone()))), ) - .child(v_flex().p_1p5().child(Label::new(message.text.clone()))) + .into_any() } fn render_last_error(&self, cx: &mut ViewContext) -> Option { @@ -477,8 +504,6 @@ impl AssistantPanel { impl Render for AssistantPanel { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let messages = self.thread.read(cx).messages().cloned().collect::>(); - v_flex() .key_context("AssistantPanel2") .justify_between() @@ -487,20 +512,7 @@ impl Render for AssistantPanel { this.new_thread(cx); })) .child(self.render_toolbar(cx)) - .child( - v_flex() - .id("message-list") - .gap_2() - .size_full() - .p_2() - .overflow_y_scroll() - .bg(cx.theme().colors().panel_background) - .children( - messages - .into_iter() - .map(|message| self.render_message(message, cx)), - ), - ) + .child(list(self.thread_list_state.clone()).flex_1()) .child( h_flex() .border_t_1() diff --git a/crates/assistant2/src/message_editor.rs b/crates/assistant2/src/message_editor.rs index 7f789587c6..d1b1cf55e4 100644 --- a/crates/assistant2/src/message_editor.rs +++ b/crates/assistant2/src/message_editor.rs @@ -56,7 +56,7 @@ impl MessageEditor { }); self.thread.update(cx, |thread, cx| { - thread.insert_user_message(user_message); + thread.insert_user_message(user_message, cx); let mut request = thread.to_completion_request(request_kind, cx); if self.use_tools { diff --git a/crates/assistant2/src/thread.rs b/crates/assistant2/src/thread.rs index a5ab415a4d..43868fffff 100644 --- a/crates/assistant2/src/thread.rs +++ b/crates/assistant2/src/thread.rs @@ -63,8 +63,8 @@ impl Thread { } } - pub fn messages(&self) -> impl Iterator { - self.messages.iter() + pub fn message(&self, id: MessageId) -> Option<&Message> { + self.messages.iter().find(|message| message.id == id) } pub fn tools(&self) -> &Arc { @@ -75,12 +75,14 @@ impl Thread { self.pending_tool_uses_by_id.values().collect() } - pub fn insert_user_message(&mut self, text: impl Into) { + pub fn insert_user_message(&mut self, text: impl Into, cx: &mut ModelContext) { + let id = self.next_message_id.post_inc(); self.messages.push(Message { - id: self.next_message_id.post_inc(), + id, role: Role::User, text: text.into(), }); + cx.emit(ThreadEvent::MessageAdded(id)); } pub fn to_completion_request( @@ -150,11 +152,13 @@ impl Thread { thread.update(&mut cx, |thread, cx| { match event { LanguageModelCompletionEvent::StartMessage { .. } => { + let id = thread.next_message_id.post_inc(); thread.messages.push(Message { - id: thread.next_message_id.post_inc(), + id, role: Role::Assistant, text: String::new(), }); + cx.emit(ThreadEvent::MessageAdded(id)); } LanguageModelCompletionEvent::Stop(reason) => { stop_reason = reason; @@ -316,6 +320,7 @@ pub enum ThreadError { pub enum ThreadEvent { ShowError(ThreadError), StreamedCompletion, + MessageAdded(MessageId), UsePendingTools, ToolFinished { #[allow(unused)] From dc32ab25a0f76280ff0f1485333a729523840e27 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Tue, 3 Dec 2024 17:14:17 -0500 Subject: [PATCH 268/886] Open folds containing selections when jumping from multibuffer (#21433) When searching within a single buffer, activating a search result causes any fold containing the result to be unfolded. However, this didn't happen when jumping to a search result from a project-wide search multibuffer. This PR fixes that. Release Notes: - Fixed folds not opening when jumping from search results multibuffer --- crates/editor/src/editor.rs | 1 + crates/editor/src/editor_tests.rs | 70 ++++++++++++++++++++++++++++++- 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 88919f9295..2e6274ef8a 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -12876,6 +12876,7 @@ impl Editor { None => Autoscroll::newest(), }; let nav_history = editor.nav_history.take(); + editor.unfold_ranges(&ranges, false, true, cx); editor.change_selections(Some(autoscroll), cx, |s| { s.select_ranges(ranges); }); diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 044e2765ed..0c15719ab5 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -11567,7 +11567,7 @@ async fn test_multibuffer_reverts(cx: &mut gpui::TestAppContext) { } #[gpui::test] -async fn test_mutlibuffer_in_navigation_history(cx: &mut gpui::TestAppContext) { +async fn test_multibuffer_in_navigation_history(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); let cols = 4; @@ -11856,6 +11856,74 @@ async fn test_mutlibuffer_in_navigation_history(cx: &mut gpui::TestAppContext) { .unwrap(); } +#[gpui::test] +async fn test_multibuffer_unfold_on_jump(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let texts = ["{\n\tx\n}".to_owned(), "y".to_owned()]; + let buffers = texts + .clone() + .map(|txt| cx.new_model(|cx| Buffer::local(txt, cx))); + let multi_buffer = cx.new_model(|cx| { + let mut multi_buffer = MultiBuffer::new(ReadWrite); + for i in 0..2 { + multi_buffer.push_excerpts( + buffers[i].clone(), + [ExcerptRange { + context: 0..texts[i].len(), + primary: None, + }], + cx, + ); + } + multi_buffer + }); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + "/project", + json!({ + "x": &texts[0], + "y": &texts[1], + }), + ) + .await; + let project = Project::test(fs, ["/project".as_ref()], cx).await; + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx); + + let multi_buffer_editor = cx.new_view(|cx| { + Editor::for_multibuffer(multi_buffer.clone(), Some(project.clone()), true, cx) + }); + let buffer_editor = + cx.new_view(|cx| Editor::for_buffer(buffers[0].clone(), Some(project.clone()), cx)); + workspace + .update(cx, |workspace, cx| { + workspace.add_item_to_active_pane( + Box::new(multi_buffer_editor.clone()), + None, + true, + cx, + ); + workspace.add_item_to_active_pane(Box::new(buffer_editor.clone()), None, false, cx); + }) + .unwrap(); + cx.executor().run_until_parked(); + buffer_editor.update(cx, |buffer_editor, cx| { + buffer_editor.fold_at_level(&FoldAtLevel { level: 1 }, cx); + assert!(buffer_editor.snapshot(cx).fold_count() == 1); + }); + cx.executor().run_until_parked(); + multi_buffer_editor.update(cx, |multi_buffer_editor, cx| { + multi_buffer_editor.change_selections(None, cx, |s| s.select_ranges([3..4])); + multi_buffer_editor.open_excerpts(&OpenExcerpts, cx); + }); + cx.executor().run_until_parked(); + buffer_editor.update(cx, |buffer_editor, cx| { + assert!(buffer_editor.snapshot(cx).fold_count() == 0); + }); +} + #[gpui::test] async fn test_toggle_hunk_diff(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); From ecaf44511cd1a1efc6ded4b56f87f1435dbc0e07 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Tue, 3 Dec 2024 23:28:59 +0000 Subject: [PATCH 269/886] Fix Perplexity extension URL (#21495) --- extensions/perplexity/extension.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/perplexity/extension.toml b/extensions/perplexity/extension.toml index 205f8a5cc2..474d9ee981 100644 --- a/extensions/perplexity/extension.toml +++ b/extensions/perplexity/extension.toml @@ -3,7 +3,7 @@ name = "Perplexity" version = "0.1.0" description = "Ask questions to Perplexity AI directly from Zed" authors = ["Zed Industries "] -repository = "https://github.com/zed-industries/zed-perplexity" +repository = "https://github.com/zed-industries/zed" schema_version = 1 [slash_commands.perplexity] From 9f459ba573b8c029c89dbbc7a8edca6d1f7b712e Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 3 Dec 2024 18:32:13 -0500 Subject: [PATCH 270/886] assistant2: Render messages as Markdown (#21496) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR updates Assistant 2 to render the messages in the thread as Markdown: Screenshot 2024-12-03 at 6 09 27 PM Release Notes: - N/A --- Cargo.lock | 2 + crates/assistant2/Cargo.toml | 4 +- crates/assistant2/src/assistant_panel.rs | 74 +++++++++++++++++++++++- crates/assistant2/src/thread.rs | 5 ++ 4 files changed, 81 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6266df7d86..e1ff5d6dae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -464,10 +464,12 @@ dependencies = [ "feature_flags", "futures 0.3.31", "gpui", + "language", "language_model", "language_model_selector", "language_models", "log", + "markdown", "project", "proto", "serde", diff --git a/crates/assistant2/Cargo.toml b/crates/assistant2/Cargo.toml index 20e8dfbc9a..257183a4ac 100644 --- a/crates/assistant2/Cargo.toml +++ b/crates/assistant2/Cargo.toml @@ -23,15 +23,17 @@ editor.workspace = true feature_flags.workspace = true futures.workspace = true gpui.workspace = true +language.workspace = true language_model.workspace = true language_model_selector.workspace = true language_models.workspace = true log.workspace = true +markdown.workspace = true project.workspace = true proto.workspace = true -settings.workspace = true serde.workspace = true serde_json.workspace = true +settings.workspace = true smol.workspace = true theme.workspace = true ui.workspace = true diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index b4ac2731e0..b8ce5b1a36 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -3,13 +3,19 @@ use std::sync::Arc; use anyhow::Result; use assistant_tool::ToolWorkingSet; use client::zed_urls; +use collections::HashMap; use gpui::{ list, prelude::*, px, Action, AnyElement, AppContext, AsyncWindowContext, Empty, EventEmitter, - FocusHandle, FocusableView, FontWeight, ListAlignment, ListState, Model, Pixels, Subscription, - Task, View, ViewContext, WeakView, WindowContext, + FocusHandle, FocusableView, FontWeight, ListAlignment, ListState, Model, Pixels, + StyleRefinement, Subscription, Task, TextStyleRefinement, View, ViewContext, WeakView, + WindowContext, }; +use language::LanguageRegistry; use language_model::{LanguageModelRegistry, Role}; use language_model_selector::LanguageModelSelector; +use markdown::{Markdown, MarkdownStyle}; +use settings::Settings; +use theme::ThemeSettings; use ui::{prelude::*, ButtonLike, Divider, IconButtonShape, Tab, Tooltip}; use workspace::dock::{DockPosition, Panel, PanelEvent}; use workspace::Workspace; @@ -32,10 +38,12 @@ pub fn init(cx: &mut AppContext) { pub struct AssistantPanel { workspace: WeakView, + language_registry: Arc, #[allow(unused)] thread_store: Model, thread: Model, thread_messages: Vec, + rendered_messages_by_id: HashMap>, thread_list_state: ListState, message_editor: View, tools: Arc, @@ -77,9 +85,11 @@ impl AssistantPanel { Self { workspace: workspace.weak_handle(), + language_registry: workspace.project().read(cx).languages().clone(), thread_store, thread: thread.clone(), thread_messages: Vec::new(), + rendered_messages_by_id: HashMap::default(), thread_list_state: ListState::new(0, ListAlignment::Bottom, px(1024.), { let this = cx.view().downgrade(); move |ix, cx: &mut WindowContext| { @@ -104,6 +114,9 @@ impl AssistantPanel { self.message_editor = cx.new_view(|cx| MessageEditor::new(thread.clone(), cx)); self.thread = thread; + self.thread_messages.clear(); + self.thread_list_state.reset(0); + self.rendered_messages_by_id.clear(); self._subscriptions = subscriptions; self.message_editor.focus_handle(cx).focus(cx); @@ -120,10 +133,61 @@ impl AssistantPanel { self.last_error = Some(error.clone()); } ThreadEvent::StreamedCompletion => {} + ThreadEvent::StreamedAssistantText(message_id, text) => { + if let Some(markdown) = self.rendered_messages_by_id.get_mut(&message_id) { + markdown.update(cx, |markdown, cx| { + markdown.append(text, cx); + }); + } + } ThreadEvent::MessageAdded(message_id) => { let old_len = self.thread_messages.len(); self.thread_messages.push(*message_id); self.thread_list_state.splice(old_len..old_len, 1); + + if let Some(message_text) = self + .thread + .read(cx) + .message(*message_id) + .map(|message| message.text.clone()) + { + let theme_settings = ThemeSettings::get_global(cx); + + let mut text_style = cx.text_style(); + text_style.refine(&TextStyleRefinement { + font_family: Some(theme_settings.ui_font.family.clone()), + font_size: Some(TextSize::Default.rems(cx).into()), + color: Some(cx.theme().colors().text), + ..Default::default() + }); + + let markdown_style = MarkdownStyle { + base_text_style: text_style, + syntax: cx.theme().syntax().clone(), + selection_background_color: cx.theme().players().local().selection, + code_block: StyleRefinement { + text: Some(TextStyleRefinement { + font_family: Some(theme_settings.buffer_font.family.clone()), + font_size: Some(theme_settings.buffer_font_size.into()), + ..Default::default() + }), + ..Default::default() + }, + ..Default::default() + }; + + let markdown = cx.new_view(|cx| { + Markdown::new( + message_text, + markdown_style, + Some(self.language_registry.clone()), + None, + cx, + ) + }); + self.rendered_messages_by_id.insert(*message_id, markdown); + } + cx.notify(); } ThreadEvent::UsePendingTools => { @@ -323,6 +387,10 @@ impl AssistantPanel { return Empty.into_any(); }; + let Some(markdown) = self.rendered_messages_by_id.get(&message_id) else { + return Empty.into_any(); + }; + let (role_icon, role_name) = match message.role { Role::User => (IconName::Person, "You"), Role::Assistant => (IconName::ZedAssistant, "Assistant"), @@ -350,7 +418,7 @@ impl AssistantPanel { .child(Label::new(role_name).size(LabelSize::Small)), ), ) - .child(v_flex().p_1p5().child(Label::new(message.text.clone()))), + .child(v_flex().p_1p5().text_ui(cx).child(markdown.clone())), ) .into_any() } diff --git a/crates/assistant2/src/thread.rs b/crates/assistant2/src/thread.rs index 43868fffff..a841325884 100644 --- a/crates/assistant2/src/thread.rs +++ b/crates/assistant2/src/thread.rs @@ -167,6 +167,10 @@ impl Thread { if let Some(last_message) = thread.messages.last_mut() { if last_message.role == Role::Assistant { last_message.text.push_str(&chunk); + cx.emit(ThreadEvent::StreamedAssistantText( + last_message.id, + chunk, + )); } } } @@ -320,6 +324,7 @@ pub enum ThreadError { pub enum ThreadEvent { ShowError(ThreadError), StreamedCompletion, + StreamedAssistantText(MessageId, String), MessageAdded(MessageId), UsePendingTools, ToolFinished { From 3019960f83e6852a5b9ce3706f88addce3aa2983 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 3 Dec 2024 18:39:00 -0500 Subject: [PATCH 271/886] markdown: Make `cx` the last parameter to `Markdown::new_text` (#21497) This PR is a follow-up to https://github.com/zed-industries/zed/pull/21487 to make sure that the `cx` is the last parameter to `Markdown::new_text` as well. Release Notes: - N/A --- crates/editor/src/hover_popover.rs | 2 +- crates/markdown/src/markdown.rs | 2 +- crates/recent_projects/src/ssh_connections.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index c402132bf3..9cac7dc713 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -378,7 +378,7 @@ fn show_hover( }, ..Default::default() }; - Markdown::new_text(text, markdown_style.clone(), None, cx, None) + Markdown::new_text(text, markdown_style.clone(), None, None, cx) }) .ok(); diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index 39217b6930..cdb464877d 100644 --- a/crates/markdown/src/markdown.rs +++ b/crates/markdown/src/markdown.rs @@ -97,8 +97,8 @@ impl Markdown { source: String, style: MarkdownStyle, language_registry: Option>, - cx: &ViewContext, fallback_code_block_language: Option, + cx: &ViewContext, ) -> Self { let focus_handle = cx.focus_handle(); let mut this = Self { diff --git a/crates/recent_projects/src/ssh_connections.rs b/crates/recent_projects/src/ssh_connections.rs index a9aeacadd8..1c084bbf6e 100644 --- a/crates/recent_projects/src/ssh_connections.rs +++ b/crates/recent_projects/src/ssh_connections.rs @@ -201,7 +201,7 @@ impl SshPrompt { selection_background_color: cx.theme().players().local().selection, ..Default::default() }; - let markdown = cx.new_view(|cx| Markdown::new_text(prompt, markdown_style, None, cx, None)); + let markdown = cx.new_view(|cx| Markdown::new_text(prompt, markdown_style, None, None, cx)); self.prompt = Some((markdown, tx)); self.status_message.take(); cx.focus_view(&self.editor); From ce5f492404d24d0b1d071dd490ec13421cce7183 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Tue, 3 Dec 2024 23:22:26 -0500 Subject: [PATCH 272/886] Update rustls and sqlx (#21506) Release Notes: - N/A --- Cargo.lock | 59 ++++++++++++++++++++++------------------- Cargo.toml | 1 + crates/sqlez/Cargo.toml | 2 +- 3 files changed, 33 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e1ff5d6dae..21db0ceb99 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5774,7 +5774,7 @@ dependencies = [ "http 1.1.0", "hyper 1.5.0", "hyper-util", - "rustls 0.23.16", + "rustls 0.23.18", "rustls-native-certs 0.8.0", "rustls-pki-types", "tokio", @@ -6866,9 +6866,9 @@ dependencies = [ [[package]] name = "libsqlite3-sys" -version = "0.28.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" dependencies = [ "cc", "pkg-config", @@ -9512,7 +9512,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash 2.0.0", - "rustls 0.23.16", + "rustls 0.23.18", "socket2 0.5.7", "thiserror 2.0.3", "tokio", @@ -9530,7 +9530,7 @@ dependencies = [ "rand 0.8.5", "ring", "rustc-hash 2.0.0", - "rustls 0.23.16", + "rustls 0.23.18", "rustls-pki-types", "slab", "thiserror 2.0.3", @@ -10085,7 +10085,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.16", + "rustls 0.23.18", "rustls-native-certs 0.8.0", "rustls-pemfile 2.2.0", "rustls-pki-types", @@ -10473,9 +10473,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.16" +version = "0.23.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eee87ff5d9b36712a58574e12e9f0ea80f915a5b0ac518d322b24a465617925e" +checksum = "9c9cc1d47e243d655ace55ed38201c19ae02c148ae56412ab8750e8f0166ab7f" dependencies = [ "once_cell", "ring", @@ -11514,9 +11514,9 @@ dependencies = [ [[package]] name = "sqlx" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27144619c6e5802f1380337a209d2ac1c431002dd74c6e60aebff3c506dc4f0c" +checksum = "fcfa89bea9500db4a0d038513d7a060566bfc51d46d1c014847049a45cce85e8" dependencies = [ "sqlx-core", "sqlx-macros", @@ -11527,9 +11527,9 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a999083c1af5b5d6c071d34a708a19ba3e02106ad82ef7bbd69f5e48266b613b" +checksum = "d06e2f2bd861719b1f3f0c7dbe1d80c30bf59e76cf019f07d9014ed7eefb8e08" dependencies = [ "atoi", "bigdecimal", @@ -11555,8 +11555,8 @@ dependencies = [ "paste", "percent-encoding", "rust_decimal", - "rustls 0.21.12", - "rustls-pemfile 1.0.4", + "rustls 0.23.18", + "rustls-pemfile 2.2.0", "serde", "serde_json", "sha2", @@ -11569,14 +11569,14 @@ dependencies = [ "tracing", "url", "uuid", - "webpki-roots 0.25.4", + "webpki-roots 0.26.7", ] [[package]] name = "sqlx-macros" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23217eb7d86c584b8cbe0337b9eacf12ab76fe7673c513141ec42565698bb88" +checksum = "2f998a9defdbd48ed005a89362bd40dd2117502f15294f61c8d47034107dbbdc" dependencies = [ "proc-macro2", "quote", @@ -11587,9 +11587,9 @@ dependencies = [ [[package]] name = "sqlx-macros-core" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a099220ae541c5db479c6424bdf1b200987934033c2584f79a0e1693601e776" +checksum = "3d100558134176a2629d46cec0c8891ba0be8910f7896abfdb75ef4ab6f4e7ce" dependencies = [ "dotenvy", "either", @@ -11613,9 +11613,9 @@ dependencies = [ [[package]] name = "sqlx-mysql" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5afe4c38a9b417b6a9a5eeffe7235d0a106716495536e7727d1c7f4b1ff3eba6" +checksum = "936cac0ab331b14cb3921c62156d913e4c15b74fb6ec0f3146bd4ef6e4fb3c12" dependencies = [ "atoi", "base64 0.22.1", @@ -11660,9 +11660,9 @@ dependencies = [ [[package]] name = "sqlx-postgres" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1dbb157e65f10dbe01f729339c06d239120221c9ad9fa0ba8408c4cc18ecf21" +checksum = "9734dbce698c67ecf67c442f768a5e90a49b2a4d61a9f1d59f73874bd4cf0710" dependencies = [ "atoi", "base64 0.22.1", @@ -11704,9 +11704,9 @@ dependencies = [ [[package]] name = "sqlx-sqlite" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2cdd83c008a622d94499c0006d8ee5f821f36c89b7d625c900e5dc30b5c5ee" +checksum = "a75b419c3c1b1697833dd927bdc4c6545a620bc1bbafabd44e1efbe9afcd337e" dependencies = [ "atoi", "chrono", @@ -12780,7 +12780,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ - "rustls 0.23.16", + "rustls 0.23.18", "rustls-pki-types", "tokio", ] @@ -14448,9 +14448,12 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.25.4" +version = "0.26.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" +checksum = "5d642ff16b7e79272ae451b7322067cdc17cadf68c23264be9d94a32319efe7e" +dependencies = [ + "rustls-pki-types", +] [[package]] name = "weezl" diff --git a/Cargo.toml b/Cargo.toml index 0465545990..ab1e9d8e1a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -391,6 +391,7 @@ jsonwebtoken = "9.3" jupyter-protocol = { version = "0.5.0" } jupyter-websocket-client = { version = "0.8.0" } libc = "0.2" +libsqlite3-sys = { version = "0.30.1", features = ["bundled"] } linkify = "0.10.0" log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] } markup5ever_rcdom = "0.3.0" diff --git a/crates/sqlez/Cargo.toml b/crates/sqlez/Cargo.toml index 43626d7747..4204a45d80 100644 --- a/crates/sqlez/Cargo.toml +++ b/crates/sqlez/Cargo.toml @@ -13,7 +13,7 @@ anyhow.workspace = true collections.workspace = true futures.workspace = true indoc.workspace = true -libsqlite3-sys = { version = "0.28", features = ["bundled"] } +libsqlite3-sys.workspace = true parking_lot.workspace = true smol.workspace = true sqlformat.workspace = true From c5d15fd0653b90c5567912d0554bca946f643e1f Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Tue, 3 Dec 2024 23:23:16 -0500 Subject: [PATCH 273/886] Add FoldFunctionBodies editor action (#21504) Related to #19424 This uses the new text object support, so will only work for languages that have `textobjects.scm`. It does not integrate with indentation-based folding for now, and the syntax-based folds don't have matching fold markers in the gutter (unless they are folded). Release Notes: - Add an editor action to fold all function bodies Co-authored-by: Conrad --- crates/editor/src/actions.rs | 1 + crates/editor/src/editor.rs | 17 +++++++++++++++++ crates/editor/src/element.rs | 1 + crates/language/src/buffer.rs | 8 ++++++++ 4 files changed, 27 insertions(+) diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index a67dd55055..9a00f1efca 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -248,6 +248,7 @@ gpui::actions!( FindAllReferences, Fold, FoldAll, + FoldFunctionBodies, FoldRecursive, FoldSelectedRanges, ToggleFold, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2e6274ef8a..3901ba47aa 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -11097,6 +11097,23 @@ impl Editor { self.fold_creases(fold_ranges, true, cx); } + pub fn fold_function_bodies( + &mut self, + _: &actions::FoldFunctionBodies, + cx: &mut ViewContext, + ) { + let snapshot = self.buffer.read(cx).snapshot(cx); + let Some((_, _, buffer)) = snapshot.as_singleton() else { + return; + }; + let creases = buffer + .function_body_fold_ranges(0..buffer.len()) + .map(|range| Crease::simple(range, self.display_map.read(cx).fold_placeholder.clone())) + .collect(); + + self.fold_creases(creases, true, cx); + } + pub fn fold_recursive(&mut self, _: &actions::FoldRecursive, cx: &mut ViewContext) { let mut to_fold = Vec::new(); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 975f1b8bf0..a82820f265 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -342,6 +342,7 @@ impl EditorElement { register_action(view, cx, Editor::fold); register_action(view, cx, Editor::fold_at_level); register_action(view, cx, Editor::fold_all); + register_action(view, cx, Editor::fold_function_bodies); register_action(view, cx, Editor::fold_at); register_action(view, cx, Editor::fold_recursive); register_action(view, cx, Editor::toggle_fold); diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index f3b6cb51ad..67b33ebd56 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -3355,6 +3355,14 @@ impl BufferSnapshot { }) } + pub fn function_body_fold_ranges( + &self, + within: Range, + ) -> impl Iterator> + '_ { + self.text_object_ranges(within, TreeSitterOptions::default()) + .filter_map(|(range, obj)| (obj == TextObject::InsideFunction).then_some(range)) + } + /// For each grammar in the language, runs the provided /// [`tree_sitter::Query`] against the given range. pub fn matches( From 8f08787cf0ce40521845bbf9a43d0945c8113ca4 Mon Sep 17 00:00:00 2001 From: Waleed Dahshan <58462210+wmstack@users.noreply.github.com> Date: Wed, 4 Dec 2024 17:19:52 +1100 Subject: [PATCH 274/886] Implement Helix Support (WIP) (#19175) Closes #4642 - Added the ability to switch to helix normal mode, with an additional helix visual mode. - ctrlh from Insert mode goes to Helix Normal mode. i and a to go back. - Need to find a way to perform the helix normal mode selection with w , e , b as a first step. Need to figure out how the mode will interoperate with the VIM mode as the new additions are in the same crate. --- assets/keymaps/vim.json | 16 ++ crates/editor/src/movement.rs | 95 ++++++++ crates/language/src/buffer.rs | 10 +- crates/text/src/selection.rs | 25 +++ crates/vim/src/helix.rs | 271 +++++++++++++++++++++++ crates/vim/src/motion.rs | 4 + crates/vim/src/normal/case.rs | 2 + crates/vim/src/object.rs | 2 +- crates/vim/src/state.rs | 3 + crates/vim/src/test/neovim_connection.rs | 1 + crates/vim/src/vim.rs | 27 ++- 11 files changed, 444 insertions(+), 12 deletions(-) create mode 100644 crates/vim/src/helix.rs diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 3c2197afcc..c80a6912cc 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -326,6 +326,22 @@ "ctrl-o": "vim::TemporaryNormal" } }, + { + "context": "vim_mode == helix_normal", + "bindings": { + "i": "vim::InsertBefore", + "a": "vim::InsertAfter", + "w": "vim::NextWordStart", + "e": "vim::NextWordEnd", + "b": "vim::PreviousWordStart", + + "h": "vim::Left", + "j": "vim::Down", + "k": "vim::Up", + "l": "vim::Right" + } + }, + { "context": "vim_mode == insert && !(showing_code_actions || showing_completions)", "use_layout_keys": true, diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 52bedde2e3..8189dd2947 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -488,6 +488,101 @@ pub fn find_boundary_point( map.clip_point(offset.to_display_point(map), Bias::Right) } +pub fn find_preceding_boundary_trail( + map: &DisplaySnapshot, + head: DisplayPoint, + mut is_boundary: impl FnMut(char, char) -> bool, +) -> (Option, DisplayPoint) { + let mut offset = head.to_offset(map, Bias::Left); + let mut trail_offset = None; + + let mut prev_ch = map.buffer_snapshot.chars_at(offset).next(); + let mut forward = map.buffer_snapshot.reversed_chars_at(offset).peekable(); + + // Skip newlines + while let Some(&ch) = forward.peek() { + if ch == '\n' { + prev_ch = forward.next(); + offset -= ch.len_utf8(); + trail_offset = Some(offset); + } else { + break; + } + } + + // Find the boundary + let start_offset = offset; + for ch in forward { + if let Some(prev_ch) = prev_ch { + if is_boundary(prev_ch, ch) { + if start_offset == offset { + trail_offset = Some(offset); + } else { + break; + } + } + } + offset -= ch.len_utf8(); + prev_ch = Some(ch); + } + + let trail = trail_offset + .map(|trail_offset: usize| map.clip_point(trail_offset.to_display_point(map), Bias::Left)); + + ( + trail, + map.clip_point(offset.to_display_point(map), Bias::Left), + ) +} + +/// Finds the location of a boundary +pub fn find_boundary_trail( + map: &DisplaySnapshot, + head: DisplayPoint, + mut is_boundary: impl FnMut(char, char) -> bool, +) -> (Option, DisplayPoint) { + let mut offset = head.to_offset(map, Bias::Right); + let mut trail_offset = None; + + let mut prev_ch = map.buffer_snapshot.reversed_chars_at(offset).next(); + let mut forward = map.buffer_snapshot.chars_at(offset).peekable(); + + // Skip newlines + while let Some(&ch) = forward.peek() { + if ch == '\n' { + prev_ch = forward.next(); + offset += ch.len_utf8(); + trail_offset = Some(offset); + } else { + break; + } + } + + // Find the boundary + let start_offset = offset; + for ch in forward { + if let Some(prev_ch) = prev_ch { + if is_boundary(prev_ch, ch) { + if start_offset == offset { + trail_offset = Some(offset); + } else { + break; + } + } + } + offset += ch.len_utf8(); + prev_ch = Some(ch); + } + + let trail = trail_offset + .map(|trail_offset: usize| map.clip_point(trail_offset.to_display_point(map), Bias::Right)); + + ( + trail, + map.clip_point(offset.to_display_point(map), Bias::Right), + ) +} + pub fn find_boundary( map: &DisplaySnapshot, from: DisplayPoint, diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 67b33ebd56..c9f5d54299 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -4632,7 +4632,7 @@ impl CharClassifier { self.kind(c) == CharKind::Punctuation } - pub fn kind(&self, c: char) -> CharKind { + pub fn kind_with(&self, c: char, ignore_punctuation: bool) -> CharKind { if c.is_whitespace() { return CharKind::Whitespace; } else if c.is_alphanumeric() || c == '_' { @@ -4642,7 +4642,7 @@ impl CharClassifier { if let Some(scope) = &self.scope { if let Some(characters) = scope.word_characters() { if characters.contains(&c) { - if c == '-' && !self.for_completion && !self.ignore_punctuation { + if c == '-' && !self.for_completion && !ignore_punctuation { return CharKind::Punctuation; } return CharKind::Word; @@ -4650,12 +4650,16 @@ impl CharClassifier { } } - if self.ignore_punctuation { + if ignore_punctuation { CharKind::Word } else { CharKind::Punctuation } } + + pub fn kind(&self, c: char) -> CharKind { + self.kind_with(c, self.ignore_punctuation) + } } /// Find all of the ranges of whitespace that occur at the ends of lines diff --git a/crates/text/src/selection.rs b/crates/text/src/selection.rs index 94c373d630..fffece26b2 100644 --- a/crates/text/src/selection.rs +++ b/crates/text/src/selection.rs @@ -84,6 +84,31 @@ impl Selection { } self.goal = new_goal; } + + pub fn set_tail(&mut self, tail: T, new_goal: SelectionGoal) { + if tail.cmp(&self.head()) <= Ordering::Equal { + if self.reversed { + self.end = self.start; + self.reversed = false; + } + self.start = tail; + } else { + if !self.reversed { + self.start = self.end; + self.reversed = true; + } + self.end = tail; + } + self.goal = new_goal; + } + + pub fn swap_head_tail(&mut self) { + if self.reversed { + self.reversed = false; + } else { + std::mem::swap(&mut self.start, &mut self.end); + } + } } impl Selection { diff --git a/crates/vim/src/helix.rs b/crates/vim/src/helix.rs new file mode 100644 index 0000000000..21abb5cbaa --- /dev/null +++ b/crates/vim/src/helix.rs @@ -0,0 +1,271 @@ +use editor::{movement, scroll::Autoscroll, DisplayPoint, Editor}; +use gpui::{actions, Action}; +use language::{CharClassifier, CharKind}; +use ui::ViewContext; + +use crate::{motion::Motion, state::Mode, Vim}; + +actions!(vim, [HelixNormalAfter]); + +pub fn register(editor: &mut Editor, cx: &mut ViewContext) { + Vim::action(editor, cx, Vim::helix_normal_after); +} + +impl Vim { + pub fn helix_normal_after(&mut self, action: &HelixNormalAfter, cx: &mut ViewContext) { + if self.active_operator().is_some() { + self.operator_stack.clear(); + self.sync_vim_settings(cx); + return; + } + self.stop_recording_immediately(action.boxed_clone(), cx); + self.switch_mode(Mode::HelixNormal, false, cx); + return; + } + + pub fn helix_normal_motion( + &mut self, + motion: Motion, + times: Option, + cx: &mut ViewContext, + ) { + self.helix_move_cursor(motion, times, cx); + } + + fn helix_find_range_forward( + &mut self, + times: Option, + cx: &mut ViewContext, + mut is_boundary: impl FnMut(char, char, &CharClassifier) -> bool, + ) { + self.update_editor(cx, |_, editor, cx| { + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.move_with(|map, selection| { + let times = times.unwrap_or(1); + + if selection.head() == map.max_point() { + return; + } + + // collapse to block cursor + if selection.tail() < selection.head() { + selection.set_tail(movement::left(map, selection.head()), selection.goal); + } else { + selection.set_tail(selection.head(), selection.goal); + selection.set_head(movement::right(map, selection.head()), selection.goal); + } + + // create a classifier + let classifier = map + .buffer_snapshot + .char_classifier_at(selection.head().to_point(map)); + + let mut last_selection = selection.clone(); + for _ in 0..times { + let (new_tail, new_head) = + movement::find_boundary_trail(map, selection.head(), |left, right| { + is_boundary(left, right, &classifier) + }); + + selection.set_head(new_head, selection.goal); + if let Some(new_tail) = new_tail { + selection.set_tail(new_tail, selection.goal); + } + + if selection.head() == last_selection.head() + && selection.tail() == last_selection.tail() + { + break; + } + last_selection = selection.clone(); + } + }); + }); + }); + } + + fn helix_find_range_backward( + &mut self, + times: Option, + cx: &mut ViewContext, + mut is_boundary: impl FnMut(char, char, &CharClassifier) -> bool, + ) { + self.update_editor(cx, |_, editor, cx| { + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.move_with(|map, selection| { + let times = times.unwrap_or(1); + + if selection.head() == DisplayPoint::zero() { + return; + } + + // collapse to block cursor + if selection.tail() < selection.head() { + selection.set_tail(movement::left(map, selection.head()), selection.goal); + } else { + selection.set_tail(selection.head(), selection.goal); + selection.set_head(movement::right(map, selection.head()), selection.goal); + } + + // flip the selection + selection.swap_head_tail(); + + // create a classifier + let classifier = map + .buffer_snapshot + .char_classifier_at(selection.head().to_point(map)); + + let mut last_selection = selection.clone(); + for _ in 0..times { + let (new_tail, new_head) = movement::find_preceding_boundary_trail( + map, + selection.head(), + |left, right| is_boundary(left, right, &classifier), + ); + + selection.set_head(new_head, selection.goal); + if let Some(new_tail) = new_tail { + selection.set_tail(new_tail, selection.goal); + } + + if selection.head() == last_selection.head() + && selection.tail() == last_selection.tail() + { + break; + } + last_selection = selection.clone(); + } + }); + }) + }); + } + + pub fn helix_move_and_collapse( + &mut self, + motion: Motion, + times: Option, + cx: &mut ViewContext, + ) { + self.update_editor(cx, |_, editor, cx| { + let text_layout_details = editor.text_layout_details(cx); + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.move_with(|map, selection| { + let goal = selection.goal; + let cursor = if selection.is_empty() || selection.reversed { + selection.head() + } else { + movement::left(map, selection.head()) + }; + + let (point, goal) = motion + .move_point(map, cursor, selection.goal, times, &text_layout_details) + .unwrap_or((cursor, goal)); + + selection.collapse_to(point, goal) + }) + }); + }); + } + + pub fn helix_move_cursor( + &mut self, + motion: Motion, + times: Option, + cx: &mut ViewContext, + ) { + match motion { + Motion::NextWordStart { ignore_punctuation } => { + self.helix_find_range_forward(times, cx, |left, right, classifier| { + let left_kind = classifier.kind_with(left, ignore_punctuation); + let right_kind = classifier.kind_with(right, ignore_punctuation); + let at_newline = right == '\n'; + + let found = + left_kind != right_kind && right_kind != CharKind::Whitespace || at_newline; + + found + }) + } + Motion::NextWordEnd { ignore_punctuation } => { + self.helix_find_range_forward(times, cx, |left, right, classifier| { + let left_kind = classifier.kind_with(left, ignore_punctuation); + let right_kind = classifier.kind_with(right, ignore_punctuation); + let at_newline = right == '\n'; + + let found = left_kind != right_kind + && (left_kind != CharKind::Whitespace || at_newline); + + found + }) + } + Motion::PreviousWordStart { ignore_punctuation } => { + self.helix_find_range_backward(times, cx, |left, right, classifier| { + let left_kind = classifier.kind_with(left, ignore_punctuation); + let right_kind = classifier.kind_with(right, ignore_punctuation); + let at_newline = right == '\n'; + + let found = left_kind != right_kind + && (left_kind != CharKind::Whitespace || at_newline); + + found + }) + } + Motion::PreviousWordEnd { ignore_punctuation } => { + self.helix_find_range_backward(times, cx, |left, right, classifier| { + let left_kind = classifier.kind_with(left, ignore_punctuation); + let right_kind = classifier.kind_with(right, ignore_punctuation); + let at_newline = right == '\n'; + + let found = left_kind != right_kind + && right_kind != CharKind::Whitespace + && !at_newline; + + found + }) + } + _ => self.helix_move_and_collapse(motion, times, cx), + } + } +} + +#[cfg(test)] +mod test { + use indoc::indoc; + + use crate::{state::Mode, test::VimTestContext}; + + #[gpui::test] + async fn test_next_word_start(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + // « + // ˇ + // » + cx.set_state( + indoc! {" + The quˇick brown + fox jumps over + the lazy dog."}, + Mode::HelixNormal, + ); + + cx.simulate_keystrokes("w"); + + cx.assert_state( + indoc! {" + The qu«ick ˇ»brown + fox jumps over + the lazy dog."}, + Mode::HelixNormal, + ); + + cx.simulate_keystrokes("w"); + + cx.assert_state( + indoc! {" + The quick «brownˇ» + fox jumps over + the lazy dog."}, + Mode::HelixNormal, + ); + } +} diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index eb6e8464a3..08cf219722 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -529,6 +529,8 @@ impl Vim { return; } } + + Mode::HelixNormal => {} } } @@ -558,6 +560,8 @@ impl Vim { Mode::Visual | Mode::VisualLine | Mode::VisualBlock => { self.visual_motion(motion.clone(), count, cx) } + + Mode::HelixNormal => self.helix_normal_motion(motion.clone(), count, cx), } self.clear_operator(cx); if let Some(operator) = waiting_operator { diff --git a/crates/vim/src/normal/case.rs b/crates/vim/src/normal/case.rs index 0aeb4c7e98..405185adf5 100644 --- a/crates/vim/src/normal/case.rs +++ b/crates/vim/src/normal/case.rs @@ -145,6 +145,8 @@ impl Vim { cursor_positions.push(selection.start..selection.start); } } + + Mode::HelixNormal => {} Mode::Insert | Mode::Normal | Mode::Replace => { let start = selection.start; let mut end = start; diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index 380acc896a..b6f164cdb1 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -143,7 +143,7 @@ impl Vim { match self.mode { Mode::Normal => self.normal_object(object, cx), Mode::Visual | Mode::VisualLine | Mode::VisualBlock => self.visual_object(object, cx), - Mode::Insert | Mode::Replace => { + Mode::Insert | Mode::Replace | Mode::HelixNormal => { // Shouldn't execute a text object in insert mode. Ignoring } } diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index f43de2cf6f..e93eeef404 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -26,6 +26,7 @@ pub enum Mode { Visual, VisualLine, VisualBlock, + HelixNormal, } impl Display for Mode { @@ -37,6 +38,7 @@ impl Display for Mode { Mode::Visual => write!(f, "VISUAL"), Mode::VisualLine => write!(f, "VISUAL LINE"), Mode::VisualBlock => write!(f, "VISUAL BLOCK"), + Mode::HelixNormal => write!(f, "HELIX NORMAL"), } } } @@ -46,6 +48,7 @@ impl Mode { match self { Mode::Normal | Mode::Insert | Mode::Replace => false, Mode::Visual | Mode::VisualLine | Mode::VisualBlock => true, + Mode::HelixNormal => false, } } } diff --git a/crates/vim/src/test/neovim_connection.rs b/crates/vim/src/test/neovim_connection.rs index a2ab1f3972..a0a2343bdf 100644 --- a/crates/vim/src/test/neovim_connection.rs +++ b/crates/vim/src/test/neovim_connection.rs @@ -442,6 +442,7 @@ impl NeovimConnection { } Mode::Insert | Mode::Normal | Mode::Replace => selections .push(Point::new(selection_row, selection_col)..Point::new(cursor_row, cursor_col)), + Mode::HelixNormal => unreachable!(), } let ranges = encode_ranges(&text, &selections); diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index db0a765170..c395e9c37e 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -6,6 +6,7 @@ mod test; mod change_list; mod command; mod digraph; +mod helix; mod indent; mod insert; mod mode_indicator; @@ -337,6 +338,7 @@ impl Vim { normal::register(editor, cx); insert::register(editor, cx); + helix::register(editor, cx); motion::register(editor, cx); command::register(editor, cx); replace::register(editor, cx); @@ -631,7 +633,9 @@ impl Vim { } } Mode::Replace => CursorShape::Underline, - Mode::Visual | Mode::VisualLine | Mode::VisualBlock => CursorShape::Block, + Mode::HelixNormal | Mode::Visual | Mode::VisualLine | Mode::VisualBlock => { + CursorShape::Block + } Mode::Insert => CursorShape::Bar, } } @@ -645,9 +649,12 @@ impl Vim { true } } - Mode::Normal | Mode::Replace | Mode::Visual | Mode::VisualLine | Mode::VisualBlock => { - false - } + Mode::Normal + | Mode::HelixNormal + | Mode::Replace + | Mode::Visual + | Mode::VisualLine + | Mode::VisualBlock => false, } } @@ -657,9 +664,12 @@ impl Vim { pub fn clip_at_line_ends(&self) -> bool { match self.mode { - Mode::Insert | Mode::Visual | Mode::VisualLine | Mode::VisualBlock | Mode::Replace => { - false - } + Mode::Insert + | Mode::Visual + | Mode::VisualLine + | Mode::VisualBlock + | Mode::Replace + | Mode::HelixNormal => false, Mode::Normal => true, } } @@ -670,6 +680,7 @@ impl Vim { Mode::Visual | Mode::VisualLine | Mode::VisualBlock => "visual", Mode::Insert => "insert", Mode::Replace => "replace", + Mode::HelixNormal => "helix_normal", } .to_string(); @@ -998,7 +1009,7 @@ impl Vim { }) }); } - Mode::Insert | Mode::Replace => {} + Mode::Insert | Mode::Replace | Mode::HelixNormal => {} } } From e231321655a170ca30438c1040da053432885b6d Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 3 Dec 2024 22:20:25 -0800 Subject: [PATCH 275/886] Fix panic in update_ime_position (#21510) This can call back into the app, so must be done when the platform lock is not held. Release Notes: - Fixes a (rare) panic when changing tab --- crates/gpui/src/platform/mac/window.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 9266f81f74..8ea7ebd4d5 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -1111,10 +1111,16 @@ impl PlatformWindow for MacWindow { } fn update_ime_position(&self, _bounds: Bounds) { - unsafe { - let input_context: id = msg_send![class!(NSTextInputContext), currentInputContext]; - let _: () = msg_send![input_context, invalidateCharacterCoordinates]; - } + let executor = self.0.lock().executor.clone(); + executor + .spawn(async move { + unsafe { + let input_context: id = + msg_send![class!(NSTextInputContext), currentInputContext]; + let _: () = msg_send![input_context, invalidateCharacterCoordinates]; + } + }) + .detach() } } From 196fd65601af0b22cba3f6f0fbbda821b0403427 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 3 Dec 2024 23:01:32 -0800 Subject: [PATCH 276/886] Fix panic folding in multi-buffers (#21511) Closes #19054 Rename `max_buffer_row()` to `widest_line_number()` to (hopefully) prevent people assuming it means the same as `max_point().row`. Release Notes: - Fixed a panic when folding in a multibuffer --- crates/editor/src/display_map.rs | 13 ++++++------- crates/editor/src/display_map/inlay_map.rs | 2 +- crates/editor/src/editor.rs | 14 +++++++++++--- crates/editor/src/element.rs | 8 +------- crates/editor/src/movement.rs | 6 +++--- crates/multi_buffer/src/multi_buffer.rs | 19 +++++++++++-------- crates/vim/src/command.rs | 12 +++++++----- crates/vim/src/motion.rs | 2 +- crates/vim/src/normal/yank.rs | 4 ++-- crates/vim/src/object.rs | 6 +++--- 10 files changed, 46 insertions(+), 40 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index b95c9312c5..a75c2ce9fa 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -684,8 +684,8 @@ impl DisplaySnapshot { .map(|row| row.map(MultiBufferRow)) } - pub fn max_buffer_row(&self) -> MultiBufferRow { - self.buffer_snapshot.max_buffer_row() + pub fn widest_line_number(&self) -> u32 { + self.buffer_snapshot.widest_line_number() } pub fn prev_line_boundary(&self, mut point: MultiBufferPoint) -> (Point, DisplayPoint) { @@ -726,11 +726,10 @@ impl DisplaySnapshot { // used by line_mode selections and tries to match vim behavior pub fn expand_to_line(&self, range: Range) -> Range { + let max_row = self.buffer_snapshot.max_row().0; let new_start = if range.start.row == 0 { MultiBufferPoint::new(0, 0) - } else if range.start.row == self.max_buffer_row().0 - || (range.end.column > 0 && range.end.row == self.max_buffer_row().0) - { + } else if range.start.row == max_row || (range.end.column > 0 && range.end.row == max_row) { MultiBufferPoint::new( range.start.row - 1, self.buffer_snapshot @@ -742,7 +741,7 @@ impl DisplaySnapshot { let new_end = if range.end.column == 0 { range.end - } else if range.end.row < self.max_buffer_row().0 { + } else if range.end.row < max_row { self.buffer_snapshot .clip_point(MultiBufferPoint::new(range.end.row + 1, 0), Bias::Left) } else { @@ -1127,7 +1126,7 @@ impl DisplaySnapshot { } pub fn starts_indent(&self, buffer_row: MultiBufferRow) -> bool { - let max_row = self.buffer_snapshot.max_buffer_row(); + let max_row = self.buffer_snapshot.max_row(); if buffer_row >= max_row { return false; } diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 673b9383bc..4598a5c015 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -1019,7 +1019,7 @@ impl InlaySnapshot { let inlay_point = InlayPoint::new(row, 0); cursor.seek(&inlay_point, Bias::Left, &()); - let max_buffer_row = MultiBufferRow(self.buffer.max_point().row); + let max_buffer_row = self.buffer.max_row(); let mut buffer_point = cursor.start().1; let buffer_row = if row == 0 { MultiBufferRow(0) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 3901ba47aa..ea03d027c4 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -6502,7 +6502,7 @@ impl Editor { let mut revert_changes = HashMap::default(); let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx); for hunk in hunks_for_rows( - Some(MultiBufferRow(0)..multi_buffer_snapshot.max_buffer_row()).into_iter(), + Some(MultiBufferRow(0)..multi_buffer_snapshot.max_row()).into_iter(), &multi_buffer_snapshot, ) { Self::prepare_revert_change(&mut revert_changes, self.buffer(), &hunk, cx); @@ -11051,10 +11051,14 @@ impl Editor { } fn fold_at_level(&mut self, fold_at: &FoldAtLevel, cx: &mut ViewContext) { + if !self.buffer.read(cx).is_singleton() { + return; + } + let fold_at_level = fold_at.level; let snapshot = self.buffer.read(cx).snapshot(cx); let mut to_fold = Vec::new(); - let mut stack = vec![(0, snapshot.max_buffer_row().0, 1)]; + let mut stack = vec![(0, snapshot.max_row().0, 1)]; while let Some((mut start_row, end_row, current_level)) = stack.pop() { while start_row < end_row { @@ -11083,10 +11087,14 @@ impl Editor { } pub fn fold_all(&mut self, _: &actions::FoldAll, cx: &mut ViewContext) { + if !self.buffer.read(cx).is_singleton() { + return; + } + let mut fold_ranges = Vec::new(); let snapshot = self.buffer.read(cx).snapshot(cx); - for row in 0..snapshot.max_buffer_row().0 { + for row in 0..snapshot.max_row().0 { if let Some(foldable_range) = self.snapshot(cx).crease_for_buffer_row(MultiBufferRow(row)) { diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index a82820f265..2bb40c4602 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -4141,13 +4141,7 @@ impl EditorElement { } fn max_line_number_width(&self, snapshot: &EditorSnapshot, cx: &WindowContext) -> Pixels { - let digit_count = snapshot - .max_buffer_row() - .next_row() - .as_f32() - .log10() - .floor() as usize - + 1; + let digit_count = (snapshot.widest_line_number() as f32).log10().floor() as usize + 1; self.column_pixels(digit_count, cx) } } diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 8189dd2947..8fbf0d16f1 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -2,7 +2,7 @@ //! in editor given a given motion (e.g. it handles converting a "move left" command into coordinates in editor). It is exposed mostly for use by vim crate. use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint}; -use crate::{scroll::ScrollAnchor, CharKind, DisplayRow, EditorStyle, RowExt, ToOffset, ToPoint}; +use crate::{scroll::ScrollAnchor, CharKind, DisplayRow, EditorStyle, ToOffset, ToPoint}; use gpui::{Pixels, WindowTextSystem}; use language::Point; use multi_buffer::{MultiBufferRow, MultiBufferSnapshot}; @@ -382,12 +382,12 @@ pub fn end_of_paragraph( mut count: usize, ) -> DisplayPoint { let point = display_point.to_point(map); - if point.row == map.max_buffer_row().0 { + if point.row == map.buffer_snapshot.max_row().0 { return map.max_point(); } let mut found_non_blank_line = false; - for row in point.row..map.max_buffer_row().next_row().0 { + for row in point.row..=map.buffer_snapshot.max_row().0 { let blank = map.buffer_snapshot.is_line_blank(MultiBufferRow(row)); if found_non_blank_line && blank { if count <= 1 { diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 461498d00d..d52d65bca2 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -281,8 +281,7 @@ pub struct ExcerptSummary { excerpt_id: ExcerptId, /// The location of the last [`Excerpt`] being summarized excerpt_locator: Locator, - /// The maximum row of the [`Excerpt`]s being summarized - max_buffer_row: MultiBufferRow, + widest_line_number: u32, text: TextSummary, } @@ -2556,8 +2555,8 @@ impl MultiBufferSnapshot { self.excerpts.summary().text.len == 0 } - pub fn max_buffer_row(&self) -> MultiBufferRow { - self.excerpts.summary().max_buffer_row + pub fn widest_line_number(&self) -> u32 { + self.excerpts.summary().widest_line_number + 1 } pub fn clip_offset(&self, offset: usize, bias: Bias) -> usize { @@ -3026,6 +3025,10 @@ impl MultiBufferSnapshot { self.text_summary().lines } + pub fn max_row(&self) -> MultiBufferRow { + MultiBufferRow(self.text_summary().lines.row) + } + pub fn text_summary(&self) -> TextSummary { self.excerpts.summary().text.clone() } @@ -4824,7 +4827,7 @@ impl sum_tree::Item for Excerpt { ExcerptSummary { excerpt_id: self.id, excerpt_locator: self.locator.clone(), - max_buffer_row: MultiBufferRow(self.max_buffer_row), + widest_line_number: self.max_buffer_row, text, } } @@ -4869,7 +4872,7 @@ impl sum_tree::Summary for ExcerptSummary { debug_assert!(summary.excerpt_locator > self.excerpt_locator); self.excerpt_locator = summary.excerpt_locator.clone(); self.text.add_summary(&summary.text, &()); - self.max_buffer_row = cmp::max(self.max_buffer_row, summary.max_buffer_row); + self.widest_line_number = cmp::max(self.widest_line_number, summary.widest_line_number); } } @@ -6383,8 +6386,8 @@ mod tests { } assert_eq!( - snapshot.max_buffer_row().0, - expected_buffer_rows.into_iter().flatten().max().unwrap() + snapshot.widest_line_number(), + expected_buffer_rows.into_iter().flatten().max().unwrap() + 1 ); let mut excerpt_starts = excerpt_starts.into_iter(); diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index 5a958da012..68aefc8cd7 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -136,7 +136,7 @@ pub fn register(editor: &mut Editor, cx: &mut ViewContext) { vim.update_editor(cx, |vim, editor, cx| { let snapshot = editor.snapshot(cx); if let Ok(range) = action.range.buffer_range(vim, editor, cx) { - let end = if range.end < snapshot.max_buffer_row() { + let end = if range.end < snapshot.buffer_snapshot.max_row() { Point::new(range.end.0 + 1, 0) } else { snapshot.buffer_snapshot.max_point() @@ -436,9 +436,11 @@ impl Position { .row .saturating_add_signed(*offset) } - Position::LastLine { offset } => { - snapshot.max_buffer_row().0.saturating_add_signed(*offset) - } + Position::LastLine { offset } => snapshot + .buffer_snapshot + .max_row() + .0 + .saturating_add_signed(*offset), Position::CurrentLine { offset } => editor .selections .newest_anchor() @@ -448,7 +450,7 @@ impl Position { .saturating_add_signed(*offset), }; - Ok(MultiBufferRow(target).min(snapshot.max_buffer_row())) + Ok(MultiBufferRow(target).min(snapshot.buffer_snapshot.max_row())) } } diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 08cf219722..0e236861b6 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -1866,7 +1866,7 @@ fn end_of_document( let new_row = if let Some(line) = line { (line - 1) as u32 } else { - map.max_buffer_row().0 + map.buffer_snapshot.max_row().0 }; let new_point = Point::new(new_row, point.column()); diff --git a/crates/vim/src/normal/yank.rs b/crates/vim/src/normal/yank.rs index 763f1a3d16..85c6531f6a 100644 --- a/crates/vim/src/normal/yank.rs +++ b/crates/vim/src/normal/yank.rs @@ -154,9 +154,9 @@ impl Vim { // contains a newline (so that delete works as expected). We undo that change // here. let is_last_line = linewise - && end.row == buffer.max_buffer_row().0 + && end.row == buffer.max_row().0 && buffer.max_point().column > 0 - && start.row < buffer.max_buffer_row().0 + && start.row < buffer.max_row().0 && start == Point::new(start.row, buffer.line_len(MultiBufferRow(start.row))); if is_last_line { diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index b6f164cdb1..c63cb0e843 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -724,7 +724,7 @@ fn indent( // Loop forwards until we find a non-blank line with less indent let mut end_row = row; - let max_rows = map.max_buffer_row().0; + let max_rows = map.buffer_snapshot.max_row().0; for next_row in (row + 1)..=max_rows { let indent = map.line_indent_for_buffer_row(MultiBufferRow(next_row)); if indent.is_line_empty() { @@ -958,13 +958,13 @@ pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> /// The trailing newline is excluded from the paragraph. pub fn end_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint { let point = display_point.to_point(map); - if point.row == map.max_buffer_row().0 { + if point.row == map.buffer_snapshot.max_row().0 { return map.max_point(); } let is_current_line_blank = map.buffer_snapshot.is_line_blank(MultiBufferRow(point.row)); - for row in point.row + 1..map.max_buffer_row().0 + 1 { + for row in point.row + 1..map.buffer_snapshot.max_row().0 + 1 { let blank = map.buffer_snapshot.is_line_blank(MultiBufferRow(row)); if blank != is_current_line_blank { let previous_row = row - 1; From d8732adfb2c96ffb0c3d9df679bf782b02e82b52 Mon Sep 17 00:00:00 2001 From: tims <0xtimsb@gmail.com> Date: Wed, 4 Dec 2024 18:10:53 +0530 Subject: [PATCH 277/886] Add fuzzy matching for snippets completions (#21524) Closes #21439 This PR uses fuzzy matching for snippet completions instead of fixed-prefix matching. This mimics the behavior of VSCode. fuzzy Release Notes: - Improved suggestions for snippets. --- crates/editor/src/editor.rs | 180 +++++++++++++++++++++++------------- 1 file changed, 115 insertions(+), 65 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ea03d027c4..69383ceb82 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -13812,80 +13812,130 @@ fn snippet_completions( buffer: &Model, buffer_position: text::Anchor, cx: &mut AppContext, -) -> Vec { +) -> Task>> { let language = buffer.read(cx).language_at(buffer_position); let language_name = language.as_ref().map(|language| language.lsp_id()); let snippet_store = project.snippets().read(cx); let snippets = snippet_store.snippets_for(language_name, cx); if snippets.is_empty() { - return vec![]; + return Task::ready(Ok(vec![])); } let snapshot = buffer.read(cx).text_snapshot(); - let chars = snapshot.reversed_chars_for_range(text::Anchor::MIN..buffer_position); + let chars: String = snapshot + .reversed_chars_for_range(text::Anchor::MIN..buffer_position) + .collect(); let scope = language.map(|language| language.default_scope()); - let classifier = CharClassifier::new(scope).for_completion(true); - let mut last_word = chars - .take_while(|c| classifier.is_word(*c)) - .collect::(); - last_word = last_word.chars().rev().collect(); - let as_offset = text::ToOffset::to_offset(&buffer_position, &snapshot); - let to_lsp = |point: &text::Anchor| { - let end = text::ToPointUtf16::to_point_utf16(point, &snapshot); - point_to_lsp(end) - }; - let lsp_end = to_lsp(&buffer_position); - snippets - .into_iter() - .filter_map(|snippet| { - let matching_prefix = snippet - .prefix - .iter() - .find(|prefix| prefix.starts_with(&last_word))?; - let start = as_offset - last_word.len(); - let start = snapshot.anchor_before(start); - let range = start..buffer_position; - let lsp_start = to_lsp(&start); - let lsp_range = lsp::Range { - start: lsp_start, - end: lsp_end, - }; - Some(Completion { - old_range: range, - new_text: snippet.body.clone(), - label: CodeLabel { - text: matching_prefix.clone(), - runs: vec![], - filter_range: 0..matching_prefix.len(), - }, - server_id: LanguageServerId(usize::MAX), - documentation: snippet.description.clone().map(Documentation::SingleLine), - lsp_completion: lsp::CompletionItem { - label: snippet.prefix.first().unwrap().clone(), - kind: Some(CompletionItemKind::SNIPPET), - label_details: snippet.description.as_ref().map(|description| { - lsp::CompletionItemLabelDetails { - detail: Some(description.clone()), - description: None, - } - }), - insert_text_format: Some(InsertTextFormat::SNIPPET), - text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace( - lsp::InsertReplaceEdit { - new_text: snippet.body.clone(), - insert: lsp_range, - replace: lsp_range, - }, - )), - filter_text: Some(snippet.body.clone()), - sort_text: Some(char::MAX.to_string()), - ..Default::default() - }, - confirm: None, + let executor = cx.background_executor().clone(); + + cx.background_executor().spawn(async move { + let classifier = CharClassifier::new(scope).for_completion(true); + let mut last_word = chars + .chars() + .take_while(|c| classifier.is_word(*c)) + .collect::(); + last_word = last_word.chars().rev().collect(); + let as_offset = text::ToOffset::to_offset(&buffer_position, &snapshot); + let to_lsp = |point: &text::Anchor| { + let end = text::ToPointUtf16::to_point_utf16(point, &snapshot); + point_to_lsp(end) + }; + let lsp_end = to_lsp(&buffer_position); + + let candidates = snippets + .iter() + .enumerate() + .flat_map(|(ix, snippet)| { + snippet + .prefix + .iter() + .map(move |prefix| StringMatchCandidate::new(ix, prefix.clone())) }) - }) - .collect() + .collect::>(); + + let mut matches = fuzzy::match_strings( + &candidates, + &last_word, + last_word.chars().any(|c| c.is_uppercase()), + 100, + &Default::default(), + executor, + ) + .await; + + // Remove all candidates where the query's start does not match the start of any word in the candidate + if let Some(query_start) = last_word.chars().next() { + matches.retain(|string_match| { + split_words(&string_match.string).any(|word| { + // Check that the first codepoint of the word as lowercase matches the first + // codepoint of the query as lowercase + word.chars() + .flat_map(|codepoint| codepoint.to_lowercase()) + .zip(query_start.to_lowercase()) + .all(|(word_cp, query_cp)| word_cp == query_cp) + }) + }); + } + + let matched_strings = matches + .into_iter() + .map(|m| m.string) + .collect::>(); + + let result: Vec = snippets + .into_iter() + .filter_map(|snippet| { + let matching_prefix = snippet + .prefix + .iter() + .find(|prefix| matched_strings.contains(*prefix))?; + let start = as_offset - last_word.len(); + let start = snapshot.anchor_before(start); + let range = start..buffer_position; + let lsp_start = to_lsp(&start); + let lsp_range = lsp::Range { + start: lsp_start, + end: lsp_end, + }; + Some(Completion { + old_range: range, + new_text: snippet.body.clone(), + label: CodeLabel { + text: matching_prefix.clone(), + runs: vec![], + filter_range: 0..matching_prefix.len(), + }, + server_id: LanguageServerId(usize::MAX), + documentation: snippet.description.clone().map(Documentation::SingleLine), + lsp_completion: lsp::CompletionItem { + label: snippet.prefix.first().unwrap().clone(), + kind: Some(CompletionItemKind::SNIPPET), + label_details: snippet.description.as_ref().map(|description| { + lsp::CompletionItemLabelDetails { + detail: Some(description.clone()), + description: None, + } + }), + insert_text_format: Some(InsertTextFormat::SNIPPET), + text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace( + lsp::InsertReplaceEdit { + new_text: snippet.body.clone(), + insert: lsp_range, + replace: lsp_range, + }, + )), + filter_text: Some(snippet.body.clone()), + sort_text: Some(char::MAX.to_string()), + ..Default::default() + }, + confirm: None, + }) + }) + .collect(); + + Ok(result) + }) } impl CompletionProvider for Model { @@ -13901,8 +13951,8 @@ impl CompletionProvider for Model { let project_completions = project.completions(buffer, buffer_position, options, cx); cx.background_executor().spawn(async move { let mut completions = project_completions.await?; - //let snippets = snippets.into_iter().; - completions.extend(snippets); + let snippets_completions = snippets.await?; + completions.extend(snippets_completions); Ok(completions) }) }) From 0ee99c6d9c60dcd84c03fee377ee26fdb82ac89b Mon Sep 17 00:00:00 2001 From: David Soria Parra <167242713+dsp-ant@users.noreply.github.com> Date: Wed, 4 Dec 2024 15:45:25 +0000 Subject: [PATCH 278/886] context_server: Add missing types for MCP spec to protocol 2024-11-05 (#21498) This commit syncs missing types for the mcp spec 2024-11-05. Release Notes: - N/A --- .../slash_command/context_server_command.rs | 2 +- crates/context_server/src/types.rs | 82 ++++++++++++++++++- 2 files changed, 79 insertions(+), 5 deletions(-) diff --git a/crates/assistant/src/slash_command/context_server_command.rs b/crates/assistant/src/slash_command/context_server_command.rs index b183a77f54..8c53ddb773 100644 --- a/crates/assistant/src/slash_command/context_server_command.rs +++ b/crates/assistant/src/slash_command/context_server_command.rs @@ -164,7 +164,7 @@ impl SlashCommand for ContextServerSlashCommand { .messages .into_iter() .filter_map(|msg| match msg.content { - context_server::types::MessageContent::Text { text } => Some(text), + context_server::types::MessageContent::Text { text, .. } => Some(text), _ => None, }) .collect::>() diff --git a/crates/context_server/src/types.rs b/crates/context_server/src/types.rs index 851ebbf08b..f3c6e1c5e2 100644 --- a/crates/context_server/src/types.rs +++ b/crates/context_server/src/types.rs @@ -167,11 +167,18 @@ pub struct InitializeResponse { #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ResourcesReadResponse { - pub contents: Vec, + pub contents: Vec, #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] pub meta: Option>, } +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum ResourceContentsType { + Text(TextResourceContents), + Blob(BlobResourceContents), +} + #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ResourcesListResponse { @@ -181,6 +188,7 @@ pub struct ResourcesListResponse { #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] pub meta: Option>, } + #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SamplingMessage { @@ -188,6 +196,35 @@ pub struct SamplingMessage { pub content: MessageContent, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CreateMessageRequest { + pub messages: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub model_preferences: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub system_prompt: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub include_context: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub temperature: Option, + pub max_tokens: u32, + #[serde(skip_serializing_if = "Option::is_none")] + pub stop_sequences: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CreateMessageResult { + pub role: Role, + pub content: MessageContent, + pub model: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub stop_reason: Option, +} + #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PromptMessage { @@ -206,11 +243,33 @@ pub enum Role { #[serde(tag = "type")] pub enum MessageContent { #[serde(rename = "text")] - Text { text: String }, + Text { + text: String, + #[serde(skip_serializing_if = "Option::is_none")] + annotations: Option, + }, #[serde(rename = "image")] - Image { data: String, mime_type: String }, + Image { + data: String, + mime_type: String, + #[serde(skip_serializing_if = "Option::is_none")] + annotations: Option, + }, #[serde(rename = "resource")] - Resource { resource: ResourceContents }, + Resource { + resource: ResourceContents, + #[serde(skip_serializing_if = "Option::is_none")] + annotations: Option, + }, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MessageAnnotations { + #[serde(skip_serializing_if = "Option::is_none")] + pub audience: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub priority: Option, } #[derive(Debug, Deserialize)] @@ -460,6 +519,11 @@ pub enum ClientNotification { Initialized, Progress(ProgressParams), RootsListChanged, + Cancelled { + request_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + reason: Option, + }, } #[derive(Debug, Serialize, Deserialize)] @@ -532,6 +596,16 @@ pub struct ListToolsResponse { pub meta: Option>, } +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ListResourceTemplatesResponse { + pub resource_templates: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub next_cursor: Option, + #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] + pub meta: Option>, +} + #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ListRootsResponse { From 207eb51df198eae9417fcd15e0df2c2d4bd3bee6 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 4 Dec 2024 11:14:35 -0500 Subject: [PATCH 279/886] assistant2: Style inline code in Markdown (#21536) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds some styling for inline code within the messages to differentiate them from the surrounding text: Screenshot 2024-12-04 at 10 58 14 AM Release Notes: - N/A --- crates/assistant2/src/assistant_panel.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index b8ce5b1a36..16d5e62a7c 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -152,11 +152,13 @@ impl AssistantPanel { .map(|message| message.text.clone()) { let theme_settings = ThemeSettings::get_global(cx); + let ui_font_size = TextSize::Default.rems(cx); + let buffer_font_size = theme_settings.buffer_font_size; let mut text_style = cx.text_style(); text_style.refine(&TextStyleRefinement { font_family: Some(theme_settings.ui_font.family.clone()), - font_size: Some(TextSize::Default.rems(cx).into()), + font_size: Some(ui_font_size.into()), color: Some(cx.theme().colors().text), ..Default::default() }); @@ -168,11 +170,17 @@ impl AssistantPanel { code_block: StyleRefinement { text: Some(TextStyleRefinement { font_family: Some(theme_settings.buffer_font.family.clone()), - font_size: Some(theme_settings.buffer_font_size.into()), + font_size: Some(buffer_font_size.into()), ..Default::default() }), ..Default::default() }, + inline_code: TextStyleRefinement { + font_family: Some(theme_settings.buffer_font.family.clone()), + font_size: Some(ui_font_size.into()), + background_color: Some(cx.theme().colors().editor_background), + ..Default::default() + }, ..Default::default() }; From 5948ea217bcf8b220098b45e74e7a9e7fb492421 Mon Sep 17 00:00:00 2001 From: Vedant Matanhelia Date: Wed, 4 Dec 2024 21:53:31 +0530 Subject: [PATCH 280/886] Configure Highlight settings on yank vim (#21479) Release Notes: - Add settings / config variables to control `highlight_on_yank` or `highlight_on_copy` --------- Co-authored-by: Conrad Irwin --- assets/settings/default.json | 1 + crates/vim/src/normal/yank.rs | 8 +++++--- crates/vim/src/vim.rs | 2 ++ docs/src/vim.md | 2 ++ 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 5930537856..97e05c9ad5 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1129,6 +1129,7 @@ "use_system_clipboard": "always", "use_multiline_find": false, "use_smartcase_find": false, + "highlight_on_yank_duration": 200, "custom_digraphs": {} }, // The server to connect to. If the environment variable diff --git a/crates/vim/src/normal/yank.rs b/crates/vim/src/normal/yank.rs index 85c6531f6a..d23dc2f9b0 100644 --- a/crates/vim/src/normal/yank.rs +++ b/crates/vim/src/normal/yank.rs @@ -4,13 +4,14 @@ use crate::{ motion::Motion, object::Object, state::{Mode, Register}, - Vim, + Vim, VimSettings, }; use collections::HashMap; use editor::{ClipboardSelection, Editor}; use gpui::ViewContext; use language::Point; use multi_buffer::MultiBufferRow; +use settings::Settings; struct HighlightOnYank; @@ -195,7 +196,8 @@ impl Vim { ) }); - if !is_yank || self.mode == Mode::Visual { + let highlight_duration = VimSettings::get_global(cx).highlight_on_yank_duration; + if !is_yank || self.mode == Mode::Visual || highlight_duration == 0 { return; } @@ -206,7 +208,7 @@ impl Vim { ); cx.spawn(|this, mut cx| async move { cx.background_executor() - .timer(Duration::from_millis(200)) + .timer(Duration::from_millis(highlight_duration)) .await; this.update(&mut cx, |editor, cx| { editor.clear_background_highlights::(cx) diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index c395e9c37e..843b094700 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -1199,6 +1199,7 @@ struct VimSettings { pub use_multiline_find: bool, pub use_smartcase_find: bool, pub custom_digraphs: HashMap>, + pub highlight_on_yank_duration: u64, } #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] @@ -1208,6 +1209,7 @@ struct VimSettingsContent { pub use_multiline_find: Option, pub use_smartcase_find: Option, pub custom_digraphs: Option>>, + pub highlight_on_yank_duration: Option, } impl Settings for VimSettings { diff --git a/docs/src/vim.md b/docs/src/vim.md index c0a7fed2e2..a350fb7773 100644 --- a/docs/src/vim.md +++ b/docs/src/vim.md @@ -438,6 +438,7 @@ You can change the following settings to modify vim mode's behavior: | use_smartcase_find | If `true`, `f` and `t` motions are case-insensitive when the target letter is lowercase. | false | | toggle_relative_line_numbers | If `true`, line numbers are relative in normal mode and absolute in insert mode, giving you the best of both options. | false | | custom_digraphs | An object that allows you to add custom digraphs. Read below for an example. | {} | +| highlight_on_yank_duration | The duration of the highlight animation(in ms). Set to `0` to disable | 200 | Here's an example of adding a digraph for the zombie emoji. This allows you to type `ctrl-k f z` to insert a zombie emoji. You can add as many digraphs as you like. @@ -460,6 +461,7 @@ Here's an example of these settings changed: "use_multiline_find": true, "use_smartcase_find": true, "toggle_relative_line_numbers": true, + "highlight_on_yank_duration": 50, "custom_digraphs": { "fz": "🧟‍♀️" } From 706372fe4eb64e52274012af6f18f42065bcc509 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 4 Dec 2024 17:59:27 +0100 Subject: [PATCH 281/886] title_bar: Add show_user_picture setting to let users hide their profile picture (#21526) Fixes #21464 Closes #21464 Release Notes: - Added `show_user_picture` setting (default: true) to allow users to hide their profile picture in titlebar. --- assets/settings/default.json | 2 ++ crates/title_bar/Cargo.toml | 1 + crates/title_bar/src/title_bar.rs | 7 ++++++- crates/workspace/src/workspace_settings.rs | 5 +++++ 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 97e05c9ad5..db3b7130e0 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1187,6 +1187,8 @@ // "W": "workspace::Save" // } "command_aliases": {}, + // Whether to show user picture in titlebar. + "show_user_picture": true, // ssh_connections is an array of ssh connections. // You can configure these from `project: Open Remote` in the command palette. // Zed's ssh support will pull configuration from your ~/.ssh too. diff --git a/crates/title_bar/Cargo.toml b/crates/title_bar/Cargo.toml index 0a2878b357..9d2fb598fa 100644 --- a/crates/title_bar/Cargo.toml +++ b/crates/title_bar/Cargo.toml @@ -37,6 +37,7 @@ project.workspace = true remote.workspace = true rpc.workspace = true serde.workspace = true +settings.workspace = true smallvec.workspace = true story = { workspace = true, optional = true } theme.workspace = true diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index 4e9a99433a..b6e08e2126 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -19,6 +19,7 @@ use gpui::{ }; use project::{Project, RepositoryEntry}; use rpc::proto; +use settings::Settings as _; use smallvec::SmallVec; use std::sync::Arc; use theme::ActiveTheme; @@ -600,7 +601,11 @@ impl TitleBar { .child( h_flex() .gap_0p5() - .child(Avatar::new(user.avatar_uri.clone())) + .children( + workspace::WorkspaceSettings::get_global(cx) + .show_user_picture + .then(|| Avatar::new(user.avatar_uri.clone())), + ) .child( Icon::new(IconName::ChevronDown) .size(IconSize::Small) diff --git a/crates/workspace/src/workspace_settings.rs b/crates/workspace/src/workspace_settings.rs index 0d872425c1..b27a09c24c 100644 --- a/crates/workspace/src/workspace_settings.rs +++ b/crates/workspace/src/workspace_settings.rs @@ -19,6 +19,7 @@ pub struct WorkspaceSettings { pub when_closing_with_no_tabs: CloseWindowWhenNoItems, pub use_system_path_prompts: bool, pub command_aliases: HashMap, + pub show_user_picture: bool, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)] @@ -128,6 +129,10 @@ pub struct WorkspaceSettingsContent { /// /// Default: true pub command_aliases: Option>, + /// Whether to show user avatar in the title bar. + /// + /// Default: true + pub show_user_picture: Option, } #[derive(Deserialize)] From cf781dff716bdda7d6e64fd3db0ee3ac7706a9f7 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Wed, 4 Dec 2024 12:01:28 -0500 Subject: [PATCH 282/886] v0.166.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 21db0ceb99..fc64ca4093 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15594,7 +15594,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.165.0" +version = "0.166.0" dependencies = [ "activity_indicator", "anyhow", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 24fc0dec8b..74dd2601ad 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.165.0" +version = "0.166.0" publish = false license = "GPL-3.0-or-later" authors = ["Zed Team "] From fee0624299fe68304da1c6cb4d3a23856237c4fe Mon Sep 17 00:00:00 2001 From: Stanislav Alekseev <43210583+WeetHet@users.noreply.github.com> Date: Wed, 4 Dec 2024 19:39:23 +0200 Subject: [PATCH 283/886] Force code actions to be single line (#21409) Addresses #21403 partially. Is consistent with the behaviour in VSCode Before: 391571084-1bef4ef9-b8f5-4c8f-9a32-9c0ab6c91af1 After: Screenshot 2024-12-02 at 18 35 11 Release Notes: - Fixed an issue with multiline code actions' rendering by forcing them to be single line --- crates/editor/src/editor.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 69383ceb82..2464ce8427 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1690,7 +1690,9 @@ impl CodeActionsMenu { }), ) // TASK: It would be good to make lsp_action.title a SharedString to avoid allocating here. - .child(SharedString::from(action.lsp_action.title.clone())) + .child(SharedString::from( + action.lsp_action.title.replace("\n", ""), + )) }) .when_some(action.as_task(), |this, task| { this.on_mouse_down( @@ -1707,7 +1709,7 @@ impl CodeActionsMenu { } }), ) - .child(SharedString::from(task.resolved_label.clone())) + .child(SharedString::from(task.resolved_label.replace("\n", ""))) }) }) .collect() From 7cfc972df660e8b3594c020dd5e00395cdf625b7 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 4 Dec 2024 12:44:03 -0500 Subject: [PATCH 284/886] assistant2: Add empty state for new threads (#21542) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds an empty state for new threads in Assistant2: Screenshot 2024-12-04 at 12 17 46 PM This is mostly just a sketch in its current state. Release Notes: - N/A --- assets/keymaps/default-macos.json | 3 +- crates/assistant2/src/assistant.rs | 8 +- crates/assistant2/src/assistant_panel.rs | 142 +++++++++++++++++++++-- 3 files changed, 141 insertions(+), 12 deletions(-) diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 71d997d2b1..65389230ac 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -210,7 +210,8 @@ { "context": "AssistantPanel2", "bindings": { - "cmd-n": "assistant2::NewThread" + "cmd-n": "assistant2::NewThread", + "cmd-shift-h": "assistant2::OpenHistory" } }, { diff --git a/crates/assistant2/src/assistant.rs b/crates/assistant2/src/assistant.rs index 8ef4a1d9dc..aa79ce0c67 100644 --- a/crates/assistant2/src/assistant.rs +++ b/crates/assistant2/src/assistant.rs @@ -11,7 +11,13 @@ pub use crate::assistant_panel::AssistantPanel; actions!( assistant2, - [ToggleFocus, NewThread, ToggleModelSelector, Chat] + [ + ToggleFocus, + NewThread, + ToggleModelSelector, + OpenHistory, + Chat + ] ); const NAMESPACE: &str = "assistant2"; diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index 16d5e62a7c..2dc4582eee 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -5,8 +5,8 @@ use assistant_tool::ToolWorkingSet; use client::zed_urls; use collections::HashMap; use gpui::{ - list, prelude::*, px, Action, AnyElement, AppContext, AsyncWindowContext, Empty, EventEmitter, - FocusHandle, FocusableView, FontWeight, ListAlignment, ListState, Model, Pixels, + list, prelude::*, px, svg, Action, AnyElement, AppContext, AsyncWindowContext, Empty, + EventEmitter, FocusHandle, FocusableView, FontWeight, ListAlignment, ListState, Model, Pixels, StyleRefinement, Subscription, Task, TextStyleRefinement, View, ViewContext, WeakView, WindowContext, }; @@ -16,14 +16,14 @@ use language_model_selector::LanguageModelSelector; use markdown::{Markdown, MarkdownStyle}; use settings::Settings; use theme::ThemeSettings; -use ui::{prelude::*, ButtonLike, Divider, IconButtonShape, Tab, Tooltip}; +use ui::{prelude::*, ButtonLike, Divider, IconButtonShape, KeyBinding, ListItem, Tab, Tooltip}; use workspace::dock::{DockPosition, Panel, PanelEvent}; use workspace::Workspace; use crate::message_editor::MessageEditor; use crate::thread::{MessageId, Thread, ThreadError, ThreadEvent}; use crate::thread_store::ThreadStore; -use crate::{NewThread, ToggleFocus, ToggleModelSelector}; +use crate::{NewThread, OpenHistory, ToggleFocus, ToggleModelSelector}; pub fn init(cx: &mut AppContext) { cx.observe_new_views( @@ -311,8 +311,8 @@ impl AssistantPanel { ) } }) - .on_click(move |_event, _cx| { - println!("New Thread"); + .on_click(move |_event, cx| { + cx.dispatch_action(NewThread.boxed_clone()); }), ) .child( @@ -320,9 +320,19 @@ impl AssistantPanel { .shape(IconButtonShape::Square) .icon_size(IconSize::Small) .style(ButtonStyle::Subtle) - .tooltip(move |cx| Tooltip::text("Open History", cx)) - .on_click(move |_event, _cx| { - println!("Open History"); + .tooltip({ + let focus_handle = focus_handle.clone(); + move |cx| { + Tooltip::for_action_in( + "Open History", + &OpenHistory, + &focus_handle, + cx, + ) + } + }) + .on_click(move |_event, cx| { + cx.dispatch_action(OpenHistory.boxed_clone()); }), ) .child( @@ -389,6 +399,99 @@ impl AssistantPanel { ) } + fn render_message_list(&self, cx: &mut ViewContext) -> AnyElement { + if self.thread_messages.is_empty() { + #[allow(clippy::useless_vec)] + let recent_threads = vec![1, 2, 3]; + + return v_flex() + .gap_2() + .mx_auto() + .child( + v_flex().w_full().child( + svg() + .path("icons/logo_96.svg") + .text_color(cx.theme().colors().text) + .w(px(40.)) + .h(px(40.)) + .mx_auto() + .mb_4(), + ), + ) + .child(v_flex()) + .child( + h_flex() + .w_full() + .justify_center() + .child(Label::new("Context Examples:").size(LabelSize::Small)), + ) + .child( + h_flex() + .gap_2() + .justify_center() + .child( + h_flex() + .gap_1() + .p_0p5() + .rounded_md() + .border_1() + .border_color(cx.theme().colors().border_variant) + .child( + Icon::new(IconName::Terminal) + .size(IconSize::Small) + .color(Color::Disabled), + ) + .child(Label::new("Terminal").size(LabelSize::Small)), + ) + .child( + h_flex() + .gap_1() + .p_0p5() + .rounded_md() + .border_1() + .border_color(cx.theme().colors().border_variant) + .child( + Icon::new(IconName::Folder) + .size(IconSize::Small) + .color(Color::Disabled), + ) + .child(Label::new("/src/components").size(LabelSize::Small)), + ), + ) + .child( + h_flex() + .w_full() + .justify_center() + .child(Label::new("Recent Threads:").size(LabelSize::Small)), + ) + .child( + v_flex().gap_2().children( + recent_threads + .iter() + .map(|_thread| self.render_past_thread(cx)), + ), + ) + .child( + h_flex().w_full().justify_center().child( + Button::new("view-all-past-threads", "View All Past Threads") + .style(ButtonStyle::Subtle) + .label_size(LabelSize::Small) + .key_binding(KeyBinding::for_action_in( + &OpenHistory, + &self.focus_handle(cx), + cx, + )) + .on_click(move |_event, cx| { + cx.dispatch_action(OpenHistory.boxed_clone()); + }), + ), + ) + .into_any(); + } + + list(self.thread_list_state.clone()).flex_1().into_any() + } + fn render_message(&self, ix: usize, cx: &mut ViewContext) -> AnyElement { let message_id = self.thread_messages[ix]; let Some(message) = self.thread.read(cx).message(message_id) else { @@ -431,6 +534,22 @@ impl AssistantPanel { .into_any() } + fn render_past_thread(&self, _cx: &mut ViewContext) -> impl IntoElement { + ListItem::new("temp") + .start_slot(Icon::new(IconName::MessageBubbles)) + .child(Label::new("Some Thread Title")) + .end_slot( + h_flex() + .gap_2() + .child(Label::new("1 hour ago").color(Color::Disabled)) + .child( + IconButton::new("delete", IconName::TrashAlt) + .shape(IconButtonShape::Square) + .icon_size(IconSize::Small), + ), + ) + } + fn render_last_error(&self, cx: &mut ViewContext) -> Option { let last_error = self.last_error.as_ref()?; @@ -587,8 +706,11 @@ impl Render for AssistantPanel { .on_action(cx.listener(|this, _: &NewThread, cx| { this.new_thread(cx); })) + .on_action(cx.listener(|_this, _: &OpenHistory, _cx| { + println!("Open History"); + })) .child(self.render_toolbar(cx)) - .child(list(self.thread_list_state.clone()).flex_1()) + .child(self.render_message_list(cx)) .child( h_flex() .border_t_1() From 44264ffedcfe1a87606e7d6a9a52aaadf88061f5 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 4 Dec 2024 10:58:56 -0800 Subject: [PATCH 285/886] Revert accidental change to Rust outline files (#21545) Release Notes: - Preview only: Fixed impl blocks in the rust outline view --- crates/languages/src/rust/outline.scm | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/languages/src/rust/outline.scm b/crates/languages/src/rust/outline.scm index 4299a01f19..3012995e2a 100644 --- a/crates/languages/src/rust/outline.scm +++ b/crates/languages/src/rust/outline.scm @@ -15,7 +15,11 @@ (visibility_modifier)? @context name: (_) @name) @item -(function_item +(impl_item + "impl" @context + trait: (_)? @name + "for"? @context + type: (_) @name body: (_ "{" @open (_)* "}" @close)) @item (trait_item From 0bde0f8e2f3879a8f304716ba570375955bfe9b2 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 4 Dec 2024 14:35:44 -0500 Subject: [PATCH 286/886] assistant2: Add ability to open past threads (#21548) This PR adds the ability to open past threads in Assistant 2. There are also some mocked threads in the history for testing purposes. Release Notes: - N/A --- Cargo.lock | 2 + crates/assistant2/Cargo.toml | 2 + crates/assistant2/src/assistant_panel.rs | 149 ++++++++++++++--------- crates/assistant2/src/thread.rs | 37 +++++- crates/assistant2/src/thread_store.rs | 113 ++++++++++++++++- 5 files changed, 243 insertions(+), 60 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fc64ca4093..72bace069b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -478,7 +478,9 @@ dependencies = [ "smol", "theme", "ui", + "unindent", "util", + "uuid", "workspace", ] diff --git a/crates/assistant2/Cargo.toml b/crates/assistant2/Cargo.toml index 257183a4ac..fb7dcbe520 100644 --- a/crates/assistant2/Cargo.toml +++ b/crates/assistant2/Cargo.toml @@ -37,5 +37,7 @@ settings.workspace = true smol.workspace = true theme.workspace = true ui.workspace = true +unindent.workspace = true util.workspace = true +uuid.workspace = true workspace.workspace = true diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index 2dc4582eee..00bd15de2e 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -21,7 +21,7 @@ use workspace::dock::{DockPosition, Panel, PanelEvent}; use workspace::Workspace; use crate::message_editor::MessageEditor; -use crate::thread::{MessageId, Thread, ThreadError, ThreadEvent}; +use crate::thread::{MessageId, Thread, ThreadError, ThreadEvent, ThreadId}; use crate::thread_store::ThreadStore; use crate::{NewThread, OpenHistory, ToggleFocus, ToggleModelSelector}; @@ -77,7 +77,7 @@ impl AssistantPanel { tools: Arc, cx: &mut ViewContext, ) -> Self { - let thread = cx.new_model(|cx| Thread::new(tools.clone(), cx)); + let thread = thread_store.update(cx, |this, cx| this.create_thread(cx)); let subscriptions = vec![ cx.observe(&thread, |_, _, cx| cx.notify()), cx.subscribe(&thread, Self::handle_thread_event), @@ -105,8 +105,27 @@ impl AssistantPanel { } fn new_thread(&mut self, cx: &mut ViewContext) { - let tools = self.thread.read(cx).tools().clone(); - let thread = cx.new_model(|cx| Thread::new(tools, cx)); + let thread = self + .thread_store + .update(cx, |this, cx| this.create_thread(cx)); + self.reset_thread(thread, cx); + } + + fn open_thread(&mut self, thread_id: &ThreadId, cx: &mut ViewContext) { + let Some(thread) = self + .thread_store + .update(cx, |this, cx| this.open_thread(thread_id, cx)) + else { + return; + }; + self.reset_thread(thread.clone(), cx); + + for message in thread.read(cx).messages().cloned().collect::>() { + self.push_message(&message.id, message.text.clone(), cx); + } + } + + fn reset_thread(&mut self, thread: Model, cx: &mut ViewContext) { let subscriptions = vec![ cx.observe(&thread, |_, _, cx| cx.notify()), cx.subscribe(&thread, Self::handle_thread_event), @@ -122,6 +141,56 @@ impl AssistantPanel { self.message_editor.focus_handle(cx).focus(cx); } + fn push_message(&mut self, id: &MessageId, text: String, cx: &mut ViewContext) { + let old_len = self.thread_messages.len(); + self.thread_messages.push(*id); + self.thread_list_state.splice(old_len..old_len, 1); + + let theme_settings = ThemeSettings::get_global(cx); + let ui_font_size = TextSize::Default.rems(cx); + let buffer_font_size = theme_settings.buffer_font_size; + + let mut text_style = cx.text_style(); + text_style.refine(&TextStyleRefinement { + font_family: Some(theme_settings.ui_font.family.clone()), + font_size: Some(ui_font_size.into()), + color: Some(cx.theme().colors().text), + ..Default::default() + }); + + let markdown_style = MarkdownStyle { + base_text_style: text_style, + syntax: cx.theme().syntax().clone(), + selection_background_color: cx.theme().players().local().selection, + code_block: StyleRefinement { + text: Some(TextStyleRefinement { + font_family: Some(theme_settings.buffer_font.family.clone()), + font_size: Some(buffer_font_size.into()), + ..Default::default() + }), + ..Default::default() + }, + inline_code: TextStyleRefinement { + font_family: Some(theme_settings.buffer_font.family.clone()), + font_size: Some(ui_font_size.into()), + background_color: Some(cx.theme().colors().editor_background), + ..Default::default() + }, + ..Default::default() + }; + + let markdown = cx.new_view(|cx| { + Markdown::new( + text, + markdown_style, + Some(self.language_registry.clone()), + None, + cx, + ) + }); + self.rendered_messages_by_id.insert(*id, markdown); + } + fn handle_thread_event( &mut self, _: Model, @@ -141,59 +210,13 @@ impl AssistantPanel { } } ThreadEvent::MessageAdded(message_id) => { - let old_len = self.thread_messages.len(); - self.thread_messages.push(*message_id); - self.thread_list_state.splice(old_len..old_len, 1); - if let Some(message_text) = self .thread .read(cx) .message(*message_id) .map(|message| message.text.clone()) { - let theme_settings = ThemeSettings::get_global(cx); - let ui_font_size = TextSize::Default.rems(cx); - let buffer_font_size = theme_settings.buffer_font_size; - - let mut text_style = cx.text_style(); - text_style.refine(&TextStyleRefinement { - font_family: Some(theme_settings.ui_font.family.clone()), - font_size: Some(ui_font_size.into()), - color: Some(cx.theme().colors().text), - ..Default::default() - }); - - let markdown_style = MarkdownStyle { - base_text_style: text_style, - syntax: cx.theme().syntax().clone(), - selection_background_color: cx.theme().players().local().selection, - code_block: StyleRefinement { - text: Some(TextStyleRefinement { - font_family: Some(theme_settings.buffer_font.family.clone()), - font_size: Some(buffer_font_size.into()), - ..Default::default() - }), - ..Default::default() - }, - inline_code: TextStyleRefinement { - font_family: Some(theme_settings.buffer_font.family.clone()), - font_size: Some(ui_font_size.into()), - background_color: Some(cx.theme().colors().editor_background), - ..Default::default() - }, - ..Default::default() - }; - - let markdown = cx.new_view(|cx| { - Markdown::new( - message_text, - markdown_style, - Some(self.language_registry.clone()), - None, - cx, - ) - }); - self.rendered_messages_by_id.insert(*message_id, markdown); + self.push_message(message_id, message_text, cx); } cx.notify(); @@ -401,8 +424,9 @@ impl AssistantPanel { fn render_message_list(&self, cx: &mut ViewContext) -> AnyElement { if self.thread_messages.is_empty() { - #[allow(clippy::useless_vec)] - let recent_threads = vec![1, 2, 3]; + let recent_threads = self + .thread_store + .update(cx, |this, cx| this.recent_threads(3, cx)); return v_flex() .gap_2() @@ -467,8 +491,8 @@ impl AssistantPanel { .child( v_flex().gap_2().children( recent_threads - .iter() - .map(|_thread| self.render_past_thread(cx)), + .into_iter() + .map(|thread| self.render_past_thread(thread, cx)), ), ) .child( @@ -534,10 +558,16 @@ impl AssistantPanel { .into_any() } - fn render_past_thread(&self, _cx: &mut ViewContext) -> impl IntoElement { - ListItem::new("temp") + fn render_past_thread( + &self, + thread: Model, + cx: &mut ViewContext, + ) -> impl IntoElement { + let id = thread.read(cx).id().clone(); + + ListItem::new(("past-thread", thread.entity_id())) .start_slot(Icon::new(IconName::MessageBubbles)) - .child(Label::new("Some Thread Title")) + .child(Label::new(format!("Thread {id}"))) .end_slot( h_flex() .gap_2() @@ -548,6 +578,9 @@ impl AssistantPanel { .icon_size(IconSize::Small), ), ) + .on_click(cx.listener(move |this, _event, cx| { + this.open_thread(&id, cx); + })) } fn render_last_error(&self, cx: &mut ViewContext) -> Option { diff --git a/crates/assistant2/src/thread.rs b/crates/assistant2/src/thread.rs index a841325884..fc5e0d6a15 100644 --- a/crates/assistant2/src/thread.rs +++ b/crates/assistant2/src/thread.rs @@ -14,12 +14,28 @@ use language_model::{ use language_models::provider::cloud::{MaxMonthlySpendReachedError, PaymentRequiredError}; use serde::{Deserialize, Serialize}; use util::post_inc; +use uuid::Uuid; #[derive(Debug, Clone, Copy)] pub enum RequestKind { Chat, } +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)] +pub struct ThreadId(Arc); + +impl ThreadId { + pub fn new() -> Self { + Self(Uuid::new_v4().to_string().into()) + } +} + +impl std::fmt::Display for ThreadId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)] pub struct MessageId(usize); @@ -39,6 +55,7 @@ pub struct Message { /// A thread of conversation with the LLM. pub struct Thread { + id: ThreadId, messages: Vec, next_message_id: MessageId, completion_count: usize, @@ -52,6 +69,7 @@ pub struct Thread { impl Thread { pub fn new(tools: Arc, _cx: &mut ModelContext) -> Self { Self { + id: ThreadId::new(), messages: Vec::new(), next_message_id: MessageId(0), completion_count: 0, @@ -63,10 +81,18 @@ impl Thread { } } + pub fn id(&self) -> &ThreadId { + &self.id + } + pub fn message(&self, id: MessageId) -> Option<&Message> { self.messages.iter().find(|message| message.id == id) } + pub fn messages(&self) -> impl Iterator { + self.messages.iter() + } + pub fn tools(&self) -> &Arc { &self.tools } @@ -76,10 +102,19 @@ impl Thread { } pub fn insert_user_message(&mut self, text: impl Into, cx: &mut ModelContext) { + self.insert_message(Role::User, text, cx) + } + + pub fn insert_message( + &mut self, + role: Role, + text: impl Into, + cx: &mut ModelContext, + ) { let id = self.next_message_id.post_inc(); self.messages.push(Message { id, - role: Role::User, + role, text: text.into(), }); cx.emit(ThreadEvent::MessageAdded(id)); diff --git a/crates/assistant2/src/thread_store.rs b/crates/assistant2/src/thread_store.rs index 99f90eace8..d784c842c9 100644 --- a/crates/assistant2/src/thread_store.rs +++ b/crates/assistant2/src/thread_store.rs @@ -7,14 +7,18 @@ use context_server::manager::ContextServerManager; use context_server::{ContextServerFactoryRegistry, ContextServerTool}; use gpui::{prelude::*, AppContext, Model, ModelContext, Task}; use project::Project; +use unindent::Unindent; use util::ResultExt as _; +use crate::thread::{Thread, ThreadId}; + pub struct ThreadStore { #[allow(unused)] project: Model, tools: Arc, context_server_manager: Model, context_server_tool_ids: HashMap, Vec>, + threads: Vec>, } impl ThreadStore { @@ -31,12 +35,14 @@ impl ThreadStore { ContextServerManager::new(context_server_factory_registry, project.clone(), cx) }); - let this = Self { + let mut this = Self { project, tools, context_server_manager, context_server_tool_ids: HashMap::default(), + threads: Vec::new(), }; + this.mock_recent_threads(cx); this.register_context_server_handlers(cx); this @@ -46,6 +52,23 @@ impl ThreadStore { }) } + pub fn recent_threads(&self, limit: usize, _cx: &ModelContext) -> Vec> { + self.threads.iter().take(limit).cloned().collect() + } + + pub fn create_thread(&mut self, cx: &mut ModelContext) -> Model { + let thread = cx.new_model(|cx| Thread::new(self.tools.clone(), cx)); + self.threads.push(thread.clone()); + thread + } + + pub fn open_thread(&self, id: &ThreadId, cx: &mut ModelContext) -> Option> { + self.threads + .iter() + .find(|thread| thread.read(cx).id() == id) + .cloned() + } + fn register_context_server_handlers(&self, cx: &mut ModelContext) { cx.subscribe( &self.context_server_manager.clone(), @@ -112,3 +135,91 @@ impl ThreadStore { } } } + +impl ThreadStore { + /// Creates some mocked recent threads for testing purposes. + fn mock_recent_threads(&mut self, cx: &mut ModelContext) { + use language_model::Role; + + self.threads.push(cx.new_model(|cx| { + let mut thread = Thread::new(self.tools.clone(), cx); + thread.insert_user_message("Hello! Can you help me understand quantum computing?", cx); + thread.insert_message(Role::Assistant, "Of course! I'd be happy to help you understand quantum computing. Quantum computing is a fascinating field that uses the principles of quantum mechanics to process information. Unlike classical computers that use bits (0s and 1s), quantum computers use quantum bits or 'qubits'. These qubits can exist in multiple states simultaneously, a property called superposition. This allows quantum computers to perform certain calculations much faster than classical computers. What specific aspect of quantum computing would you like to know more about?", cx); + thread.insert_user_message("That's interesting! Can you explain how quantum entanglement is used in quantum computing?", cx); + thread.insert_message(Role::Assistant, "Certainly! Quantum entanglement is a key principle used in quantum computing. When two qubits become entangled, the state of one qubit is directly related to the state of the other, regardless of the distance between them. This property is used in quantum computing to create complex quantum states and to perform operations on multiple qubits simultaneously. Entanglement allows quantum computers to process information in ways that classical computers cannot, potentially solving certain problems much more efficiently. For example, it's crucial in quantum error correction and in algorithms like quantum teleportation, which is important for quantum communication.", cx); + thread + })); + + self.threads.push(cx.new_model(|cx| { + let mut thread = Thread::new(self.tools.clone(), cx); + thread.insert_user_message("Can you show me an example of Rust code for a simple web server?", cx); + thread.insert_message(Role::Assistant, "Certainly! Here's an example of a simple web server in Rust using the `actix-web` framework: + + ```rust + use actix_web::{web, App, HttpResponse, HttpServer, Responder}; + + async fn hello() -> impl Responder { + HttpResponse::Ok().body(\"Hello, World!\") + } + + #[actix_web::main] + async fn main() -> std::io::Result<()> { + HttpServer::new(|| { + App::new() + .route(\"/\", web::get().to(hello)) + }) + .bind(\"127.0.0.1:8080\")? + .run() + .await + } + ``` + + This code creates a basic web server that responds with 'Hello, World!' when you access the root URL. Here's a breakdown of what's happening: + + 1. We import necessary items from the `actix-web` crate. + 2. We define an async `hello` function that returns a simple HTTP response. + 3. In the `main` function, we set up the server to listen on `127.0.0.1:8080`. + 4. We configure the app to respond to GET requests on the root path with our `hello` function. + + To run this, you'd need to add `actix-web` to your `Cargo.toml` dependencies: + + ```toml + [dependencies] + actix-web = \"4.0\" + ``` + + Then you can run the server with `cargo run` and access it at `http://localhost:8080`.".unindent(), cx); + thread.insert_user_message("That's great! Can you explain more about async functions in Rust?", cx); + thread.insert_message(Role::Assistant, "Certainly! Async functions are a key feature in Rust for writing efficient, non-blocking code, especially for I/O-bound operations. Here's an overview: + + 1. **Syntax**: Async functions are declared using the `async` keyword: + + ```rust + async fn my_async_function() -> Result<(), Error> { + // Asynchronous code here + } + ``` + + 2. **Futures**: Async functions return a `Future`. A `Future` represents a value that may not be available yet but will be at some point. + + 3. **Await**: Inside an async function, you can use the `.await` syntax to wait for other async operations to complete: + + ```rust + async fn fetch_data() -> Result { + let response = make_http_request().await?; + let data = process_response(response).await?; + Ok(data) + } + ``` + + 4. **Non-blocking**: Async functions allow the runtime to work on other tasks while waiting for I/O or other operations to complete, making efficient use of system resources. + + 5. **Runtime**: To execute async code, you need a runtime like `tokio` or `async-std`. Actix-web, which we used in the previous example, includes its own runtime. + + 6. **Error Handling**: Async functions work well with Rust's `?` operator for error handling. + + Async programming in Rust provides a powerful way to write concurrent code that's both safe and efficient. It's particularly useful for servers, network programming, and any application that deals with many concurrent operations.".unindent(), cx); + thread + })); + } +} From f0fac41ca4ec7433addcbbe3f8cae39a633ad95a Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Wed, 4 Dec 2024 14:13:50 -0700 Subject: [PATCH 287/886] Add action `editor::OpenContextMenu` (#21494) This addresses the editor context menu portion of #17819. Release Notes: - Added `editor::OpenContextMenu` action to open context menu at current cursor position. --- assets/keymaps/default-linux.json | 4 +- crates/editor/src/actions.rs | 1 + crates/editor/src/editor.rs | 38 ++++++++++----- crates/editor/src/element.rs | 29 +++++------ crates/editor/src/mouse_context_menu.rs | 65 ++++++++++++------------- 5 files changed, 77 insertions(+), 60 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 2b792f353f..3787f97a8d 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -108,7 +108,9 @@ "ctrl-'": "editor::ToggleHunkDiff", "ctrl-\"": "editor::ExpandAllHunkDiffs", "ctrl-i": "editor::ShowSignatureHelp", - "alt-g b": "editor::ToggleGitBlame" + "alt-g b": "editor::ToggleGitBlame", + "menu": "editor::OpenContextMenu", + "shift-f10": "editor::OpenContextMenu" } }, { diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 9a00f1efca..99e7c6cd0b 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -296,6 +296,7 @@ gpui::actions!( NewlineBelow, NextInlineCompletion, NextScreen, + OpenContextMenu, OpenExcerpts, OpenExcerptsSplit, OpenProposedChangesEditor, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2464ce8427..883ea570a9 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -13075,6 +13075,12 @@ impl Editor { cx.write_to_clipboard(ClipboardItem::new_string(lines)); } + pub fn open_context_menu(&mut self, _: &OpenContextMenu, cx: &mut ViewContext) { + self.request_autoscroll(Autoscroll::newest(), cx); + let position = self.selections.newest_display(cx).start; + mouse_context_menu::deploy_context_menu(self, None, position, cx); + } + pub fn inlay_hint_cache(&self) -> &InlayHintCache { &self.inlay_hint_cache } @@ -13296,6 +13302,23 @@ impl Editor { .get(&type_id) .and_then(|item| item.to_any().downcast_ref::()) } + + fn character_size(&self, cx: &mut ViewContext) -> gpui::Point { + let text_layout_details = self.text_layout_details(cx); + let style = &text_layout_details.editor_style; + let font_id = cx.text_system().resolve_font(&style.text.font()); + let font_size = style.text.font_size.to_pixels(cx.rem_size()); + let line_height = style.text.line_height_in_pixels(cx.rem_size()); + + let em_width = cx + .text_system() + .typographic_bounds(font_id, font_size, 'm') + .unwrap() + .size + .width; + + gpui::Point::new(em_width, line_height) + } } fn char_len_with_expanded_tabs(offset: usize, text: &str, tab_size: NonZeroU32) -> usize { @@ -14725,17 +14748,10 @@ impl ViewInputHandler for Editor { cx: &mut ViewContext, ) -> Option> { let text_layout_details = self.text_layout_details(cx); - let style = &text_layout_details.editor_style; - let font_id = cx.text_system().resolve_font(&style.text.font()); - let font_size = style.text.font_size.to_pixels(cx.rem_size()); - let line_height = style.text.line_height_in_pixels(cx.rem_size()); - - let em_width = cx - .text_system() - .typographic_bounds(font_id, font_size, 'm') - .unwrap() - .size - .width; + let gpui::Point { + x: em_width, + y: line_height, + } = self.character_size(cx); let snapshot = self.snapshot(cx); let scroll_position = snapshot.scroll_position(); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 2bb40c4602..47de2609f7 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -169,6 +169,7 @@ impl EditorElement { crate::rust_analyzer_ext::apply_related_actions(view, cx); crate::clangd_ext::apply_related_actions(view, cx); + register_action(view, cx, Editor::open_context_menu); register_action(view, cx, Editor::move_left); register_action(view, cx, Editor::move_right); register_action(view, cx, Editor::move_down); @@ -595,7 +596,7 @@ impl EditorElement { position_map.point_for_position(text_hitbox.bounds, event.position); mouse_context_menu::deploy_context_menu( editor, - event.position, + Some(event.position), point_for_position.previous_valid, cx, ); @@ -2730,6 +2731,7 @@ impl EditorElement { &self, editor_snapshot: &EditorSnapshot, visible_range: Range, + content_origin: gpui::Point, cx: &mut WindowContext, ) -> Option { let position = self.editor.update(cx, |editor, cx| { @@ -2747,16 +2749,11 @@ impl EditorElement { let mouse_context_menu = editor.mouse_context_menu.as_ref()?; let (source_display_point, position) = match mouse_context_menu.position { MenuPosition::PinnedToScreen(point) => (None, point), - MenuPosition::PinnedToEditor { - source, - offset_x, - offset_y, - } => { + MenuPosition::PinnedToEditor { source, offset } => { let source_display_point = source.to_display_point(editor_snapshot); - let mut source_point = editor.to_pixel_point(source, editor_snapshot, cx)?; - source_point.x += offset_x; - source_point.y += offset_y; - (Some(source_display_point), source_point) + let source_point = editor.to_pixel_point(source, editor_snapshot, cx)?; + let position = content_origin + source_point + offset; + (Some(source_display_point), position) } }; @@ -4325,8 +4322,8 @@ fn deploy_blame_entry_context_menu( }); editor.update(cx, move |editor, cx| { - editor.mouse_context_menu = Some(MouseContextMenu::pinned_to_screen( - position, + editor.mouse_context_menu = Some(MouseContextMenu::new( + MenuPosition::PinnedToScreen(position), context_menu, cx, )); @@ -5578,8 +5575,12 @@ impl Element for EditorElement { ); } - let mouse_context_menu = - self.layout_mouse_context_menu(&snapshot, start_row..end_row, cx); + let mouse_context_menu = self.layout_mouse_context_menu( + &snapshot, + start_row..end_row, + content_origin, + cx, + ); cx.with_element_namespace("crease_toggles", |cx| { self.prepaint_crease_toggles( diff --git a/crates/editor/src/mouse_context_menu.rs b/crates/editor/src/mouse_context_menu.rs index 9abf4d990c..6861d424ec 100644 --- a/crates/editor/src/mouse_context_menu.rs +++ b/crates/editor/src/mouse_context_menu.rs @@ -20,8 +20,7 @@ pub enum MenuPosition { /// Disappears when the position is no longer visible. PinnedToEditor { source: multi_buffer::Anchor, - offset_x: Pixels, - offset_y: Pixels, + offset: Point, }, } @@ -48,36 +47,22 @@ impl MouseContextMenu { context_menu: View, cx: &mut ViewContext, ) -> Option { - let context_menu_focus = context_menu.focus_handle(cx); - cx.focus(&context_menu_focus); - - let _subscription = cx.subscribe( - &context_menu, - move |editor, _, _event: &DismissEvent, cx| { - editor.mouse_context_menu.take(); - if context_menu_focus.contains_focused(cx) { - editor.focus(cx); - } - }, - ); - let editor_snapshot = editor.snapshot(cx); - let source_point = editor.to_pixel_point(source, &editor_snapshot, cx)?; - let offset = position - source_point; - - Some(Self { - position: MenuPosition::PinnedToEditor { - source, - offset_x: offset.x, - offset_y: offset.y, - }, - context_menu, - _subscription, - }) + let content_origin = editor.last_bounds?.origin + + Point { + x: editor.gutter_dimensions.width, + y: Pixels(0.0), + }; + let source_position = editor.to_pixel_point(source, &editor_snapshot, cx)?; + let menu_position = MenuPosition::PinnedToEditor { + source, + offset: position - (source_position + content_origin), + }; + return Some(MouseContextMenu::new(menu_position, context_menu, cx)); } - pub(crate) fn pinned_to_screen( - position: Point, + pub(crate) fn new( + position: MenuPosition, context_menu: View, cx: &mut ViewContext, ) -> Self { @@ -95,7 +80,7 @@ impl MouseContextMenu { ); Self { - position: MenuPosition::PinnedToScreen(position), + position, context_menu, _subscription, } @@ -119,7 +104,7 @@ fn display_ranges<'a>( pub fn deploy_context_menu( editor: &mut Editor, - position: Point, + position: Option>, point: DisplayPoint, cx: &mut ViewContext, ) { @@ -213,8 +198,18 @@ pub fn deploy_context_menu( }) }; - editor.mouse_context_menu = - MouseContextMenu::pinned_to_editor(editor, source_anchor, position, context_menu, cx); + editor.mouse_context_menu = match position { + Some(position) => { + MouseContextMenu::pinned_to_editor(editor, source_anchor, position, context_menu, cx) + } + None => { + let menu_position = MenuPosition::PinnedToEditor { + source: source_anchor, + offset: editor.character_size(cx), + }; + Some(MouseContextMenu::new(menu_position, context_menu, cx)) + } + }; cx.notify(); } @@ -248,7 +243,9 @@ mod tests { } "}); cx.editor(|editor, _app| assert!(editor.mouse_context_menu.is_none())); - cx.update_editor(|editor, cx| deploy_context_menu(editor, Default::default(), point, cx)); + cx.update_editor(|editor, cx| { + deploy_context_menu(editor, Some(Default::default()), point, cx) + }); cx.assert_editor_state(indoc! {" fn test() { From 8d18dfa4c1c91ee5c77f3e4be3c8b6116d2992fc Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 4 Dec 2024 23:36:36 +0200 Subject: [PATCH 288/886] Add a prototype with a multi buffer having all project git changes (#21543) Part of https://github.com/zed-industries/zed/issues/20925 This prototype is behind a feature flag and being merged to avoid conflicts with further git-related resturctures. To be a proper, public feature, this needs at least: * showing deleted files * better performance * randomized tests * `TODO`s in the `project_diff.rs` file fixed The good thing is, >90% of the changes are in the `project_diff.rs` file only, have a basic test and already work on simple cases. Release Notes: - N/A --------- Co-authored-by: Thorsten Ball Co-authored-by: Cole Miller --- Cargo.lock | 2 + crates/editor/Cargo.toml | 2 + crates/editor/src/editor.rs | 1 + crates/editor/src/git.rs | 1 + crates/editor/src/git/blame.rs | 2 +- crates/editor/src/git/project_diff.rs | 1235 ++++++++++++++++++++ crates/file_finder/src/file_finder.rs | 2 +- crates/git/src/diff.rs | 1 - crates/language/src/buffer.rs | 1 - crates/project/src/project.rs | 41 +- crates/project_panel/src/project_panel.rs | 2 +- crates/semantic_index/src/project_index.rs | 2 +- crates/workspace/src/workspace.rs | 4 +- 13 files changed, 1269 insertions(+), 27 deletions(-) create mode 100644 crates/editor/src/git/project_diff.rs diff --git a/Cargo.lock b/Cargo.lock index 72bace069b..f3c0fa3176 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3790,6 +3790,7 @@ dependencies = [ "db", "emojis", "env_logger 0.11.5", + "feature_flags", "file_icons", "fs", "futures 0.3.31", @@ -3823,6 +3824,7 @@ dependencies = [ "snippet", "sum_tree", "task", + "tempfile", "text", "theme", "time", diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index f1f1b34981..166e7383fc 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -39,6 +39,7 @@ collections.workspace = true convert_case.workspace = true db.workspace = true emojis.workspace = true +feature_flags.workspace = true file_icons.workspace = true futures.workspace = true fuzzy.workspace = true @@ -97,6 +98,7 @@ project = { workspace = true, features = ["test-support"] } release_channel.workspace = true rand.workspace = true settings = { workspace = true, features = ["test-support"] } +tempfile.workspace = true text = { workspace = true, features = ["test-support"] } theme = { workspace = true, features = ["test-support"] } tree-sitter-html.workspace = true diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 883ea570a9..b11e15b567 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -327,6 +327,7 @@ pub fn init(cx: &mut AppContext) { .detach(); } }); + git::project_diff::init(cx); } pub struct SearchWithinRange; diff --git a/crates/editor/src/git.rs b/crates/editor/src/git.rs index 080babe4c6..97ca80ea29 100644 --- a/crates/editor/src/git.rs +++ b/crates/editor/src/git.rs @@ -1 +1,2 @@ pub mod blame; +pub mod project_diff; diff --git a/crates/editor/src/git/blame.rs b/crates/editor/src/git/blame.rs index c5cfb2e850..b4fe2efec6 100644 --- a/crates/editor/src/git/blame.rs +++ b/crates/editor/src/git/blame.rs @@ -154,7 +154,7 @@ impl GitBlame { this.generate(cx); } } - project::Event::WorktreeUpdatedGitRepositories => { + project::Event::WorktreeUpdatedGitRepositories(_) => { log::debug!("Status of git repositories updated. Regenerating blame data...",); this.generate(cx); } diff --git a/crates/editor/src/git/project_diff.rs b/crates/editor/src/git/project_diff.rs new file mode 100644 index 0000000000..3e28e28a18 --- /dev/null +++ b/crates/editor/src/git/project_diff.rs @@ -0,0 +1,1235 @@ +use std::{ + any::{Any, TypeId}, + cmp::Ordering, + collections::HashSet, + ops::Range, + time::Duration, +}; + +use anyhow::Context as _; +use collections::{BTreeMap, HashMap}; +use feature_flags::FeatureFlagAppExt; +use futures::{stream::FuturesUnordered, StreamExt}; +use git::{diff::DiffHunk, repository::GitFileStatus}; +use gpui::{ + actions, AnyElement, AnyView, AppContext, EventEmitter, FocusHandle, FocusableView, + InteractiveElement, Model, Render, Subscription, Task, View, WeakView, +}; +use language::{Buffer, BufferRow, BufferSnapshot}; +use multi_buffer::{ExcerptId, ExcerptRange, ExpandExcerptDirection, MultiBuffer}; +use project::{Project, ProjectEntryId, ProjectPath, WorktreeId}; +use text::{OffsetRangeExt, ToPoint}; +use theme::ActiveTheme; +use ui::{ + div, h_flex, Color, Context, FluentBuilder, Icon, IconName, IntoElement, Label, LabelCommon, + ParentElement, SharedString, Styled, ViewContext, VisualContext, WindowContext, +}; +use util::{paths::compare_paths, ResultExt}; +use workspace::{ + item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams}, + ItemNavHistory, ToolbarItemLocation, Workspace, +}; + +use crate::{Editor, EditorEvent, DEFAULT_MULTIBUFFER_CONTEXT}; + +actions!(project_diff, [Deploy]); + +pub fn init(cx: &mut AppContext) { + cx.observe_new_views(ProjectDiffEditor::register).detach(); +} + +const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50); + +struct ProjectDiffEditor { + buffer_changes: BTreeMap>, + entry_order: HashMap>, + excerpts: Model, + editor: View, + + project: Model, + workspace: WeakView, + focus_handle: FocusHandle, + worktree_rescans: HashMap>, + _subscriptions: Vec, +} + +struct Changes { + _status: GitFileStatus, + buffer: Model, + hunks: Vec, +} + +impl ProjectDiffEditor { + fn register(workspace: &mut Workspace, cx: &mut ViewContext) { + if cx.is_staff() { + workspace.register_action(Self::deploy); + } + } + + fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { + if let Some(existing) = workspace.item_of_type::(cx) { + workspace.activate_item(&existing, true, true, cx); + } else { + let workspace_handle = cx.view().downgrade(); + let project_diff = + cx.new_view(|cx| Self::new(workspace.project().clone(), workspace_handle, cx)); + workspace.add_item_to_active_pane(Box::new(project_diff), None, true, cx); + } + } + + fn new( + project: Model, + workspace: WeakView, + cx: &mut ViewContext, + ) -> Self { + // TODO diff change subscriptions. For that, needed: + // * `-20/+50` stats retrieval: some background process that reacts on file changes + let focus_handle = cx.focus_handle(); + let changed_entries_subscription = + cx.subscribe(&project, |project_diff_editor, _, e, cx| { + let mut worktree_to_rescan = None; + match e { + project::Event::WorktreeAdded(id) => { + worktree_to_rescan = Some(*id); + // project_diff_editor + // .buffer_changes + // .insert(*id, HashMap::default()); + } + project::Event::WorktreeRemoved(id) => { + project_diff_editor.buffer_changes.remove(id); + } + project::Event::WorktreeUpdatedEntries(id, _updated_entries) => { + // TODO cannot invalidate buffer entries without invalidating the corresponding excerpts and order entries. + worktree_to_rescan = Some(*id); + // let entry_changes = + // project_diff_editor.buffer_changes.entry(*id).or_default(); + // for (_, entry_id, change) in updated_entries.iter() { + // let changes = entry_changes.entry(*entry_id); + // match change { + // project::PathChange::Removed => { + // if let hash_map::Entry::Occupied(entry) = changes { + // entry.remove(); + // } + // } + // // TODO understand the invalidation case better: now, we do that but still rescan the entire worktree + // // What if we already have the buffer loaded inside the diff multi buffer and it was edited there? We should not do anything. + // _ => match changes { + // hash_map::Entry::Occupied(mut o) => o.get_mut().invalidate(), + // hash_map::Entry::Vacant(v) => { + // v.insert(None); + // } + // }, + // } + // } + } + project::Event::WorktreeUpdatedGitRepositories(id) => { + worktree_to_rescan = Some(*id); + // project_diff_editor.buffer_changes.clear(); + } + project::Event::DeletedEntry(id, _entry_id) => { + worktree_to_rescan = Some(*id); + // if let Some(entries) = project_diff_editor.buffer_changes.get_mut(id) { + // entries.remove(entry_id); + // } + } + project::Event::Closed => { + project_diff_editor.buffer_changes.clear(); + } + _ => {} + } + + if let Some(worktree_to_rescan) = worktree_to_rescan { + project_diff_editor.schedule_worktree_rescan(worktree_to_rescan, cx); + } + }); + + let excerpts = cx.new_model(|cx| MultiBuffer::new(project.read(cx).capability())); + + let editor = cx.new_view(|cx| { + let mut diff_display_editor = + Editor::for_multibuffer(excerpts.clone(), Some(project.clone()), true, cx); + diff_display_editor.set_expand_all_diff_hunks(); + diff_display_editor + }); + + let mut new_self = Self { + project, + workspace, + buffer_changes: BTreeMap::default(), + entry_order: HashMap::default(), + worktree_rescans: HashMap::default(), + focus_handle, + editor, + excerpts, + _subscriptions: vec![changed_entries_subscription], + }; + new_self.schedule_rescan_all(cx); + new_self + } + + fn schedule_rescan_all(&mut self, cx: &mut ViewContext) { + let mut current_worktrees = HashSet::::default(); + for worktree in self.project.read(cx).worktrees(cx).collect::>() { + let worktree_id = worktree.read(cx).id(); + current_worktrees.insert(worktree_id); + self.schedule_worktree_rescan(worktree_id, cx); + } + + self.worktree_rescans + .retain(|worktree_id, _| current_worktrees.contains(worktree_id)); + self.buffer_changes + .retain(|worktree_id, _| current_worktrees.contains(worktree_id)); + self.entry_order + .retain(|worktree_id, _| current_worktrees.contains(worktree_id)); + } + + fn schedule_worktree_rescan(&mut self, id: WorktreeId, cx: &mut ViewContext) { + let project = self.project.clone(); + self.worktree_rescans.insert( + id, + cx.spawn(|project_diff_editor, mut cx| async move { + cx.background_executor().timer(UPDATE_DEBOUNCE).await; + let open_tasks = project + .update(&mut cx, |project, cx| { + let worktree = project.worktree_for_id(id, cx)?; + let applicable_entries = worktree + .read(cx) + .entries(false, 0) + .filter(|entry| !entry.is_external) + .filter(|entry| entry.is_file()) + .filter_map(|entry| Some((entry.git_status?, entry))) + .filter_map(|(git_status, entry)| { + Some((git_status, entry.id, project.path_for_entry(entry.id, cx)?)) + }) + .collect::>(); + Some( + applicable_entries + .into_iter() + .map(|(status, entry_id, entry_path)| { + let open_task = project.open_path(entry_path.clone(), cx); + (status, entry_id, entry_path, open_task) + }) + .collect::>(), + ) + }) + .ok() + .flatten() + .unwrap_or_default(); + let buffers_with_git_diff = cx + .background_executor() + .spawn(async move { + let mut open_tasks = open_tasks + .into_iter() + .map(|(status, entry_id, entry_path, open_task)| async move { + let (_, opened_model) = open_task.await.with_context(|| { + format!( + "loading buffer {} for git diff", + entry_path.path.display() + ) + })?; + let buffer = match opened_model.downcast::() { + Ok(buffer) => buffer, + Err(_model) => anyhow::bail!( + "Could not load {} as a buffer for git diff", + entry_path.path.display() + ), + }; + anyhow::Ok((status, entry_id, entry_path, buffer)) + }) + .collect::>(); + + let mut buffers_with_git_diff = Vec::new(); + while let Some(opened_buffer) = open_tasks.next().await { + if let Some(opened_buffer) = opened_buffer.log_err() { + buffers_with_git_diff.push(opened_buffer); + } + } + buffers_with_git_diff + }) + .await; + + let Some((buffers, mut new_entries)) = cx + .update(|cx| { + let mut buffers = HashMap::< + ProjectEntryId, + (GitFileStatus, Model, BufferSnapshot), + >::default(); + let mut new_entries = Vec::new(); + for (status, entry_id, entry_path, buffer) in buffers_with_git_diff { + let buffer_snapshot = buffer.read(cx).snapshot(); + buffers.insert(entry_id, (status, buffer, buffer_snapshot)); + new_entries.push((entry_path, entry_id)); + } + (buffers, new_entries) + }) + .ok() + else { + return; + }; + + let (new_changes, new_entry_order) = cx + .background_executor() + .spawn(async move { + let mut new_changes = HashMap::::default(); + for (entry_id, (status, buffer, buffer_snapshot)) in buffers { + new_changes.insert( + entry_id, + Changes { + _status: status, + buffer, + hunks: buffer_snapshot + .git_diff_hunks_in_row_range(0..BufferRow::MAX) + .collect::>(), + }, + ); + } + + new_entries.sort_by(|(project_path_a, _), (project_path_b, _)| { + compare_paths( + (project_path_a.path.as_ref(), true), + (project_path_b.path.as_ref(), true), + ) + }); + (new_changes, new_entries) + }) + .await; + + let mut diff_recalculations = FuturesUnordered::new(); + project_diff_editor + .update(&mut cx, |project_diff_editor, cx| { + project_diff_editor.update_excerpts(id, new_changes, new_entry_order, cx); + for buffer in project_diff_editor + .editor + .read(cx) + .buffer() + .read(cx) + .all_buffers() + { + buffer.update(cx, |buffer, cx| { + if let Some(diff_recalculation) = buffer.recalculate_diff(cx) { + diff_recalculations.push(diff_recalculation); + } + }); + } + }) + .ok(); + + cx.background_executor() + .spawn(async move { + while let Some(()) = diff_recalculations.next().await { + // another diff is calculated + } + }) + .await; + }), + ); + } + + fn update_excerpts( + &mut self, + worktree_id: WorktreeId, + new_changes: HashMap, + new_entry_order: Vec<(ProjectPath, ProjectEntryId)>, + cx: &mut ViewContext, + ) { + if let Some(current_order) = self.entry_order.get(&worktree_id) { + let current_entries = self.buffer_changes.entry(worktree_id).or_default(); + let mut new_order_entries = new_entry_order.iter().fuse().peekable(); + let mut excerpts_to_remove = Vec::new(); + let mut new_excerpt_hunks = BTreeMap::< + ExcerptId, + Vec<(ProjectPath, Model, Vec>)>, + >::new(); + let mut excerpt_to_expand = + HashMap::<(u32, ExpandExcerptDirection), Vec>::default(); + let mut latest_excerpt_id = ExcerptId::min(); + + for (current_path, current_entry_id) in current_order { + let current_changes = match current_entries.get(current_entry_id) { + Some(current_changes) => { + if current_changes.hunks.is_empty() { + continue; + } + current_changes + } + None => continue, + }; + let buffer_excerpts = self + .excerpts + .read(cx) + .excerpts_for_buffer(¤t_changes.buffer, cx); + let last_current_excerpt_id = + buffer_excerpts.last().map(|(excerpt_id, _)| *excerpt_id); + let mut current_excerpts = buffer_excerpts.into_iter().fuse().peekable(); + loop { + match new_order_entries.peek() { + Some((new_path, new_entry)) => { + match compare_paths( + (current_path.path.as_ref(), true), + (new_path.path.as_ref(), true), + ) { + Ordering::Less => { + excerpts_to_remove + .extend(current_excerpts.map(|(excerpt_id, _)| excerpt_id)); + break; + } + Ordering::Greater => { + if let Some(new_changes) = new_changes.get(new_entry) { + if !new_changes.hunks.is_empty() { + let hunks = new_excerpt_hunks + .entry(latest_excerpt_id) + .or_default(); + match hunks.binary_search_by(|(probe, ..)| { + compare_paths( + (new_path.path.as_ref(), true), + (probe.path.as_ref(), true), + ) + }) { + Ok(i) => hunks[i].2.extend( + new_changes + .hunks + .iter() + .map(|hunk| hunk.buffer_range.clone()), + ), + Err(i) => hunks.insert( + i, + ( + new_path.clone(), + new_changes.buffer.clone(), + new_changes + .hunks + .iter() + .map(|hunk| hunk.buffer_range.clone()) + .collect(), + ), + ), + } + } + }; + let _ = new_order_entries.next(); + } + Ordering::Equal => { + match new_changes.get(new_entry) { + Some(new_changes) => { + let buffer_snapshot = + new_changes.buffer.read(cx).snapshot(); + let mut current_hunks = + current_changes.hunks.iter().fuse().peekable(); + let mut new_hunks_unchanged = + Vec::with_capacity(new_changes.hunks.len()); + let mut new_hunks_with_updates = + Vec::with_capacity(new_changes.hunks.len()); + 'new_changes: for new_hunk in &new_changes.hunks { + loop { + match current_hunks.peek() { + Some(current_hunk) => { + match ( + current_hunk + .buffer_range + .start + .cmp( + &new_hunk + .buffer_range + .start, + &buffer_snapshot, + ), + current_hunk.buffer_range.end.cmp( + &new_hunk.buffer_range.end, + &buffer_snapshot, + ), + ) { + ( + Ordering::Equal, + Ordering::Equal, + ) => { + new_hunks_unchanged + .push(new_hunk); + let _ = current_hunks.next(); + continue 'new_changes; + } + (Ordering::Equal, _) + | (_, Ordering::Equal) => { + new_hunks_with_updates + .push(new_hunk); + continue 'new_changes; + } + ( + Ordering::Less, + Ordering::Greater, + ) + | ( + Ordering::Greater, + Ordering::Less, + ) => { + new_hunks_with_updates + .push(new_hunk); + continue 'new_changes; + } + ( + Ordering::Less, + Ordering::Less, + ) => { + if current_hunk + .buffer_range + .start + .cmp( + &new_hunk + .buffer_range + .end, + &buffer_snapshot, + ) + .is_le() + { + new_hunks_with_updates + .push(new_hunk); + continue 'new_changes; + } else { + let _ = + current_hunks.next(); + } + } + ( + Ordering::Greater, + Ordering::Greater, + ) => { + if current_hunk + .buffer_range + .end + .cmp( + &new_hunk + .buffer_range + .start, + &buffer_snapshot, + ) + .is_ge() + { + new_hunks_with_updates + .push(new_hunk); + continue 'new_changes; + } else { + let _ = + current_hunks.next(); + } + } + } + } + None => { + new_hunks_with_updates.push(new_hunk); + continue 'new_changes; + } + } + } + } + + let mut excerpts_with_new_changes = + HashSet::::default(); + 'new_hunks: for new_hunk in new_hunks_with_updates { + loop { + match current_excerpts.peek() { + Some(( + current_excerpt_id, + current_excerpt_range, + )) => { + match ( + current_excerpt_range + .context + .start + .cmp( + &new_hunk + .buffer_range + .start, + &buffer_snapshot, + ), + current_excerpt_range + .context + .end + .cmp( + &new_hunk.buffer_range.end, + &buffer_snapshot, + ), + ) { + ( + Ordering::Less + | Ordering::Equal, + Ordering::Greater + | Ordering::Equal, + ) => { + excerpts_with_new_changes + .insert( + *current_excerpt_id, + ); + continue 'new_hunks; + } + ( + Ordering::Greater + | Ordering::Equal, + Ordering::Less + | Ordering::Equal, + ) => { + let expand_up = current_excerpt_range + .context + .start + .to_point(&buffer_snapshot) + .row + .saturating_sub( + new_hunk + .buffer_range + .start + .to_point(&buffer_snapshot) + .row, + ); + let expand_down = new_hunk + .buffer_range + .end + .to_point(&buffer_snapshot) + .row + .saturating_sub( + current_excerpt_range + .context + .end + .to_point( + &buffer_snapshot, + ) + .row, + ); + excerpt_to_expand.entry((expand_up.max(expand_down).max(DEFAULT_MULTIBUFFER_CONTEXT), ExpandExcerptDirection::UpAndDown)).or_default().push(*current_excerpt_id); + excerpts_with_new_changes + .insert( + *current_excerpt_id, + ); + continue 'new_hunks; + } + ( + Ordering::Less, + Ordering::Less, + ) => { + if current_excerpt_range + .context + .start + .cmp( + &new_hunk + .buffer_range + .end, + &buffer_snapshot, + ) + .is_le() + { + let expand_up = current_excerpt_range + .context + .start + .to_point(&buffer_snapshot) + .row + .saturating_sub( + new_hunk.buffer_range + .start + .to_point( + &buffer_snapshot, + ) + .row, + ); + excerpt_to_expand.entry((expand_up.max(DEFAULT_MULTIBUFFER_CONTEXT), ExpandExcerptDirection::Up)).or_default().push(*current_excerpt_id); + excerpts_with_new_changes + .insert( + *current_excerpt_id, + ); + continue 'new_hunks; + } else { + if !new_changes + .hunks + .is_empty() + { + let hunks = new_excerpt_hunks + .entry(latest_excerpt_id) + .or_default(); + match hunks.binary_search_by(|(probe, ..)| { + compare_paths( + (new_path.path.as_ref(), true), + (probe.path.as_ref(), true), + ) + }) { + Ok(i) => hunks[i].2.extend( + new_changes + .hunks + .iter() + .map(|hunk| hunk.buffer_range.clone()), + ), + Err(i) => hunks.insert( + i, + ( + new_path.clone(), + new_changes.buffer.clone(), + new_changes + .hunks + .iter() + .map(|hunk| hunk.buffer_range.clone()) + .collect(), + ), + ), + } + } + continue 'new_hunks; + } + } + /* TODO remove or leave? + [ ><<<<<<<--]----<-- + cur_s > cur_e < + > < + new_s>>>>>>>>< + */ + ( + Ordering::Greater, + Ordering::Greater, + ) => { + if current_excerpt_range + .context + .end + .cmp( + &new_hunk + .buffer_range + .start, + &buffer_snapshot, + ) + .is_ge() + { + let expand_down = new_hunk + .buffer_range + .end + .to_point(&buffer_snapshot) + .row + .saturating_sub( + current_excerpt_range + .context + .end + .to_point( + &buffer_snapshot, + ) + .row, + ); + excerpt_to_expand.entry((expand_down.max(DEFAULT_MULTIBUFFER_CONTEXT), ExpandExcerptDirection::Down)).or_default().push(*current_excerpt_id); + excerpts_with_new_changes + .insert( + *current_excerpt_id, + ); + continue 'new_hunks; + } else { + latest_excerpt_id = + *current_excerpt_id; + let _ = + current_excerpts.next(); + } + } + } + } + None => { + let hunks = new_excerpt_hunks + .entry(latest_excerpt_id) + .or_default(); + match hunks.binary_search_by( + |(probe, ..)| { + compare_paths( + ( + new_path.path.as_ref(), + true, + ), + (probe.path.as_ref(), true), + ) + }, + ) { + Ok(i) => hunks[i].2.extend( + new_changes.hunks.iter().map( + |hunk| { + hunk.buffer_range + .clone() + }, + ), + ), + Err(i) => hunks.insert( + i, + ( + new_path.clone(), + new_changes.buffer.clone(), + new_changes + .hunks + .iter() + .map(|hunk| { + hunk.buffer_range + .clone() + }) + .collect(), + ), + ), + } + continue 'new_hunks; + } + } + } + } + + for (excerpt_id, excerpt_range) in current_excerpts { + if !excerpts_with_new_changes.contains(&excerpt_id) + && !new_hunks_unchanged.iter().any(|hunk| { + excerpt_range + .context + .start + .cmp( + &hunk.buffer_range.end, + &buffer_snapshot, + ) + .is_le() + && excerpt_range + .context + .end + .cmp( + &hunk.buffer_range.start, + &buffer_snapshot, + ) + .is_ge() + }) + { + excerpts_to_remove.push(excerpt_id); + } + latest_excerpt_id = excerpt_id; + } + } + None => excerpts_to_remove.extend( + current_excerpts.map(|(excerpt_id, _)| excerpt_id), + ), + } + let _ = new_order_entries.next(); + break; + } + } + } + None => { + excerpts_to_remove + .extend(current_excerpts.map(|(excerpt_id, _)| excerpt_id)); + break; + } + } + } + latest_excerpt_id = last_current_excerpt_id.unwrap_or(latest_excerpt_id); + } + + for (path, project_entry_id) in new_order_entries { + if let Some(changes) = new_changes.get(project_entry_id) { + if !changes.hunks.is_empty() { + let hunks = new_excerpt_hunks.entry(latest_excerpt_id).or_default(); + match hunks.binary_search_by(|(probe, ..)| { + compare_paths((path.path.as_ref(), true), (probe.path.as_ref(), true)) + }) { + Ok(i) => hunks[i] + .2 + .extend(changes.hunks.iter().map(|hunk| hunk.buffer_range.clone())), + Err(i) => hunks.insert( + i, + ( + path.clone(), + changes.buffer.clone(), + changes + .hunks + .iter() + .map(|hunk| hunk.buffer_range.clone()) + .collect(), + ), + ), + } + } + } + } + + self.excerpts.update(cx, |multi_buffer, cx| { + for (mut after_excerpt_id, excerpts_to_add) in new_excerpt_hunks { + for (_, buffer, hunk_ranges) in excerpts_to_add { + let buffer_snapshot = buffer.read(cx).snapshot(); + let max_point = buffer_snapshot.max_point(); + let new_excerpts = multi_buffer.insert_excerpts_after( + after_excerpt_id, + buffer, + hunk_ranges.into_iter().map(|range| { + let mut extended_point_range = range.to_point(&buffer_snapshot); + extended_point_range.start.row = extended_point_range + .start + .row + .saturating_sub(DEFAULT_MULTIBUFFER_CONTEXT); + extended_point_range.end.row = (extended_point_range.end.row + + DEFAULT_MULTIBUFFER_CONTEXT) + .min(max_point.row); + ExcerptRange { + context: extended_point_range, + primary: None, + } + }), + cx, + ); + after_excerpt_id = new_excerpts.last().copied().unwrap_or(after_excerpt_id); + } + } + multi_buffer.remove_excerpts(excerpts_to_remove, cx); + for ((line_count, direction), excerpts) in excerpt_to_expand { + multi_buffer.expand_excerpts(excerpts, line_count, direction, cx); + } + }); + } else { + self.excerpts.update(cx, |multi_buffer, cx| { + for new_changes in new_entry_order + .iter() + .filter_map(|(_, entry_id)| new_changes.get(entry_id)) + { + multi_buffer.push_excerpts_with_context_lines( + new_changes.buffer.clone(), + new_changes + .hunks + .iter() + .map(|hunk| hunk.buffer_range.clone()) + .collect(), + DEFAULT_MULTIBUFFER_CONTEXT, + cx, + ); + } + }); + }; + + let mut new_changes = new_changes; + let mut new_entry_order = new_entry_order; + std::mem::swap( + self.buffer_changes.entry(worktree_id).or_default(), + &mut new_changes, + ); + std::mem::swap( + self.entry_order.entry(worktree_id).or_default(), + &mut new_entry_order, + ); + } +} + +impl EventEmitter for ProjectDiffEditor {} + +impl FocusableView for ProjectDiffEditor { + fn focus_handle(&self, _: &AppContext) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl Item for ProjectDiffEditor { + type Event = EditorEvent; + + fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) { + Editor::to_item_events(event, f) + } + + fn deactivated(&mut self, cx: &mut ViewContext) { + self.editor.update(cx, |editor, cx| editor.deactivated(cx)); + } + + fn navigate(&mut self, data: Box, cx: &mut ViewContext) -> bool { + self.editor + .update(cx, |editor, cx| editor.navigate(data, cx)) + } + + fn tab_tooltip_text(&self, _: &AppContext) -> Option { + Some("Project Diff".into()) + } + + fn tab_content(&self, params: TabContentParams, _: &WindowContext) -> AnyElement { + if self.buffer_changes.is_empty() { + Label::new("No changes") + .color(if params.selected { + Color::Default + } else { + Color::Muted + }) + .into_any_element() + } else { + h_flex() + .gap_1() + .when(true, |then| { + then.child( + h_flex() + .gap_1() + .child(Icon::new(IconName::XCircle).color(Color::Error)) + .child(Label::new(self.buffer_changes.len().to_string()).color( + if params.selected { + Color::Default + } else { + Color::Muted + }, + )), + ) + }) + .when(true, |then| { + then.child( + h_flex() + .gap_1() + .child(Icon::new(IconName::Indicator).color(Color::Warning)) + .child(Label::new(self.buffer_changes.len().to_string()).color( + if params.selected { + Color::Default + } else { + Color::Muted + }, + )), + ) + }) + .into_any_element() + } + } + + fn telemetry_event_text(&self) -> Option<&'static str> { + Some("project diagnostics") + } + + fn for_each_project_item( + &self, + cx: &AppContext, + f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem), + ) { + self.editor.for_each_project_item(cx, f) + } + + fn is_singleton(&self, _: &AppContext) -> bool { + false + } + + fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext) { + self.editor.update(cx, |editor, _| { + editor.set_nav_history(Some(nav_history)); + }); + } + + fn clone_on_split( + &self, + _workspace_id: Option, + cx: &mut ViewContext, + ) -> Option> + where + Self: Sized, + { + Some(cx.new_view(|cx| { + ProjectDiffEditor::new(self.project.clone(), self.workspace.clone(), cx) + })) + } + + fn is_dirty(&self, cx: &AppContext) -> bool { + self.excerpts.read(cx).is_dirty(cx) + } + + fn has_conflict(&self, cx: &AppContext) -> bool { + self.excerpts.read(cx).has_conflict(cx) + } + + fn can_save(&self, _: &AppContext) -> bool { + true + } + + fn save( + &mut self, + format: bool, + project: Model, + cx: &mut ViewContext, + ) -> Task> { + self.editor.save(format, project, cx) + } + + fn save_as( + &mut self, + _: Model, + _: ProjectPath, + _: &mut ViewContext, + ) -> Task> { + unreachable!() + } + + fn reload( + &mut self, + project: Model, + cx: &mut ViewContext, + ) -> Task> { + self.editor.reload(project, cx) + } + + fn act_as_type<'a>( + &'a self, + type_id: TypeId, + self_handle: &'a View, + _: &'a AppContext, + ) -> Option { + if type_id == TypeId::of::() { + Some(self_handle.to_any()) + } else if type_id == TypeId::of::() { + Some(self.editor.to_any()) + } else { + None + } + } + + fn breadcrumb_location(&self, _: &AppContext) -> ToolbarItemLocation { + ToolbarItemLocation::PrimaryLeft + } + + fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option> { + self.editor.breadcrumbs(theme, cx) + } + + fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext) { + self.editor + .update(cx, |editor, cx| editor.added_to_workspace(workspace, cx)); + } +} + +impl Render for ProjectDiffEditor { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let child = if self.buffer_changes.is_empty() { + div() + .bg(cx.theme().colors().editor_background) + .flex() + .items_center() + .justify_center() + .size_full() + .child(Label::new("No changes in the workspace")) + } else { + div().size_full().child(self.editor.clone()) + }; + + div() + .track_focus(&self.focus_handle) + .size_full() + .child(child) + } +} + +#[cfg(test)] +mod tests { + use std::{ops::Deref as _, path::Path, sync::Arc}; + + use fs::RealFs; + use gpui::{SemanticVersion, TestAppContext, VisualTestContext}; + use settings::SettingsStore; + + use super::*; + + // TODO finish + // #[gpui::test] + // async fn randomized_tests(cx: &mut TestAppContext) { + // // Create a new project (how?? temp fs?), + // let fs = FakeFs::new(cx.executor()); + // let project = Project::test(fs, [], cx).await; + + // // create random files with random content + + // // Commit it into git somehow (technically can do with "real" fs in a temp dir) + // // + // // Apply randomized changes to the project: select a random file, random change and apply to buffers + // } + + #[gpui::test] + async fn simple_edit_test(cx: &mut TestAppContext) { + cx.executor().allow_parking(); + init_test(cx); + + let dir = tempfile::tempdir().unwrap(); + let dst = dir.path(); + + std::fs::write(dst.join("file_a"), "This is file_a").unwrap(); + std::fs::write(dst.join("file_b"), "This is file_b").unwrap(); + + run_git(dst, &["init"]); + run_git(dst, &["add", "*"]); + run_git(dst, &["commit", "-m", "Initial commit"]); + + let project = Project::test(Arc::new(RealFs::default()), [dst], cx).await; + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx); + + let file_a_editor = workspace + .update(cx, |workspace, cx| { + let file_a_editor = workspace.open_abs_path(dst.join("file_a"), true, cx); + ProjectDiffEditor::deploy(workspace, &Deploy, cx); + file_a_editor + }) + .unwrap() + .await + .expect("did not open an item at all") + .downcast::() + .expect("did not open an editor for file_a"); + + let project_diff_editor = workspace + .update(cx, |workspace, cx| { + workspace + .active_pane() + .read(cx) + .items() + .find_map(|item| item.downcast::()) + }) + .unwrap() + .expect("did not find a ProjectDiffEditor"); + project_diff_editor.update(cx, |project_diff_editor, cx| { + assert!( + project_diff_editor.editor.read(cx).text(cx).is_empty(), + "Should have no changes after opening the diff on no git changes" + ); + }); + + let old_text = file_a_editor.update(cx, |editor, cx| editor.text(cx)); + let change = "an edit after git add"; + file_a_editor + .update(cx, |file_a_editor, cx| { + file_a_editor.insert(change, cx); + file_a_editor.save(false, project.clone(), cx) + }) + .await + .expect("failed to save a file"); + cx.executor().advance_clock(Duration::from_secs(1)); + cx.run_until_parked(); + + // TODO does not work on Linux for some reason, returning a blank line + // hence disable the last check for now, and do some fiddling to avoid the warnings. + #[cfg(target_os = "linux")] + { + if true { + return; + } + } + project_diff_editor.update(cx, |project_diff_editor, cx| { + // TODO assert it better: extract added text (based on the background changes) and deleted text (based on the deleted blocks added) + assert_eq!( + project_diff_editor.editor.read(cx).text(cx), + format!("{change}{old_text}"), + "Should have a new change shown in the beginning, and the old text shown as deleted text afterwards" + ); + }); + } + + fn run_git(path: &Path, args: &[&str]) -> String { + let output = std::process::Command::new("git") + .args(args) + .current_dir(path) + .output() + .expect("git commit failed"); + + format!( + "Stdout: {}; stderr: {}", + String::from_utf8(output.stdout).unwrap(), + String::from_utf8(output.stderr).unwrap() + ) + } + + fn init_test(cx: &mut gpui::TestAppContext) { + if std::env::var("RUST_LOG").is_ok() { + env_logger::try_init().ok(); + } + + cx.update(|cx| { + assets::Assets.load_test_fonts(cx); + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + theme::init(theme::LoadThemes::JustBase, cx); + release_channel::init(SemanticVersion::default(), cx); + client::init_settings(cx); + language::init(cx); + Project::init_settings(cx); + workspace::init_settings(cx); + crate::init(cx); + }); + } +} diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 62e0818b74..10cde076e1 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -648,7 +648,7 @@ impl FileFinderDelegate { cx.subscribe(project, |file_finder, _, event, cx| { match event { project::Event::WorktreeUpdatedEntries(_, _) - | project::Event::WorktreeAdded + | project::Event::WorktreeAdded(_) | project::Event::WorktreeRemoved(_) => file_finder .picker .update(cx, |picker, cx| picker.refresh(cx)), diff --git a/crates/git/src/diff.rs b/crates/git/src/diff.rs index baad824577..23e9388a28 100644 --- a/crates/git/src/diff.rs +++ b/crates/git/src/diff.rs @@ -80,7 +80,6 @@ impl BufferDiff { self.tree.is_empty() } - #[cfg(any(test, feature = "test-support"))] pub fn hunks_in_row_range<'a>( &'a self, range: Range, diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index c9f5d54299..e39d4523d7 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -3998,7 +3998,6 @@ impl BufferSnapshot { } /// Returns all the Git diff hunks intersecting the given row range. - #[cfg(any(test, feature = "test-support"))] pub fn git_diff_hunks_in_row_range( &self, range: Range, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 30732fc8b2..74bd065c32 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -240,11 +240,11 @@ pub enum Event { LanguageNotFound(Model), ActiveEntryChanged(Option), ActivateProjectPanel, - WorktreeAdded, + WorktreeAdded(WorktreeId), WorktreeOrderChanged, WorktreeRemoved(WorktreeId), WorktreeUpdatedEntries(WorktreeId, UpdatedEntriesSet), - WorktreeUpdatedGitRepositories, + WorktreeUpdatedGitRepositories(WorktreeId), DiskBasedDiagnosticsStarted { language_server_id: LanguageServerId, }, @@ -259,7 +259,7 @@ pub enum Event { DisconnectedFromHost, DisconnectedFromSshRemote, Closed, - DeletedEntry(ProjectEntryId), + DeletedEntry(WorktreeId, ProjectEntryId), CollaboratorUpdated { old_peer_id: proto::PeerId, new_peer_id: proto::PeerId, @@ -1504,6 +1504,7 @@ impl Project { cx: &mut ModelContext, ) -> Option>> { let worktree = self.worktree_for_entry(entry_id, cx)?; + cx.emit(Event::DeletedEntry(worktree.read(cx).id(), entry_id)); worktree.update(cx, |worktree, cx| { worktree.delete_entry(entry_id, trash, cx) }) @@ -2204,7 +2205,7 @@ impl Project { match event { WorktreeStoreEvent::WorktreeAdded(worktree) => { self.on_worktree_added(worktree, cx); - cx.emit(Event::WorktreeAdded); + cx.emit(Event::WorktreeAdded(worktree.read(cx).id())); } WorktreeStoreEvent::WorktreeRemoved(_, id) => { cx.emit(Event::WorktreeRemoved(*id)); @@ -2225,23 +2226,25 @@ impl Project { } } cx.observe(worktree, |_, _, cx| cx.notify()).detach(); - cx.subscribe(worktree, |project, worktree, event, cx| match event { - worktree::Event::UpdatedEntries(changes) => { - cx.emit(Event::WorktreeUpdatedEntries( - worktree.read(cx).id(), - changes.clone(), - )); + cx.subscribe(worktree, |project, worktree, event, cx| { + let worktree_id = worktree.update(cx, |worktree, _| worktree.id()); + match event { + worktree::Event::UpdatedEntries(changes) => { + cx.emit(Event::WorktreeUpdatedEntries( + worktree.read(cx).id(), + changes.clone(), + )); - let worktree_id = worktree.update(cx, |worktree, _| worktree.id()); - project - .client() - .telemetry() - .report_discovered_project_events(worktree_id, changes); + project + .client() + .telemetry() + .report_discovered_project_events(worktree_id, changes); + } + worktree::Event::UpdatedGitRepositories(_) => { + cx.emit(Event::WorktreeUpdatedGitRepositories(worktree_id)); + } + worktree::Event::DeletedEntry(id) => cx.emit(Event::DeletedEntry(worktree_id, *id)), } - worktree::Event::UpdatedGitRepositories(_) => { - cx.emit(Event::WorktreeUpdatedGitRepositories); - } - worktree::Event::DeletedEntry(id) => cx.emit(Event::DeletedEntry(*id)), }) .detach(); cx.notify(); diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index df78ff1118..3ef9f1905d 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -304,7 +304,7 @@ impl ProjectPanel { cx.notify(); } project::Event::WorktreeUpdatedEntries(_, _) - | project::Event::WorktreeAdded + | project::Event::WorktreeAdded(_) | project::Event::WorktreeOrderChanged => { this.update_visible_entries(None, cx); cx.notify(); diff --git a/crates/semantic_index/src/project_index.rs b/crates/semantic_index/src/project_index.rs index 21c036d60a..bc18eccc18 100644 --- a/crates/semantic_index/src/project_index.rs +++ b/crates/semantic_index/src/project_index.rs @@ -125,7 +125,7 @@ impl ProjectIndex { cx: &mut ModelContext, ) { match event { - project::Event::WorktreeAdded | project::Event::WorktreeRemoved(_) => { + project::Event::WorktreeAdded(_) | project::Event::WorktreeRemoved(_) => { self.update_worktree_indices(cx); } _ => {} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index c5de8822dc..0d47cec441 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -810,7 +810,7 @@ impl Workspace { this.collaborator_left(*peer_id, cx); } - project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded => { + project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded(_) => { this.update_window_title(cx); this.serialize_workspace(cx); } @@ -832,7 +832,7 @@ impl Workspace { cx.remove_window(); } - project::Event::DeletedEntry(entry_id) => { + project::Event::DeletedEntry(_, entry_id) => { for pane in this.panes.iter() { pane.update(cx, |pane, cx| { pane.handle_deleted_project_item(*entry_id, cx) From 55ecb3c51b17e7c335e0f47d60cb262a7c1dd54e Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 4 Dec 2024 23:37:24 +0200 Subject: [PATCH 289/886] Regenerate completion labels on resolve (#21521) Closes https://github.com/zed-industries/zed/issues/21516 Technically, this is an LSP violation from `vtsls`, but seems that it's not going to be fixed adequately on that side, see https://github.com/yioneko/vtsls/issues/213 for more context. So, we have to accommodate at least for now. Release Notes: - Fixed completion item labels not being updated after the resolve for non-LSP compliant servers --- crates/editor/src/editor.rs | 4 +- crates/editor/src/editor_tests.rs | 91 +++++++++++++++++++++++++++++++ crates/project/src/lsp_store.rs | 35 ++++++++++-- 3 files changed, 124 insertions(+), 6 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b11e15b567..8af10cd0c9 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1181,9 +1181,9 @@ impl CompletionsMenu { let delay = Duration::from_millis(delay_ms); completion_resolve.lock().fire_new(delay, cx, |_, cx| { - cx.spawn(move |this, mut cx| async move { + cx.spawn(move |editor, mut cx| async move { if let Some(true) = resolve_task.await.log_err() { - this.update(&mut cx, |_, cx| cx.notify()).ok(); + editor.update(&mut cx, |_, cx| cx.notify()).ok(); } }) }); diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 0c15719ab5..136003dcc3 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -10625,6 +10625,97 @@ async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) { cx.assert_editor_state(indoc! {"fn main() { let a = Some(2)ˇ; }"}); } +#[gpui::test] +async fn test_completions_resolve_updates_labels(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string()]), + resolve_provider: Some(true), + ..Default::default() + }), + ..Default::default() + }, + cx, + ) + .await; + + cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"}); + cx.simulate_keystroke("."); + + let completion_item = lsp::CompletionItem { + label: "unresolved".to_string(), + detail: None, + documentation: None, + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)), + new_text: ".unresolved".to_string(), + })), + ..lsp::CompletionItem::default() + }; + + cx.handle_request::(move |_, _, _| { + let item = completion_item.clone(); + async move { Ok(Some(lsp::CompletionResponse::Array(vec![item]))) } + }) + .next() + .await; + + cx.condition(|editor, _| editor.context_menu_visible()) + .await; + cx.update_editor(|editor, _| { + let context_menu = editor.context_menu.read(); + let context_menu = context_menu + .as_ref() + .expect("Should have the context menu deployed"); + match context_menu { + ContextMenu::Completions(completions_menu) => { + let completions = completions_menu.completions.read(); + assert_eq!(completions.len(), 1, "Should have one completion"); + assert_eq!(completions.get(0).unwrap().label.text, "unresolved"); + } + ContextMenu::CodeActions(_) => panic!("Should show the completions menu"), + } + }); + + cx.handle_request::(move |_, _, _| async move { + Ok(lsp::CompletionItem { + label: "resolved".to_string(), + detail: Some("Now resolved!".to_string()), + documentation: Some(lsp::Documentation::String("Docs".to_string())), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)), + new_text: ".resolved".to_string(), + })), + ..lsp::CompletionItem::default() + }) + }) + .next() + .await; + cx.run_until_parked(); + + cx.update_editor(|editor, _| { + let context_menu = editor.context_menu.read(); + let context_menu = context_menu + .as_ref() + .expect("Should have the context menu deployed"); + match context_menu { + ContextMenu::Completions(completions_menu) => { + let completions = completions_menu.completions.read(); + assert_eq!(completions.len(), 1, "Should have one completion"); + assert_eq!( + completions.get(0).unwrap().label.text, + "resolved", + "Should update the completion label after resolving" + ); + } + ContextMenu::CodeActions(_) => panic!("Should show the completions menu"), + } + }); +} + #[gpui::test] async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 41a3ccc0a3..ff2a3d47e7 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -2241,17 +2241,23 @@ impl LspStore { (server_id, completion) }; - let server = this - .read_with(&cx, |this, _| this.language_server_for_id(server_id)) + let server_and_adapter = this + .read_with(&cx, |lsp_store, _| { + let server = lsp_store.language_server_for_id(server_id)?; + let adapter = + lsp_store.language_server_adapter_for_id(server.server_id())?; + Some((server, adapter)) + }) .ok() .flatten(); - let Some(server) = server else { + let Some((server, adapter)) = server_and_adapter else { continue; }; did_resolve = true; Self::resolve_completion_local( server, + adapter, &buffer_snapshot, completions.clone(), completion_index, @@ -2268,6 +2274,7 @@ impl LspStore { async fn resolve_completion_local( server: Arc, + adapter: Arc, snapshot: &BufferSnapshot, completions: Arc>>, completion_index: usize, @@ -2293,7 +2300,7 @@ impl LspStore { let documentation = language::prepare_completion_documentation( lsp_documentation, &language_registry, - None, // TODO: Try to reasonably work out which language the completion is for + snapshot.language().cloned(), ) .await; @@ -2332,9 +2339,29 @@ impl LspStore { } } + // NB: Zed does not have `details` inside the completion resolve capabilities, but certain language servers violate the spec and do not return `details` immediately, e.g. https://github.com/yioneko/vtsls/issues/213 + // So we have to update the label here anyway... + let new_label = match snapshot.language() { + Some(language) => adapter + .labels_for_completions(&[completion_item.clone()], language) + .await + .log_err() + .unwrap_or_default(), + None => Vec::new(), + } + .pop() + .flatten() + .unwrap_or_else(|| { + CodeLabel::plain( + completion_item.label.clone(), + completion_item.filter_text.as_deref(), + ) + }); + let mut completions = completions.write(); let completion = &mut completions[completion_index]; completion.lsp_completion = completion_item; + completion.label = new_label; } #[allow(clippy::too_many_arguments)] From a30ea2fc682bdaec572f3e130631e297670612a6 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 4 Dec 2024 16:39:39 -0500 Subject: [PATCH 290/886] assistant2: Factor out `ActiveThread` view (#21555) This PR factors a new `ActiveThread` view out of the `AssistantPanel` to group together the state that pertains solely to the active view. There was a bunch of related state on the `AssistantPanel` pertaining to the active thread that needed to be initialized/reset together and it makes for a clearer narrative is this state is encapsulated in its own view. Release Notes: - N/A --- crates/assistant2/src/active_thread.rs | 237 ++++++++++++ crates/assistant2/src/assistant.rs | 1 + crates/assistant2/src/assistant_panel.rs | 436 ++++++++--------------- crates/assistant2/src/thread.rs | 4 + crates/assistant2/src/thread_store.rs | 9 +- 5 files changed, 396 insertions(+), 291 deletions(-) create mode 100644 crates/assistant2/src/active_thread.rs diff --git a/crates/assistant2/src/active_thread.rs b/crates/assistant2/src/active_thread.rs new file mode 100644 index 0000000000..13b67dc437 --- /dev/null +++ b/crates/assistant2/src/active_thread.rs @@ -0,0 +1,237 @@ +use std::sync::Arc; + +use assistant_tool::ToolWorkingSet; +use collections::HashMap; +use gpui::{ + list, AnyElement, Empty, ListAlignment, ListState, Model, StyleRefinement, Subscription, + TextStyleRefinement, View, WeakView, +}; +use language::LanguageRegistry; +use language_model::Role; +use markdown::{Markdown, MarkdownStyle}; +use settings::Settings as _; +use theme::ThemeSettings; +use ui::prelude::*; +use workspace::Workspace; + +use crate::thread::{MessageId, Thread, ThreadError, ThreadEvent}; + +pub struct ActiveThread { + workspace: WeakView, + language_registry: Arc, + tools: Arc, + thread: Model, + messages: Vec, + list_state: ListState, + rendered_messages_by_id: HashMap>, + last_error: Option, + _subscriptions: Vec, +} + +impl ActiveThread { + pub fn new( + thread: Model, + workspace: WeakView, + language_registry: Arc, + tools: Arc, + cx: &mut ViewContext, + ) -> Self { + let subscriptions = vec![ + cx.observe(&thread, |_, _, cx| cx.notify()), + cx.subscribe(&thread, Self::handle_thread_event), + ]; + + let mut this = Self { + workspace, + language_registry, + tools, + thread: thread.clone(), + messages: Vec::new(), + rendered_messages_by_id: HashMap::default(), + list_state: ListState::new(0, ListAlignment::Bottom, px(1024.), { + let this = cx.view().downgrade(); + move |ix, cx: &mut WindowContext| { + this.update(cx, |this, cx| this.render_message(ix, cx)) + .unwrap() + } + }), + last_error: None, + _subscriptions: subscriptions, + }; + + for message in thread.read(cx).messages().cloned().collect::>() { + this.push_message(&message.id, message.text.clone(), cx); + } + + this + } + + pub fn is_empty(&self) -> bool { + self.messages.is_empty() + } + + pub fn last_error(&self) -> Option { + self.last_error.clone() + } + + pub fn clear_last_error(&mut self) { + self.last_error.take(); + } + + fn push_message(&mut self, id: &MessageId, text: String, cx: &mut ViewContext) { + let old_len = self.messages.len(); + self.messages.push(*id); + self.list_state.splice(old_len..old_len, 1); + + let theme_settings = ThemeSettings::get_global(cx); + let ui_font_size = TextSize::Default.rems(cx); + let buffer_font_size = theme_settings.buffer_font_size; + + let mut text_style = cx.text_style(); + text_style.refine(&TextStyleRefinement { + font_family: Some(theme_settings.ui_font.family.clone()), + font_size: Some(ui_font_size.into()), + color: Some(cx.theme().colors().text), + ..Default::default() + }); + + let markdown_style = MarkdownStyle { + base_text_style: text_style, + syntax: cx.theme().syntax().clone(), + selection_background_color: cx.theme().players().local().selection, + code_block: StyleRefinement { + text: Some(TextStyleRefinement { + font_family: Some(theme_settings.buffer_font.family.clone()), + font_size: Some(buffer_font_size.into()), + ..Default::default() + }), + ..Default::default() + }, + inline_code: TextStyleRefinement { + font_family: Some(theme_settings.buffer_font.family.clone()), + font_size: Some(ui_font_size.into()), + background_color: Some(cx.theme().colors().editor_background), + ..Default::default() + }, + ..Default::default() + }; + + let markdown = cx.new_view(|cx| { + Markdown::new( + text, + markdown_style, + Some(self.language_registry.clone()), + None, + cx, + ) + }); + self.rendered_messages_by_id.insert(*id, markdown); + } + + fn handle_thread_event( + &mut self, + _: Model, + event: &ThreadEvent, + cx: &mut ViewContext, + ) { + match event { + ThreadEvent::ShowError(error) => { + self.last_error = Some(error.clone()); + } + ThreadEvent::StreamedCompletion => {} + ThreadEvent::StreamedAssistantText(message_id, text) => { + if let Some(markdown) = self.rendered_messages_by_id.get_mut(&message_id) { + markdown.update(cx, |markdown, cx| { + markdown.append(text, cx); + }); + } + } + ThreadEvent::MessageAdded(message_id) => { + if let Some(message_text) = self + .thread + .read(cx) + .message(*message_id) + .map(|message| message.text.clone()) + { + self.push_message(message_id, message_text, cx); + } + + cx.notify(); + } + ThreadEvent::UsePendingTools => { + let pending_tool_uses = self + .thread + .read(cx) + .pending_tool_uses() + .into_iter() + .filter(|tool_use| tool_use.status.is_idle()) + .cloned() + .collect::>(); + + for tool_use in pending_tool_uses { + if let Some(tool) = self.tools.tool(&tool_use.name, cx) { + let task = tool.run(tool_use.input, self.workspace.clone(), cx); + + self.thread.update(cx, |thread, cx| { + thread.insert_tool_output( + tool_use.assistant_message_id, + tool_use.id.clone(), + task, + cx, + ); + }); + } + } + } + ThreadEvent::ToolFinished { .. } => {} + } + } + + fn render_message(&self, ix: usize, cx: &mut ViewContext) -> AnyElement { + let message_id = self.messages[ix]; + let Some(message) = self.thread.read(cx).message(message_id) else { + return Empty.into_any(); + }; + + let Some(markdown) = self.rendered_messages_by_id.get(&message_id) else { + return Empty.into_any(); + }; + + let (role_icon, role_name) = match message.role { + Role::User => (IconName::Person, "You"), + Role::Assistant => (IconName::ZedAssistant, "Assistant"), + Role::System => (IconName::Settings, "System"), + }; + + div() + .id(("message-container", ix)) + .p_2() + .child( + v_flex() + .border_1() + .border_color(cx.theme().colors().border_variant) + .rounded_md() + .child( + h_flex() + .justify_between() + .p_1p5() + .border_b_1() + .border_color(cx.theme().colors().border_variant) + .child( + h_flex() + .gap_2() + .child(Icon::new(role_icon).size(IconSize::Small)) + .child(Label::new(role_name).size(LabelSize::Small)), + ), + ) + .child(v_flex().p_1p5().text_ui(cx).child(markdown.clone())), + ) + .into_any() + } +} + +impl Render for ActiveThread { + fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { + list(self.list_state.clone()).flex_1() + } +} diff --git a/crates/assistant2/src/assistant.rs b/crates/assistant2/src/assistant.rs index aa79ce0c67..dfa361ad8c 100644 --- a/crates/assistant2/src/assistant.rs +++ b/crates/assistant2/src/assistant.rs @@ -1,3 +1,4 @@ +mod active_thread; mod assistant_panel; mod message_editor; mod thread; diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index 00bd15de2e..d17480cd0e 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -3,25 +3,21 @@ use std::sync::Arc; use anyhow::Result; use assistant_tool::ToolWorkingSet; use client::zed_urls; -use collections::HashMap; use gpui::{ - list, prelude::*, px, svg, Action, AnyElement, AppContext, AsyncWindowContext, Empty, - EventEmitter, FocusHandle, FocusableView, FontWeight, ListAlignment, ListState, Model, Pixels, - StyleRefinement, Subscription, Task, TextStyleRefinement, View, ViewContext, WeakView, + prelude::*, px, svg, Action, AnyElement, AppContext, AsyncWindowContext, EventEmitter, + FocusHandle, FocusableView, FontWeight, Model, Pixels, Task, View, ViewContext, WeakView, WindowContext, }; use language::LanguageRegistry; -use language_model::{LanguageModelRegistry, Role}; +use language_model::LanguageModelRegistry; use language_model_selector::LanguageModelSelector; -use markdown::{Markdown, MarkdownStyle}; -use settings::Settings; -use theme::ThemeSettings; use ui::{prelude::*, ButtonLike, Divider, IconButtonShape, KeyBinding, ListItem, Tab, Tooltip}; use workspace::dock::{DockPosition, Panel, PanelEvent}; use workspace::Workspace; +use crate::active_thread::ActiveThread; use crate::message_editor::MessageEditor; -use crate::thread::{MessageId, Thread, ThreadError, ThreadEvent, ThreadId}; +use crate::thread::{Thread, ThreadError, ThreadId}; use crate::thread_store::ThreadStore; use crate::{NewThread, OpenHistory, ToggleFocus, ToggleModelSelector}; @@ -39,16 +35,10 @@ pub fn init(cx: &mut AppContext) { pub struct AssistantPanel { workspace: WeakView, language_registry: Arc, - #[allow(unused)] thread_store: Model, - thread: Model, - thread_messages: Vec, - rendered_messages_by_id: HashMap>, - thread_list_state: ListState, + thread: Option>, message_editor: View, tools: Arc, - last_error: Option, - _subscriptions: Vec, } impl AssistantPanel { @@ -78,29 +68,14 @@ impl AssistantPanel { cx: &mut ViewContext, ) -> Self { let thread = thread_store.update(cx, |this, cx| this.create_thread(cx)); - let subscriptions = vec![ - cx.observe(&thread, |_, _, cx| cx.notify()), - cx.subscribe(&thread, Self::handle_thread_event), - ]; Self { workspace: workspace.weak_handle(), language_registry: workspace.project().read(cx).languages().clone(), thread_store, - thread: thread.clone(), - thread_messages: Vec::new(), - rendered_messages_by_id: HashMap::default(), - thread_list_state: ListState::new(0, ListAlignment::Bottom, px(1024.), { - let this = cx.view().downgrade(); - move |ix, cx: &mut WindowContext| { - this.update(cx, |this, cx| this.render_message(ix, cx)) - .unwrap() - } - }), + thread: None, message_editor: cx.new_view(|cx| MessageEditor::new(thread, cx)), tools, - last_error: None, - _subscriptions: subscriptions, } } @@ -108,7 +83,18 @@ impl AssistantPanel { let thread = self .thread_store .update(cx, |this, cx| this.create_thread(cx)); - self.reset_thread(thread, cx); + + self.thread = Some(cx.new_view(|cx| { + ActiveThread::new( + thread.clone(), + self.workspace.clone(), + self.language_registry.clone(), + self.tools.clone(), + cx, + ) + })); + self.message_editor = cx.new_view(|cx| MessageEditor::new(thread, cx)); + self.message_editor.focus_handle(cx).focus(cx); } fn open_thread(&mut self, thread_id: &ThreadId, cx: &mut ViewContext) { @@ -118,136 +104,18 @@ impl AssistantPanel { else { return; }; - self.reset_thread(thread.clone(), cx); - for message in thread.read(cx).messages().cloned().collect::>() { - self.push_message(&message.id, message.text.clone(), cx); - } - } - - fn reset_thread(&mut self, thread: Model, cx: &mut ViewContext) { - let subscriptions = vec![ - cx.observe(&thread, |_, _, cx| cx.notify()), - cx.subscribe(&thread, Self::handle_thread_event), - ]; - - self.message_editor = cx.new_view(|cx| MessageEditor::new(thread.clone(), cx)); - self.thread = thread; - self.thread_messages.clear(); - self.thread_list_state.reset(0); - self.rendered_messages_by_id.clear(); - self._subscriptions = subscriptions; - - self.message_editor.focus_handle(cx).focus(cx); - } - - fn push_message(&mut self, id: &MessageId, text: String, cx: &mut ViewContext) { - let old_len = self.thread_messages.len(); - self.thread_messages.push(*id); - self.thread_list_state.splice(old_len..old_len, 1); - - let theme_settings = ThemeSettings::get_global(cx); - let ui_font_size = TextSize::Default.rems(cx); - let buffer_font_size = theme_settings.buffer_font_size; - - let mut text_style = cx.text_style(); - text_style.refine(&TextStyleRefinement { - font_family: Some(theme_settings.ui_font.family.clone()), - font_size: Some(ui_font_size.into()), - color: Some(cx.theme().colors().text), - ..Default::default() - }); - - let markdown_style = MarkdownStyle { - base_text_style: text_style, - syntax: cx.theme().syntax().clone(), - selection_background_color: cx.theme().players().local().selection, - code_block: StyleRefinement { - text: Some(TextStyleRefinement { - font_family: Some(theme_settings.buffer_font.family.clone()), - font_size: Some(buffer_font_size.into()), - ..Default::default() - }), - ..Default::default() - }, - inline_code: TextStyleRefinement { - font_family: Some(theme_settings.buffer_font.family.clone()), - font_size: Some(ui_font_size.into()), - background_color: Some(cx.theme().colors().editor_background), - ..Default::default() - }, - ..Default::default() - }; - - let markdown = cx.new_view(|cx| { - Markdown::new( - text, - markdown_style, - Some(self.language_registry.clone()), - None, + self.thread = Some(cx.new_view(|cx| { + ActiveThread::new( + thread.clone(), + self.workspace.clone(), + self.language_registry.clone(), + self.tools.clone(), cx, ) - }); - self.rendered_messages_by_id.insert(*id, markdown); - } - - fn handle_thread_event( - &mut self, - _: Model, - event: &ThreadEvent, - cx: &mut ViewContext, - ) { - match event { - ThreadEvent::ShowError(error) => { - self.last_error = Some(error.clone()); - } - ThreadEvent::StreamedCompletion => {} - ThreadEvent::StreamedAssistantText(message_id, text) => { - if let Some(markdown) = self.rendered_messages_by_id.get_mut(&message_id) { - markdown.update(cx, |markdown, cx| { - markdown.append(text, cx); - }); - } - } - ThreadEvent::MessageAdded(message_id) => { - if let Some(message_text) = self - .thread - .read(cx) - .message(*message_id) - .map(|message| message.text.clone()) - { - self.push_message(message_id, message_text, cx); - } - - cx.notify(); - } - ThreadEvent::UsePendingTools => { - let pending_tool_uses = self - .thread - .read(cx) - .pending_tool_uses() - .into_iter() - .filter(|tool_use| tool_use.status.is_idle()) - .cloned() - .collect::>(); - - for tool_use in pending_tool_uses { - if let Some(tool) = self.tools.tool(&tool_use.name, cx) { - let task = tool.run(tool_use.input, self.workspace.clone(), cx); - - self.thread.update(cx, |thread, cx| { - thread.insert_tool_output( - tool_use.assistant_message_id, - tool_use.id.clone(), - task, - cx, - ); - }); - } - } - } - ThreadEvent::ToolFinished { .. } => {} - } + })); + self.message_editor = cx.new_view(|cx| MessageEditor::new(thread, cx)); + self.message_editor.focus_handle(cx).focus(cx); } } @@ -422,140 +290,105 @@ impl AssistantPanel { ) } - fn render_message_list(&self, cx: &mut ViewContext) -> AnyElement { - if self.thread_messages.is_empty() { - let recent_threads = self - .thread_store - .update(cx, |this, cx| this.recent_threads(3, cx)); + fn render_active_thread_or_empty_state(&self, cx: &mut ViewContext) -> AnyElement { + let Some(thread) = self.thread.as_ref() else { + return self.render_thread_empty_state(cx).into_any_element(); + }; - return v_flex() - .gap_2() - .mx_auto() - .child( - v_flex().w_full().child( - svg() - .path("icons/logo_96.svg") - .text_color(cx.theme().colors().text) - .w(px(40.)) - .h(px(40.)) - .mx_auto() - .mb_4(), - ), - ) - .child(v_flex()) - .child( - h_flex() - .w_full() - .justify_center() - .child(Label::new("Context Examples:").size(LabelSize::Small)), - ) - .child( - h_flex() - .gap_2() - .justify_center() - .child( - h_flex() - .gap_1() - .p_0p5() - .rounded_md() - .border_1() - .border_color(cx.theme().colors().border_variant) - .child( - Icon::new(IconName::Terminal) - .size(IconSize::Small) - .color(Color::Disabled), - ) - .child(Label::new("Terminal").size(LabelSize::Small)), - ) - .child( - h_flex() - .gap_1() - .p_0p5() - .rounded_md() - .border_1() - .border_color(cx.theme().colors().border_variant) - .child( - Icon::new(IconName::Folder) - .size(IconSize::Small) - .color(Color::Disabled), - ) - .child(Label::new("/src/components").size(LabelSize::Small)), - ), - ) - .child( - h_flex() - .w_full() - .justify_center() - .child(Label::new("Recent Threads:").size(LabelSize::Small)), - ) - .child( - v_flex().gap_2().children( - recent_threads - .into_iter() - .map(|thread| self.render_past_thread(thread, cx)), - ), - ) - .child( - h_flex().w_full().justify_center().child( - Button::new("view-all-past-threads", "View All Past Threads") - .style(ButtonStyle::Subtle) - .label_size(LabelSize::Small) - .key_binding(KeyBinding::for_action_in( - &OpenHistory, - &self.focus_handle(cx), - cx, - )) - .on_click(move |_event, cx| { - cx.dispatch_action(OpenHistory.boxed_clone()); - }), - ), - ) - .into_any(); + if thread.read(cx).is_empty() { + return self.render_thread_empty_state(cx).into_any_element(); } - list(self.thread_list_state.clone()).flex_1().into_any() + thread.clone().into_any() } - fn render_message(&self, ix: usize, cx: &mut ViewContext) -> AnyElement { - let message_id = self.thread_messages[ix]; - let Some(message) = self.thread.read(cx).message(message_id) else { - return Empty.into_any(); - }; + fn render_thread_empty_state(&self, cx: &mut ViewContext) -> impl IntoElement { + let recent_threads = self + .thread_store + .update(cx, |this, cx| this.recent_threads(3, cx)); - let Some(markdown) = self.rendered_messages_by_id.get(&message_id) else { - return Empty.into_any(); - }; - - let (role_icon, role_name) = match message.role { - Role::User => (IconName::Person, "You"), - Role::Assistant => (IconName::ZedAssistant, "Assistant"), - Role::System => (IconName::Settings, "System"), - }; - - div() - .id(("message-container", ix)) - .p_2() + v_flex() + .gap_2() + .mx_auto() .child( - v_flex() - .border_1() - .border_color(cx.theme().colors().border_variant) - .rounded_md() + v_flex().w_full().child( + svg() + .path("icons/logo_96.svg") + .text_color(cx.theme().colors().text) + .w(px(40.)) + .h(px(40.)) + .mx_auto() + .mb_4(), + ), + ) + .child(v_flex()) + .child( + h_flex() + .w_full() + .justify_center() + .child(Label::new("Context Examples:").size(LabelSize::Small)), + ) + .child( + h_flex() + .gap_2() + .justify_center() .child( h_flex() - .justify_between() - .p_1p5() - .border_b_1() + .gap_1() + .p_0p5() + .rounded_md() + .border_1() .border_color(cx.theme().colors().border_variant) .child( - h_flex() - .gap_2() - .child(Icon::new(role_icon).size(IconSize::Small)) - .child(Label::new(role_name).size(LabelSize::Small)), - ), + Icon::new(IconName::Terminal) + .size(IconSize::Small) + .color(Color::Disabled), + ) + .child(Label::new("Terminal").size(LabelSize::Small)), ) - .child(v_flex().p_1p5().text_ui(cx).child(markdown.clone())), + .child( + h_flex() + .gap_1() + .p_0p5() + .rounded_md() + .border_1() + .border_color(cx.theme().colors().border_variant) + .child( + Icon::new(IconName::Folder) + .size(IconSize::Small) + .color(Color::Disabled), + ) + .child(Label::new("/src/components").size(LabelSize::Small)), + ), + ) + .child( + h_flex() + .w_full() + .justify_center() + .child(Label::new("Recent Threads:").size(LabelSize::Small)), + ) + .child( + v_flex().gap_2().children( + recent_threads + .into_iter() + .map(|thread| self.render_past_thread(thread, cx)), + ), + ) + .child( + h_flex().w_full().justify_center().child( + Button::new("view-all-past-threads", "View All Past Threads") + .style(ButtonStyle::Subtle) + .label_size(LabelSize::Small) + .key_binding(KeyBinding::for_action_in( + &OpenHistory, + &self.focus_handle(cx), + cx, + )) + .on_click(move |_event, cx| { + cx.dispatch_action(OpenHistory.boxed_clone()); + }), + ), ) - .into_any() } fn render_past_thread( @@ -584,7 +417,7 @@ impl AssistantPanel { } fn render_last_error(&self, cx: &mut ViewContext) -> Option { - let last_error = self.last_error.as_ref()?; + let last_error = self.thread.as_ref()?.read(cx).last_error()?; Some( div() @@ -602,7 +435,7 @@ impl AssistantPanel { self.render_max_monthly_spend_reached_error(cx) } ThreadError::Message(error_message) => { - self.render_error_message(error_message, cx) + self.render_error_message(&error_message, cx) } }) .into_any(), @@ -634,14 +467,24 @@ impl AssistantPanel { .mt_1() .child(Button::new("subscribe", "Subscribe").on_click(cx.listener( |this, _, cx| { - this.last_error = None; + if let Some(thread) = this.thread.as_ref() { + thread.update(cx, |this, _cx| { + this.clear_last_error(); + }); + } + cx.open_url(&zed_urls::account_url(cx)); cx.notify(); }, ))) .child(Button::new("dismiss", "Dismiss").on_click(cx.listener( |this, _, cx| { - this.last_error = None; + if let Some(thread) = this.thread.as_ref() { + thread.update(cx, |this, _cx| { + this.clear_last_error(); + }); + } + cx.notify(); }, ))), @@ -675,7 +518,12 @@ impl AssistantPanel { .child( Button::new("subscribe", "Update Monthly Spend Limit").on_click( cx.listener(|this, _, cx| { - this.last_error = None; + if let Some(thread) = this.thread.as_ref() { + thread.update(cx, |this, _cx| { + this.clear_last_error(); + }); + } + cx.open_url(&zed_urls::account_url(cx)); cx.notify(); }), @@ -683,7 +531,12 @@ impl AssistantPanel { ) .child(Button::new("dismiss", "Dismiss").on_click(cx.listener( |this, _, cx| { - this.last_error = None; + if let Some(thread) = this.thread.as_ref() { + thread.update(cx, |this, _cx| { + this.clear_last_error(); + }); + } + cx.notify(); }, ))), @@ -721,7 +574,12 @@ impl AssistantPanel { .mt_1() .child(Button::new("dismiss", "Dismiss").on_click(cx.listener( |this, _, cx| { - this.last_error = None; + if let Some(thread) = this.thread.as_ref() { + thread.update(cx, |this, _cx| { + this.clear_last_error(); + }); + } + cx.notify(); }, ))), @@ -743,7 +601,7 @@ impl Render for AssistantPanel { println!("Open History"); })) .child(self.render_toolbar(cx)) - .child(self.render_message_list(cx)) + .child(self.render_active_thread_or_empty_state(cx)) .child( h_flex() .border_t_1() diff --git a/crates/assistant2/src/thread.rs b/crates/assistant2/src/thread.rs index fc5e0d6a15..185719fa98 100644 --- a/crates/assistant2/src/thread.rs +++ b/crates/assistant2/src/thread.rs @@ -85,6 +85,10 @@ impl Thread { &self.id } + pub fn is_empty(&self) -> bool { + self.messages.is_empty() + } + pub fn message(&self, id: MessageId) -> Option<&Message> { self.messages.iter().find(|message| message.id == id) } diff --git a/crates/assistant2/src/thread_store.rs b/crates/assistant2/src/thread_store.rs index d784c842c9..80e6d29265 100644 --- a/crates/assistant2/src/thread_store.rs +++ b/crates/assistant2/src/thread_store.rs @@ -52,8 +52,13 @@ impl ThreadStore { }) } - pub fn recent_threads(&self, limit: usize, _cx: &ModelContext) -> Vec> { - self.threads.iter().take(limit).cloned().collect() + pub fn recent_threads(&self, limit: usize, cx: &ModelContext) -> Vec> { + self.threads + .iter() + .filter(|thread| !thread.read(cx).is_empty()) + .take(limit) + .cloned() + .collect() } pub fn create_thread(&mut self, cx: &mut ModelContext) -> Model { From 31796171deb2c11946c5e9fc1c41b3cc791eb3f3 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 4 Dec 2024 18:00:28 -0500 Subject: [PATCH 291/886] assistant2: Sketch in context picker (#21560) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR sketches in a context picker into the message editor in Assistant 2. Not functional yet. Screenshot 2024-12-04 at 5 45 19 PM Release Notes: - N/A --- Cargo.lock | 1 + crates/assistant2/Cargo.toml | 1 + crates/assistant2/src/assistant.rs | 1 + crates/assistant2/src/context_picker.rs | 197 ++++++++++++++++++++++++ crates/assistant2/src/message_editor.rs | 48 +++--- 5 files changed, 227 insertions(+), 21 deletions(-) create mode 100644 crates/assistant2/src/context_picker.rs diff --git a/Cargo.lock b/Cargo.lock index f3c0fa3176..c47c2fd126 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -470,6 +470,7 @@ dependencies = [ "language_models", "log", "markdown", + "picker", "project", "proto", "serde", diff --git a/crates/assistant2/Cargo.toml b/crates/assistant2/Cargo.toml index fb7dcbe520..e5253adbce 100644 --- a/crates/assistant2/Cargo.toml +++ b/crates/assistant2/Cargo.toml @@ -29,6 +29,7 @@ language_model_selector.workspace = true language_models.workspace = true log.workspace = true markdown.workspace = true +picker.workspace = true project.workspace = true proto.workspace = true serde.workspace = true diff --git a/crates/assistant2/src/assistant.rs b/crates/assistant2/src/assistant.rs index dfa361ad8c..13ac2d821b 100644 --- a/crates/assistant2/src/assistant.rs +++ b/crates/assistant2/src/assistant.rs @@ -1,5 +1,6 @@ mod active_thread; mod assistant_panel; +mod context_picker; mod message_editor; mod thread; mod thread_store; diff --git a/crates/assistant2/src/context_picker.rs b/crates/assistant2/src/context_picker.rs new file mode 100644 index 0000000000..679ba8b9e7 --- /dev/null +++ b/crates/assistant2/src/context_picker.rs @@ -0,0 +1,197 @@ +use std::sync::Arc; + +use gpui::{DismissEvent, SharedString, Task, WeakView}; +use picker::{Picker, PickerDelegate, PickerEditorPosition}; +use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger, Tooltip}; + +use crate::message_editor::MessageEditor; + +#[derive(IntoElement)] +pub(super) struct ContextPicker { + message_editor: WeakView, + trigger: T, +} + +#[derive(Clone)] +struct ContextPickerEntry { + name: SharedString, + description: SharedString, + icon: IconName, +} + +pub(crate) struct ContextPickerDelegate { + all_entries: Vec, + filtered_entries: Vec, + message_editor: WeakView, + selected_ix: usize, +} + +impl ContextPicker { + pub(crate) fn new(message_editor: WeakView, trigger: T) -> Self { + ContextPicker { + message_editor, + trigger, + } + } +} + +impl PickerDelegate for ContextPickerDelegate { + type ListItem = ListItem; + + fn match_count(&self) -> usize { + self.filtered_entries.len() + } + + fn selected_index(&self) -> usize { + self.selected_ix + } + + fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext>) { + self.selected_ix = ix.min(self.filtered_entries.len().saturating_sub(1)); + cx.notify(); + } + + fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc { + "Select a context source…".into() + } + + fn update_matches(&mut self, query: String, cx: &mut ViewContext>) -> Task<()> { + let all_commands = self.all_entries.clone(); + cx.spawn(|this, mut cx| async move { + let filtered_commands = cx + .background_executor() + .spawn(async move { + if query.is_empty() { + all_commands + } else { + all_commands + .into_iter() + .filter(|model_info| { + model_info + .name + .to_lowercase() + .contains(&query.to_lowercase()) + }) + .collect() + } + }) + .await; + + this.update(&mut cx, |this, cx| { + this.delegate.filtered_entries = filtered_commands; + this.delegate.set_selected_index(0, cx); + cx.notify(); + }) + .ok(); + }) + } + + fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext>) { + if let Some(entry) = self.filtered_entries.get(self.selected_ix) { + self.message_editor + .update(cx, |_message_editor, _cx| { + println!("Insert context from {}", entry.name); + }) + .ok(); + cx.emit(DismissEvent); + } + } + + fn dismissed(&mut self, _cx: &mut ViewContext>) {} + + fn editor_position(&self) -> PickerEditorPosition { + PickerEditorPosition::End + } + + fn render_match( + &self, + ix: usize, + selected: bool, + _cx: &mut ViewContext>, + ) -> Option { + let entry = self.filtered_entries.get(ix)?; + + Some( + ListItem::new(ix) + .inset(true) + .spacing(ListItemSpacing::Dense) + .selected(selected) + .tooltip({ + let description = entry.description.clone(); + move |cx| cx.new_view(|_cx| Tooltip::new(description.clone())).into() + }) + .child( + v_flex() + .group(format!("context-entry-label-{ix}")) + .w_full() + .py_0p5() + .min_w(px(250.)) + .max_w(px(400.)) + .child( + h_flex() + .gap_1p5() + .child(Icon::new(entry.icon).size(IconSize::XSmall)) + .child( + Label::new(entry.name.clone()) + .single_line() + .size(LabelSize::Small), + ), + ) + .child( + div().overflow_hidden().text_ellipsis().child( + Label::new(entry.description.clone()) + .size(LabelSize::Small) + .color(Color::Muted), + ), + ), + ), + ) + } +} + +impl RenderOnce for ContextPicker { + fn render(self, cx: &mut WindowContext) -> impl IntoElement { + let entries = vec![ + ContextPickerEntry { + name: "directory".into(), + description: "Insert any directory".into(), + icon: IconName::Folder, + }, + ContextPickerEntry { + name: "file".into(), + description: "Insert any file".into(), + icon: IconName::File, + }, + ContextPickerEntry { + name: "web".into(), + description: "Fetch content from URL".into(), + icon: IconName::Globe, + }, + ]; + + let delegate = ContextPickerDelegate { + all_entries: entries.clone(), + message_editor: self.message_editor.clone(), + filtered_entries: entries, + selected_ix: 0, + }; + + let picker = + cx.new_view(|cx| Picker::uniform_list(delegate, cx).max_height(Some(rems(20.).into()))); + + let handle = self + .message_editor + .update(cx, |this, _| this.context_picker_handle.clone()) + .ok(); + PopoverMenu::new("context-picker") + .menu(move |_cx| Some(picker.clone())) + .trigger(self.trigger) + .attach(gpui::AnchorCorner::TopLeft) + .anchor(gpui::AnchorCorner::BottomLeft) + .offset(gpui::Point { + x: px(0.0), + y: px(-16.0), + }) + .when_some(handle, |this, handle| this.with_handle(handle)) + } +} diff --git a/crates/assistant2/src/message_editor.rs b/crates/assistant2/src/message_editor.rs index d1b1cf55e4..f3e618067b 100644 --- a/crates/assistant2/src/message_editor.rs +++ b/crates/assistant2/src/message_editor.rs @@ -1,16 +1,22 @@ use editor::{Editor, EditorElement, EditorStyle}; use gpui::{AppContext, FocusableView, Model, TextStyle, View}; use language_model::{LanguageModelRegistry, LanguageModelRequestTool}; +use picker::Picker; use settings::Settings; use theme::ThemeSettings; -use ui::{prelude::*, ButtonLike, CheckboxWithLabel, ElevationIndex, KeyBinding}; +use ui::{ + prelude::*, ButtonLike, CheckboxWithLabel, ElevationIndex, IconButtonShape, KeyBinding, + PopoverMenuHandle, +}; +use crate::context_picker::{ContextPicker, ContextPickerDelegate}; use crate::thread::{RequestKind, Thread}; use crate::Chat; pub struct MessageEditor { thread: Model, editor: View, + pub(crate) context_picker_handle: PopoverMenuHandle>, use_tools: bool, } @@ -24,6 +30,7 @@ impl MessageEditor { editor }), + context_picker_handle: PopoverMenuHandle::default(), use_tools: false, } } @@ -98,6 +105,14 @@ impl Render for MessageEditor { .gap_2() .p_2() .bg(cx.theme().colors().editor_background) + .child( + h_flex().gap_2().child(ContextPicker::new( + cx.view().downgrade(), + IconButton::new("add-context", IconName::Plus) + .shape(IconButtonShape::Square) + .icon_size(IconSize::Small), + )), + ) .child({ let settings = ThemeSettings::get_global(cx); let text_style = TextStyle { @@ -123,26 +138,17 @@ impl Render for MessageEditor { .child( h_flex() .justify_between() - .child( - h_flex() - .child( - Button::new("add-context", "Add Context") - .style(ButtonStyle::Filled) - .icon(IconName::Plus) - .icon_position(IconPosition::Start), - ) - .child(CheckboxWithLabel::new( - "use-tools", - Label::new("Tools"), - self.use_tools.into(), - cx.listener(|this, selection, _cx| { - this.use_tools = match selection { - Selection::Selected => true, - Selection::Unselected | Selection::Indeterminate => false, - }; - }), - )), - ) + .child(h_flex().gap_2().child(CheckboxWithLabel::new( + "use-tools", + Label::new("Tools"), + self.use_tools.into(), + cx.listener(|this, selection, _cx| { + this.use_tools = match selection { + Selection::Selected => true, + Selection::Unselected | Selection::Indeterminate => false, + }; + }), + ))) .child( h_flex() .gap_2() From a2115e7242551aa4e3f64966ed20a96710fa09f2 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 4 Dec 2024 15:02:33 -0800 Subject: [PATCH 292/886] Restructure git diff state management to allow viewing buffers with different diff bases (#21258) This is a pure refactor of our Git diff state management. Buffers are no longer are associated with one single diff (the unstaged changes). Instead, there is an explicit project API for retrieving a buffer's unstaged changes, and the `Editor` view layer is responsible for choosing what diff to associate with a buffer. The reason for this change is that we'll soon want to add multiple "git diff views" to Zed, one of which will show the *uncommitted* changes for a buffer. But that view will need to co-exist with other views of the same buffer, which may want to show the unstaged changes. ### Todo * [x] Get git gutter and git hunks working with new structure * [x] Update editor tests to use new APIs * [x] Update buffer tests * [x] Restructure remoting/collab protocol * [x] Update assertions about staged text in `random_project_collaboration_tests` * [x] Move buffer tests for git diff management to a new spot, using the new APIs Release Notes: - N/A --------- Co-authored-by: Richard Co-authored-by: Cole Co-authored-by: Conrad --- Cargo.lock | 2 - Cargo.toml | 1 + crates/collab/src/rpc.rs | 1 + crates/collab/src/tests/integration_tests.rs | 127 ++-- .../random_project_collaboration_tests.rs | 22 +- crates/collab/src/tests/test_server.rs | 2 +- crates/editor/src/editor.rs | 242 ++++--- crates/editor/src/editor_tests.rs | 444 +++++------- crates/editor/src/element.rs | 65 +- crates/editor/src/git/project_diff.rs | 327 +++++---- crates/editor/src/hunk_diff.rs | 564 ++++++++------- crates/editor/src/items.rs | 2 +- crates/editor/src/proposed_changes_editor.rs | 74 +- .../src/test/editor_lsp_test_context.rs | 10 +- crates/editor/src/test/editor_test_context.rs | 51 +- crates/fs/src/fs.rs | 13 +- crates/git/Cargo.toml | 1 - crates/git/src/diff.rs | 37 +- crates/language/Cargo.toml | 1 - crates/language/src/buffer.rs | 174 +---- crates/language/src/buffer_tests.rs | 48 -- crates/multi_buffer/src/multi_buffer.rs | 343 ++++----- crates/project/src/buffer_store.rs | 666 +++++++++++++----- crates/project/src/project.rs | 71 +- crates/project/src/project_tests.rs | 93 +++ crates/proto/proto/zed.proto | 21 +- crates/proto/src/proto.rs | 4 + .../remote_server/src/remote_editing_tests.rs | 25 +- crates/worktree/src/worktree.rs | 52 +- 29 files changed, 1832 insertions(+), 1651 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c47c2fd126..820f52a150 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4995,7 +4995,6 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", - "clock", "collections", "derive_more", "git2", @@ -6534,7 +6533,6 @@ dependencies = [ "fs", "futures 0.3.31", "fuzzy", - "git", "globset", "gpui", "http_client", diff --git a/Cargo.toml b/Cargo.toml index ab1e9d8e1a..5bf65b3e14 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -673,6 +673,7 @@ new_ret_no_self = { level = "allow" } # We have a few `next` functions that differ in lifetimes # compared to Iterator::next. Yet, clippy complains about those. should_implement_trait = { level = "allow" } +let_underscore_future = "allow" [workspace.metadata.cargo-machete] ignored = ["bindgen", "cbindgen", "prost_build", "serde"] diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index a17d4924b7..0d9cb2f6c2 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -309,6 +309,7 @@ impl Server { .add_request_handler(forward_read_only_project_request::) .add_request_handler(forward_read_only_project_request::) .add_request_handler(forward_read_only_project_request::) + .add_request_handler(forward_read_only_project_request::) .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) .add_request_handler( diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index b6a0247424..04b9a36fc7 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -2561,19 +2561,23 @@ async fn test_git_diff_base_change( .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) .await .unwrap(); + let change_set_local_a = project_local + .update(cx_a, |p, cx| { + p.open_unstaged_changes(buffer_local_a.clone(), cx) + }) + .await + .unwrap(); // Wait for it to catch up to the new diff executor.run_until_parked(); - - // Smoke test diffing - - buffer_local_a.read_with(cx_a, |buffer, _| { + change_set_local_a.read_with(cx_a, |change_set, cx| { + let buffer = buffer_local_a.read(cx); assert_eq!( - buffer.diff_base().map(|rope| rope.to_string()).as_deref(), + change_set.base_text_string(cx).as_deref(), Some(diff_base.as_str()) ); git::diff::assert_hunks( - buffer.snapshot().git_diff_hunks_in_row_range(0..4), + change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer), buffer, &diff_base, &[(1..2, "", "two\n")], @@ -2585,25 +2589,30 @@ async fn test_git_diff_base_change( .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) .await .unwrap(); + let change_set_remote_a = project_remote + .update(cx_b, |p, cx| { + p.open_unstaged_changes(buffer_remote_a.clone(), cx) + }) + .await + .unwrap(); // Wait remote buffer to catch up to the new diff executor.run_until_parked(); - - // Smoke test diffing - - buffer_remote_a.read_with(cx_b, |buffer, _| { + change_set_remote_a.read_with(cx_b, |change_set, cx| { + let buffer = buffer_remote_a.read(cx); assert_eq!( - buffer.diff_base().map(|rope| rope.to_string()).as_deref(), + change_set.base_text_string(cx).as_deref(), Some(diff_base.as_str()) ); git::diff::assert_hunks( - buffer.snapshot().git_diff_hunks_in_row_range(0..4), + change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer), buffer, &diff_base, &[(1..2, "", "two\n")], ); }); + // Update the staged text of the open buffer client_a.fs().set_index_for_repo( Path::new("/dir/.git"), &[(Path::new("a.txt"), new_diff_base.clone())], @@ -2611,40 +2620,35 @@ async fn test_git_diff_base_change( // Wait for buffer_local_a to receive it executor.run_until_parked(); - - // Smoke test new diffing - - buffer_local_a.read_with(cx_a, |buffer, _| { + change_set_local_a.read_with(cx_a, |change_set, cx| { + let buffer = buffer_local_a.read(cx); assert_eq!( - buffer.diff_base().map(|rope| rope.to_string()).as_deref(), + change_set.base_text_string(cx).as_deref(), Some(new_diff_base.as_str()) ); - git::diff::assert_hunks( - buffer.snapshot().git_diff_hunks_in_row_range(0..4), + change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer), buffer, - &diff_base, + &new_diff_base, &[(2..3, "", "three\n")], ); }); - // Smoke test B - - buffer_remote_a.read_with(cx_b, |buffer, _| { + change_set_remote_a.read_with(cx_b, |change_set, cx| { + let buffer = buffer_remote_a.read(cx); assert_eq!( - buffer.diff_base().map(|rope| rope.to_string()).as_deref(), + change_set.base_text_string(cx).as_deref(), Some(new_diff_base.as_str()) ); git::diff::assert_hunks( - buffer.snapshot().git_diff_hunks_in_row_range(0..4), + change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer), buffer, - &diff_base, + &new_diff_base, &[(2..3, "", "three\n")], ); }); - //Nested git dir - + // Nested git dir let diff_base = " one three @@ -2667,19 +2671,23 @@ async fn test_git_diff_base_change( .update(cx_a, |p, cx| p.open_buffer((worktree_id, "sub/b.txt"), cx)) .await .unwrap(); + let change_set_local_b = project_local + .update(cx_a, |p, cx| { + p.open_unstaged_changes(buffer_local_b.clone(), cx) + }) + .await + .unwrap(); // Wait for it to catch up to the new diff executor.run_until_parked(); - - // Smoke test diffing - - buffer_local_b.read_with(cx_a, |buffer, _| { + change_set_local_b.read_with(cx_a, |change_set, cx| { + let buffer = buffer_local_b.read(cx); assert_eq!( - buffer.diff_base().map(|rope| rope.to_string()).as_deref(), + change_set.base_text_string(cx).as_deref(), Some(diff_base.as_str()) ); git::diff::assert_hunks( - buffer.snapshot().git_diff_hunks_in_row_range(0..4), + change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer), buffer, &diff_base, &[(1..2, "", "two\n")], @@ -2691,25 +2699,29 @@ async fn test_git_diff_base_change( .update(cx_b, |p, cx| p.open_buffer((worktree_id, "sub/b.txt"), cx)) .await .unwrap(); + let change_set_remote_b = project_remote + .update(cx_b, |p, cx| { + p.open_unstaged_changes(buffer_remote_b.clone(), cx) + }) + .await + .unwrap(); - // Wait remote buffer to catch up to the new diff executor.run_until_parked(); - - // Smoke test diffing - - buffer_remote_b.read_with(cx_b, |buffer, _| { + change_set_remote_b.read_with(cx_b, |change_set, cx| { + let buffer = buffer_remote_b.read(cx); assert_eq!( - buffer.diff_base().map(|rope| rope.to_string()).as_deref(), + change_set.base_text_string(cx).as_deref(), Some(diff_base.as_str()) ); git::diff::assert_hunks( - buffer.snapshot().git_diff_hunks_in_row_range(0..4), + change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer), buffer, &diff_base, &[(1..2, "", "two\n")], ); }); + // Update the staged text client_a.fs().set_index_for_repo( Path::new("/dir/sub/.git"), &[(Path::new("b.txt"), new_diff_base.clone())], @@ -2717,43 +2729,30 @@ async fn test_git_diff_base_change( // Wait for buffer_local_b to receive it executor.run_until_parked(); - - // Smoke test new diffing - - buffer_local_b.read_with(cx_a, |buffer, _| { + change_set_local_b.read_with(cx_a, |change_set, cx| { + let buffer = buffer_local_b.read(cx); assert_eq!( - buffer.diff_base().map(|rope| rope.to_string()).as_deref(), + change_set.base_text_string(cx).as_deref(), Some(new_diff_base.as_str()) ); - println!("{:?}", buffer.as_rope().to_string()); - println!("{:?}", buffer.diff_base()); - println!( - "{:?}", - buffer - .snapshot() - .git_diff_hunks_in_row_range(0..4) - .collect::>() - ); - git::diff::assert_hunks( - buffer.snapshot().git_diff_hunks_in_row_range(0..4), + change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer), buffer, - &diff_base, + &new_diff_base, &[(2..3, "", "three\n")], ); }); - // Smoke test B - - buffer_remote_b.read_with(cx_b, |buffer, _| { + change_set_remote_b.read_with(cx_b, |change_set, cx| { + let buffer = buffer_remote_b.read(cx); assert_eq!( - buffer.diff_base().map(|rope| rope.to_string()).as_deref(), + change_set.base_text_string(cx).as_deref(), Some(new_diff_base.as_str()) ); git::diff::assert_hunks( - buffer.snapshot().git_diff_hunks_in_row_range(0..4), + change_set.diff_to_buffer.hunks_in_row_range(0..4, buffer), buffer, - &diff_base, + &new_diff_base, &[(2..3, "", "three\n")], ); }); diff --git a/crates/collab/src/tests/random_project_collaboration_tests.rs b/crates/collab/src/tests/random_project_collaboration_tests.rs index 1f39190d75..351ae0cbe6 100644 --- a/crates/collab/src/tests/random_project_collaboration_tests.rs +++ b/crates/collab/src/tests/random_project_collaboration_tests.rs @@ -1336,10 +1336,24 @@ impl RandomizedTest for ProjectCollaborationTest { (_, None) => panic!("guest's file is None, hosts's isn't"), } - let host_diff_base = host_buffer - .read_with(host_cx, |b, _| b.diff_base().map(ToString::to_string)); - let guest_diff_base = guest_buffer - .read_with(client_cx, |b, _| b.diff_base().map(ToString::to_string)); + let host_diff_base = host_project.read_with(host_cx, |project, cx| { + project + .buffer_store() + .read(cx) + .get_unstaged_changes(host_buffer.read(cx).remote_id()) + .unwrap() + .read(cx) + .base_text_string(cx) + }); + let guest_diff_base = guest_project.read_with(client_cx, |project, cx| { + project + .buffer_store() + .read(cx) + .get_unstaged_changes(guest_buffer.read(cx).remote_id()) + .unwrap() + .read(cx) + .base_text_string(cx) + }); assert_eq!( guest_diff_base, host_diff_base, "guest {} diff base does not match host's for path {path:?} in project {project_id}", diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index c93cce9770..1528da2ff0 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -585,7 +585,7 @@ impl Deref for TestClient { } impl TestClient { - pub fn fs(&self) -> &FakeFs { + pub fn fs(&self) -> Arc { self.app_state.fs.as_fake() } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 8af10cd0c9..c5d09ed1bf 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -83,7 +83,7 @@ use gpui::{ use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HoverState}; pub(crate) use hunk_diff::HoveredHunk; -use hunk_diff::{diff_hunk_to_display, ExpandedHunks}; +use hunk_diff::{diff_hunk_to_display, DiffMap, DiffMapSnapshot}; use indent_guides::ActiveIndentGuidesState; use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy}; pub use inline_completion::Direction; @@ -625,7 +625,7 @@ pub struct Editor { enable_inline_completions: bool, show_inline_completions_override: Option, inlay_hint_cache: InlayHintCache, - expanded_hunks: ExpandedHunks, + diff_map: DiffMap, next_inlay_id: usize, _subscriptions: Vec, pixel_position_of_newest_cursor: Option>, @@ -692,6 +692,7 @@ pub struct EditorSnapshot { git_blame_gutter_max_author_length: Option, pub display_snapshot: DisplaySnapshot, pub placeholder_text: Option>, + diff_map: DiffMapSnapshot, is_focused: bool, scroll_anchor: ScrollAnchor, ongoing_scroll: OngoingScroll, @@ -2002,11 +2003,10 @@ impl Editor { } } - let inlay_hint_settings = inlay_hint_settings( - selections.newest_anchor().head(), - &buffer.read(cx).snapshot(cx), - cx, - ); + let buffer_snapshot = buffer.read(cx).snapshot(cx); + + let inlay_hint_settings = + inlay_hint_settings(selections.newest_anchor().head(), &buffer_snapshot, cx); let focus_handle = cx.focus_handle(); cx.on_focus(&focus_handle, Self::handle_focus).detach(); cx.on_focus_in(&focus_handle, Self::handle_focus_in) @@ -2023,6 +2023,28 @@ impl Editor { let mut code_action_providers = Vec::new(); if let Some(project) = project.clone() { + let mut tasks = Vec::new(); + buffer.update(cx, |multibuffer, cx| { + project.update(cx, |project, cx| { + multibuffer.for_each_buffer(|buffer| { + tasks.push(project.open_unstaged_changes(buffer.clone(), cx)) + }); + }); + }); + + cx.spawn(|this, mut cx| async move { + let change_sets = futures::future::join_all(tasks).await; + this.update(&mut cx, |this, cx| { + for change_set in change_sets { + if let Some(change_set) = change_set.log_err() { + this.diff_map.add_change_set(change_set, cx); + } + } + }) + .ok(); + }) + .detach(); + code_action_providers.push(Arc::new(project) as Arc<_>); } @@ -2105,7 +2127,7 @@ impl Editor { inline_completion_provider: None, active_inline_completion: None, inlay_hint_cache: InlayHintCache::new(inlay_hint_settings), - expanded_hunks: ExpandedHunks::default(), + diff_map: DiffMap::default(), gutter_hovered: false, pixel_position_of_newest_cursor: None, last_bounds: None, @@ -2365,6 +2387,7 @@ impl Editor { scroll_anchor: self.scroll_manager.anchor(), ongoing_scroll: self.scroll_manager.ongoing_scroll(), placeholder_text: self.placeholder_text.clone(), + diff_map: self.diff_map.snapshot(), is_focused: self.focus_handle.is_focused(cx), current_line_highlight: self .current_line_highlight @@ -6503,12 +6526,12 @@ impl Editor { pub fn revert_file(&mut self, _: &RevertFile, cx: &mut ViewContext) { let mut revert_changes = HashMap::default(); - let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx); - for hunk in hunks_for_rows( - Some(MultiBufferRow(0)..multi_buffer_snapshot.max_row()).into_iter(), - &multi_buffer_snapshot, + let snapshot = self.snapshot(cx); + for hunk in hunks_for_ranges( + Some(Point::zero()..snapshot.buffer_snapshot.max_point()).into_iter(), + &snapshot, ) { - Self::prepare_revert_change(&mut revert_changes, self.buffer(), &hunk, cx); + self.prepare_revert_change(&mut revert_changes, &hunk, cx); } if !revert_changes.is_empty() { self.transact(cx, |editor, cx| { @@ -6525,7 +6548,7 @@ impl Editor { } pub fn revert_selected_hunks(&mut self, _: &RevertSelectedHunks, cx: &mut ViewContext) { - let revert_changes = self.gather_revert_changes(&self.selections.disjoint_anchors(), cx); + let revert_changes = self.gather_revert_changes(&self.selections.all(cx), cx); if !revert_changes.is_empty() { self.transact(cx, |editor, cx| { editor.revert(revert_changes, cx); @@ -6533,6 +6556,18 @@ impl Editor { } } + fn revert_hunk(&mut self, hunk: HoveredHunk, cx: &mut ViewContext) { + let snapshot = self.buffer.read(cx).read(cx); + if let Some(hunk) = crate::hunk_diff::to_diff_hunk(&hunk, &snapshot) { + drop(snapshot); + let mut revert_changes = HashMap::default(); + self.prepare_revert_change(&mut revert_changes, &hunk, cx); + if !revert_changes.is_empty() { + self.revert(revert_changes, cx) + } + } + } + pub fn open_active_item_in_terminal(&mut self, _: &OpenInTerminal, cx: &mut ViewContext) { if let Some(working_directory) = self.active_excerpt(cx).and_then(|(_, buffer, _)| { let project_path = buffer.read(cx).project_path(cx)?; @@ -6552,26 +6587,33 @@ impl Editor { fn gather_revert_changes( &mut self, - selections: &[Selection], + selections: &[Selection], cx: &mut ViewContext<'_, Editor>, ) -> HashMap, Rope)>> { let mut revert_changes = HashMap::default(); - let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx); - for hunk in hunks_for_selections(&multi_buffer_snapshot, selections) { - Self::prepare_revert_change(&mut revert_changes, self.buffer(), &hunk, cx); + let snapshot = self.snapshot(cx); + for hunk in hunks_for_selections(&snapshot, selections) { + self.prepare_revert_change(&mut revert_changes, &hunk, cx); } revert_changes } pub fn prepare_revert_change( + &mut self, revert_changes: &mut HashMap, Rope)>>, - multi_buffer: &Model, hunk: &MultiBufferDiffHunk, cx: &AppContext, ) -> Option<()> { - let buffer = multi_buffer.read(cx).buffer(hunk.buffer_id)?; + let buffer = self.buffer.read(cx).buffer(hunk.buffer_id)?; let buffer = buffer.read(cx); - let original_text = buffer.diff_base()?.slice(hunk.diff_base_byte_range.clone()); + let change_set = &self.diff_map.diff_bases.get(&hunk.buffer_id)?.change_set; + let original_text = change_set + .read(cx) + .base_text + .as_ref()? + .read(cx) + .as_rope() + .slice(hunk.diff_base_byte_range.clone()); let buffer_snapshot = buffer.snapshot(); let buffer_revert_changes = revert_changes.entry(buffer.remote_id()).or_default(); if let Err(i) = buffer_revert_changes.binary_search_by(|probe| { @@ -9752,80 +9794,63 @@ impl Editor { } fn go_to_next_hunk(&mut self, _: &GoToHunk, cx: &mut ViewContext) { - let snapshot = self - .display_map - .update(cx, |display_map, cx| display_map.snapshot(cx)); + let snapshot = self.snapshot(cx); let selection = self.selections.newest::(cx); self.go_to_hunk_after_position(&snapshot, selection.head(), cx); } fn go_to_hunk_after_position( &mut self, - snapshot: &DisplaySnapshot, + snapshot: &EditorSnapshot, position: Point, cx: &mut ViewContext<'_, Editor>, ) -> Option { - if let Some(hunk) = self.go_to_next_hunk_in_direction( - snapshot, - position, - false, - snapshot - .buffer_snapshot - .git_diff_hunks_in_range(MultiBufferRow(position.row + 1)..MultiBufferRow::MAX), - cx, - ) { - return Some(hunk); + for (ix, position) in [position, Point::zero()].into_iter().enumerate() { + if let Some(hunk) = self.go_to_next_hunk_in_direction( + snapshot, + position, + ix > 0, + snapshot.diff_map.diff_hunks_in_range( + position + Point::new(1, 0)..snapshot.buffer_snapshot.max_point(), + &snapshot.buffer_snapshot, + ), + cx, + ) { + return Some(hunk); + } } - - let wrapped_point = Point::zero(); - self.go_to_next_hunk_in_direction( - snapshot, - wrapped_point, - true, - snapshot.buffer_snapshot.git_diff_hunks_in_range( - MultiBufferRow(wrapped_point.row + 1)..MultiBufferRow::MAX, - ), - cx, - ) + None } fn go_to_prev_hunk(&mut self, _: &GoToPrevHunk, cx: &mut ViewContext) { - let snapshot = self - .display_map - .update(cx, |display_map, cx| display_map.snapshot(cx)); + let snapshot = self.snapshot(cx); let selection = self.selections.newest::(cx); - self.go_to_hunk_before_position(&snapshot, selection.head(), cx); } fn go_to_hunk_before_position( &mut self, - snapshot: &DisplaySnapshot, + snapshot: &EditorSnapshot, position: Point, cx: &mut ViewContext<'_, Editor>, ) -> Option { - if let Some(hunk) = self.go_to_next_hunk_in_direction( - snapshot, - position, - false, - snapshot - .buffer_snapshot - .git_diff_hunks_in_range_rev(MultiBufferRow(0)..MultiBufferRow(position.row)), - cx, - ) { - return Some(hunk); + for (ix, position) in [position, snapshot.buffer_snapshot.max_point()] + .into_iter() + .enumerate() + { + if let Some(hunk) = self.go_to_next_hunk_in_direction( + snapshot, + position, + ix > 0, + snapshot + .diff_map + .diff_hunks_in_range_rev(Point::zero()..position, &snapshot.buffer_snapshot), + cx, + ) { + return Some(hunk); + } } - - let wrapped_point = snapshot.buffer_snapshot.max_point(); - self.go_to_next_hunk_in_direction( - snapshot, - wrapped_point, - true, - snapshot - .buffer_snapshot - .git_diff_hunks_in_range_rev(MultiBufferRow(0)..MultiBufferRow(wrapped_point.row)), - cx, - ) + None } fn go_to_next_hunk_in_direction( @@ -11270,13 +11295,13 @@ impl Editor { return; } - let mut buffers_affected = HashMap::default(); + let mut buffers_affected = HashSet::default(); let multi_buffer = self.buffer().read(cx); for crease in &creases { if let Some((_, buffer, _)) = multi_buffer.excerpt_containing(crease.range().start.clone(), cx) { - buffers_affected.insert(buffer.read(cx).remote_id(), buffer); + buffers_affected.insert(buffer.read(cx).remote_id()); }; } @@ -11286,8 +11311,8 @@ impl Editor { self.request_autoscroll(Autoscroll::fit(), cx); } - for buffer in buffers_affected.into_values() { - self.sync_expanded_diff_hunks(buffer, cx); + for buffer_id in buffers_affected { + Self::sync_expanded_diff_hunks(&mut self.diff_map, buffer_id, cx); } cx.notify(); @@ -11344,11 +11369,11 @@ impl Editor { return; } - let mut buffers_affected = HashMap::default(); + let mut buffers_affected = HashSet::default(); let multi_buffer = self.buffer().read(cx); for range in ranges { if let Some((_, buffer, _)) = multi_buffer.excerpt_containing(range.start.clone(), cx) { - buffers_affected.insert(buffer.read(cx).remote_id(), buffer); + buffers_affected.insert(buffer.read(cx).remote_id()); }; } @@ -11358,8 +11383,8 @@ impl Editor { self.request_autoscroll(Autoscroll::fit(), cx); } - for buffer in buffers_affected.into_values() { - self.sync_expanded_diff_hunks(buffer, cx); + for buffer_id in buffers_affected { + Self::sync_expanded_diff_hunks(&mut self.diff_map, buffer_id, cx); } cx.notify(); @@ -12653,15 +12678,11 @@ impl Editor { multi_buffer::Event::FileHandleChanged | multi_buffer::Event::Reloaded => { cx.emit(EditorEvent::TitleChanged) } - multi_buffer::Event::DiffBaseChanged => { - self.scrollbar_marker_state.dirty = true; - cx.emit(EditorEvent::DiffBaseChanged); - cx.notify(); - } - multi_buffer::Event::DiffUpdated { buffer } => { - self.sync_expanded_diff_hunks(buffer.clone(), cx); - cx.notify(); - } + // multi_buffer::Event::DiffBaseChanged => { + // self.scrollbar_marker_state.dirty = true; + // cx.emit(EditorEvent::DiffBaseChanged); + // cx.notify(); + // } multi_buffer::Event::Closed => cx.emit(EditorEvent::Closed), multi_buffer::Event::DiagnosticsUpdated => { self.refresh_active_diagnostics(cx); @@ -12829,7 +12850,7 @@ impl Editor { // When editing branch buffers, jump to the corresponding location // in their base buffer. let buffer = buffer_handle.read(cx); - if let Some(base_buffer) = buffer.diff_base_buffer() { + if let Some(base_buffer) = buffer.base_buffer() { range = buffer.range_to_version(range, &base_buffer.read(cx).version()); buffer_handle = base_buffer; } @@ -13606,35 +13627,29 @@ fn test_wrap_with_prefix() { } fn hunks_for_selections( - multi_buffer_snapshot: &MultiBufferSnapshot, - selections: &[Selection], + snapshot: &EditorSnapshot, + selections: &[Selection], ) -> Vec { - let buffer_rows_for_selections = selections.iter().map(|selection| { - let head = selection.head(); - let tail = selection.tail(); - let start = MultiBufferRow(tail.to_point(multi_buffer_snapshot).row); - let end = MultiBufferRow(head.to_point(multi_buffer_snapshot).row); - if start > end { - end..start - } else { - start..end - } - }); - - hunks_for_rows(buffer_rows_for_selections, multi_buffer_snapshot) + hunks_for_ranges( + selections.iter().map(|selection| selection.range()), + snapshot, + ) } -pub fn hunks_for_rows( - rows: impl Iterator>, - multi_buffer_snapshot: &MultiBufferSnapshot, +pub fn hunks_for_ranges( + ranges: impl Iterator>, + snapshot: &EditorSnapshot, ) -> Vec { let mut hunks = Vec::new(); let mut processed_buffer_rows: HashMap>> = HashMap::default(); - for selected_multi_buffer_rows in rows { + for query_range in ranges { let query_rows = - selected_multi_buffer_rows.start..selected_multi_buffer_rows.end.next_row(); - for hunk in multi_buffer_snapshot.git_diff_hunks_in_range(query_rows.clone()) { + MultiBufferRow(query_range.start.row)..MultiBufferRow(query_range.end.row + 1); + for hunk in snapshot.diff_map.diff_hunks_in_range( + Point::new(query_rows.start.0, 0)..Point::new(query_rows.end.0, 0), + &snapshot.buffer_snapshot, + ) { // Deleted hunk is an empty row range, no caret can be placed there and Zed allows to revert it // when the caret is just above or just below the deleted hunk. let allow_adjacent = hunk_status(&hunk) == DiffHunkStatus::Removed; @@ -13643,10 +13658,7 @@ pub fn hunks_for_rows( || hunk.row_range.start == query_rows.end || hunk.row_range.end == query_rows.start } else { - // `selected_multi_buffer_rows` are inclusive (e.g. [2..2] means 2nd row is selected) - // `hunk.row_range` is exclusive (e.g. [2..3] means 2nd row is selected) - hunk.row_range.overlaps(&selected_multi_buffer_rows) - || selected_multi_buffer_rows.end == hunk.row_range.start + hunk.row_range.overlaps(&query_rows) }; if related_to_selection { if !processed_buffer_rows diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 136003dcc3..7f900e2c39 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -25,7 +25,7 @@ use language::{ use language_settings::{Formatter, FormatterList, IndentGuideSettings}; use multi_buffer::MultiBufferIndentGuide; use parking_lot::Mutex; -use project::FakeFs; +use project::{buffer_store::BufferChangeSet, FakeFs}; use project::{ lsp_command::SIGNATURE_HELP_HIGHLIGHT_CURRENT, project_settings::{LspSettings, ProjectSettings}, @@ -3313,7 +3313,7 @@ async fn test_join_lines_with_git_diff_base( .unindent(), ); - cx.set_diff_base(Some(&diff_base)); + cx.set_diff_base(&diff_base); executor.run_until_parked(); // Join lines @@ -3353,16 +3353,15 @@ async fn test_custom_newlines_cause_no_false_positive_diffs( init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx).await; cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3"); - cx.set_diff_base(Some("Line 0\r\nLine 1\r\nLine 2\r\nLine 3")); + cx.set_diff_base("Line 0\r\nLine 1\r\nLine 2\r\nLine 3"); executor.run_until_parked(); cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); assert_eq!( - editor - .buffer() - .read(cx) - .snapshot(cx) - .git_diff_hunks_in_range(MultiBufferRow::MIN..MultiBufferRow::MAX) + snapshot + .diff_map + .diff_hunks_in_range(0..snapshot.buffer_snapshot.len(), &snapshot.buffer_snapshot) .collect::>(), Vec::new(), "Should not have any diffs for files with custom newlines" @@ -10088,7 +10087,7 @@ async fn go_to_hunk(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) .unindent(), ); - cx.set_diff_base(Some(&diff_base)); + cx.set_diff_base(&diff_base); executor.run_until_parked(); cx.update_editor(|editor, cx| { @@ -11125,17 +11124,18 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) { async fn test_addition_reverts(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await; - let base_text = indoc! {r#"struct Row; -struct Row1; -struct Row2; + let base_text = indoc! {r#" + struct Row; + struct Row1; + struct Row2; -struct Row4; -struct Row5; -struct Row6; + struct Row4; + struct Row5; + struct Row6; -struct Row8; -struct Row9; -struct Row10;"#}; + struct Row8; + struct Row9; + struct Row10;"#}; // When addition hunks are not adjacent to carets, no hunk revert is performed assert_hunk_revert( @@ -11266,17 +11266,18 @@ struct Row10;"#}; async fn test_modification_reverts(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await; - let base_text = indoc! {r#"struct Row; -struct Row1; -struct Row2; + let base_text = indoc! {r#" + struct Row; + struct Row1; + struct Row2; -struct Row4; -struct Row5; -struct Row6; + struct Row4; + struct Row5; + struct Row6; -struct Row8; -struct Row9; -struct Row10;"#}; + struct Row8; + struct Row9; + struct Row10;"#}; // Modification hunks behave the same as the addition ones. assert_hunk_revert( @@ -11494,54 +11495,18 @@ struct Row10;"#}; async fn test_multibuffer_reverts(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); - let cols = 4; - let rows = 10; - let sample_text_1 = sample_text(rows, cols, 'a'); - assert_eq!( - sample_text_1, - "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj" - ); - let sample_text_2 = sample_text(rows, cols, 'l'); - assert_eq!( - sample_text_2, - "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu" - ); - let sample_text_3 = sample_text(rows, cols, 'v'); - assert_eq!( - sample_text_3, - "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}" - ); + let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"; + let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"; + let base_text_3 = + "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"; - fn diff_every_buffer_row( - buffer: &Model, - sample_text: String, - cols: usize, - cx: &mut gpui::TestAppContext, - ) { - // revert first character in each row, creating one large diff hunk per buffer - let is_first_char = |offset: usize| offset % cols == 0; - buffer.update(cx, |buffer, cx| { - buffer.set_text( - sample_text - .chars() - .enumerate() - .map(|(offset, c)| if is_first_char(offset) { 'X' } else { c }) - .collect::(), - cx, - ); - buffer.set_diff_base(Some(sample_text), cx); - }); - cx.executor().run_until_parked(); - } + let text_1 = edit_first_char_of_every_line(base_text_1); + let text_2 = edit_first_char_of_every_line(base_text_2); + let text_3 = edit_first_char_of_every_line(base_text_3); - let buffer_1 = cx.new_model(|cx| Buffer::local(sample_text_1.clone(), cx)); - diff_every_buffer_row(&buffer_1, sample_text_1.clone(), cols, cx); - - let buffer_2 = cx.new_model(|cx| Buffer::local(sample_text_2.clone(), cx)); - diff_every_buffer_row(&buffer_2, sample_text_2.clone(), cols, cx); - - let buffer_3 = cx.new_model(|cx| Buffer::local(sample_text_3.clone(), cx)); - diff_every_buffer_row(&buffer_3, sample_text_3.clone(), cols, cx); + let buffer_1 = cx.new_model(|cx| Buffer::local(text_1.clone(), cx)); + let buffer_2 = cx.new_model(|cx| Buffer::local(text_2.clone(), cx)); + let buffer_3 = cx.new_model(|cx| Buffer::local(text_3.clone(), cx)); let multibuffer = cx.new_model(|cx| { let mut multibuffer = MultiBuffer::new(ReadWrite); @@ -11604,57 +11569,85 @@ async fn test_multibuffer_reverts(cx: &mut gpui::TestAppContext) { let (editor, cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx)); editor.update(cx, |editor, cx| { - assert_eq!(editor.text(cx), "XaaaXbbbX\nccXc\ndXdd\n\nhXhh\nXiiiXjjjX\n\nXlllXmmmX\nnnXn\noXoo\n\nsXss\nXtttXuuuX\n\nXvvvXwwwX\nxxXx\nyXyy\n\n}X}}\nX~~~X\u{7f}\u{7f}\u{7f}X\n"); + for (buffer, diff_base) in [ + (buffer_1.clone(), base_text_1), + (buffer_2.clone(), base_text_2), + (buffer_3.clone(), base_text_3), + ] { + let change_set = cx.new_model(|cx| { + BufferChangeSet::new_with_base_text( + diff_base.to_string(), + buffer.read(cx).text_snapshot(), + cx, + ) + }); + editor.diff_map.add_change_set(change_set, cx) + } + }); + cx.executor().run_until_parked(); + + editor.update(cx, |editor, cx| { + assert_eq!(editor.text(cx), "Xaaa\nXbbb\nXccc\n\nXfff\nXggg\n\nXjjj\nXlll\nXmmm\nXnnn\n\nXqqq\nXrrr\n\nXuuu\nXvvv\nXwww\nXxxx\n\nX{{{\nX|||\n\nX\u{7f}\u{7f}\u{7f}"); editor.select_all(&SelectAll, cx); editor.revert_selected_hunks(&RevertSelectedHunks, cx); }); cx.executor().run_until_parked(); + // When all ranges are selected, all buffer hunks are reverted. editor.update(cx, |editor, cx| { assert_eq!(editor.text(cx), "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n\n\nllll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu\n\n\nvvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}\n\n"); }); buffer_1.update(cx, |buffer, _| { - assert_eq!(buffer.text(), sample_text_1); + assert_eq!(buffer.text(), base_text_1); }); buffer_2.update(cx, |buffer, _| { - assert_eq!(buffer.text(), sample_text_2); + assert_eq!(buffer.text(), base_text_2); }); buffer_3.update(cx, |buffer, _| { - assert_eq!(buffer.text(), sample_text_3); + assert_eq!(buffer.text(), base_text_3); + }); + + editor.update(cx, |editor, cx| { + editor.undo(&Default::default(), cx); }); - diff_every_buffer_row(&buffer_1, sample_text_1.clone(), cols, cx); - diff_every_buffer_row(&buffer_2, sample_text_2.clone(), cols, cx); - diff_every_buffer_row(&buffer_3, sample_text_3.clone(), cols, cx); editor.update(cx, |editor, cx| { editor.change_selections(None, cx, |s| { s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0))); }); editor.revert_selected_hunks(&RevertSelectedHunks, cx); }); + // Now, when all ranges selected belong to buffer_1, the revert should succeed, // but not affect buffer_2 and its related excerpts. editor.update(cx, |editor, cx| { assert_eq!( editor.text(cx), - "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n\n\nXlllXmmmX\nnnXn\noXoo\nXpppXqqqX\nrrXr\nsXss\nXtttXuuuX\n\n\nXvvvXwwwX\nxxXx\nyXyy\nXzzzX{{{X\n||X|\n}X}}\nX~~~X\u{7f}\u{7f}\u{7f}X\n\n" + "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n\n\nXlll\nXmmm\nXnnn\n\nXqqq\nXrrr\n\nXuuu\nXvvv\nXwww\nXxxx\n\nX{{{\nX|||\n\nX\u{7f}\u{7f}\u{7f}" ); }); buffer_1.update(cx, |buffer, _| { - assert_eq!(buffer.text(), sample_text_1); + assert_eq!(buffer.text(), base_text_1); }); buffer_2.update(cx, |buffer, _| { assert_eq!( buffer.text(), - "XlllXmmmX\nnnXn\noXoo\nXpppXqqqX\nrrXr\nsXss\nXtttXuuuX" + "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu" ); }); buffer_3.update(cx, |buffer, _| { assert_eq!( buffer.text(), - "XvvvXwwwX\nxxXx\nyXyy\nXzzzX{{{X\n||X|\n}X}}\nX~~~X\u{7f}\u{7f}\u{7f}X" + "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}" ); }); + + fn edit_first_char_of_every_line(text: &str) -> String { + text.split('\n') + .map(|line| format!("X{}", &line[1..])) + .collect::>() + .join("\n") + } } #[gpui::test] @@ -12049,7 +12042,7 @@ async fn test_toggle_hunk_diff(executor: BackgroundExecutor, cx: &mut gpui::Test .unindent(), ); - cx.set_diff_base(Some(&diff_base)); + cx.set_diff_base(&diff_base); executor.run_until_parked(); cx.update_editor(|editor, cx| { @@ -12057,14 +12050,14 @@ async fn test_toggle_hunk_diff(executor: BackgroundExecutor, cx: &mut gpui::Test editor.toggle_hunk_diff(&ToggleHunkDiff, cx); }); executor.run_until_parked(); - cx.assert_diff_hunks( + cx.assert_state_with_diff( r#" use some::modified; fn main() { - println!("hello"); - + println!("hello there"); + + ˇ println!("hello there"); println!("around the"); println!("world"); @@ -12080,28 +12073,13 @@ async fn test_toggle_hunk_diff(executor: BackgroundExecutor, cx: &mut gpui::Test } }); executor.run_until_parked(); - cx.assert_editor_state( - &r#" - use some::modified; - - ˇ - fn main() { - println!("hello there"); - - println!("around the"); - println!("world"); - } - "# - .unindent(), - ); - - cx.assert_diff_hunks( + cx.assert_state_with_diff( r#" - use some::mod; + use some::modified; - const A: u32 = 42; - + ˇ fn main() { - println!("hello"); + println!("hello there"); @@ -12117,11 +12095,11 @@ async fn test_toggle_hunk_diff(executor: BackgroundExecutor, cx: &mut gpui::Test editor.cancel(&Cancel, cx); }); - cx.assert_diff_hunks( + cx.assert_state_with_diff( r#" use some::modified; - + ˇ fn main() { println!("hello there"); @@ -12176,14 +12154,14 @@ async fn test_diff_base_change_with_expanded_diff_hunks( .unindent(), ); - cx.set_diff_base(Some(&diff_base)); + cx.set_diff_base(&diff_base); executor.run_until_parked(); cx.update_editor(|editor, cx| { editor.expand_all_hunk_diffs(&ExpandAllHunkDiffs, cx); }); executor.run_until_parked(); - cx.assert_diff_hunks( + cx.assert_state_with_diff( r#" - use some::mod1; use some::mod2; @@ -12192,7 +12170,7 @@ async fn test_diff_base_change_with_expanded_diff_hunks( - const B: u32 = 42; const C: u32 = 42; - fn main() { + fn main(ˇ) { - println!("hello"); + //println!("hello"); @@ -12204,16 +12182,16 @@ async fn test_diff_base_change_with_expanded_diff_hunks( .unindent(), ); - cx.set_diff_base(Some("new diff base!")); + cx.set_diff_base("new diff base!"); executor.run_until_parked(); - cx.assert_diff_hunks( + cx.assert_state_with_diff( r#" use some::mod2; const A: u32 = 42; const C: u32 = 42; - fn main() { + fn main(ˇ) { //println!("hello"); println!("world"); @@ -12228,7 +12206,7 @@ async fn test_diff_base_change_with_expanded_diff_hunks( editor.expand_all_hunk_diffs(&ExpandAllHunkDiffs, cx); }); executor.run_until_parked(); - cx.assert_diff_hunks( + cx.assert_state_with_diff( r#" - new diff base! + use some::mod2; @@ -12236,7 +12214,7 @@ async fn test_diff_base_change_with_expanded_diff_hunks( + const A: u32 = 42; + const C: u32 = 42; + - + fn main() { + + fn main(ˇ) { + //println!("hello"); + + println!("world"); @@ -12304,7 +12282,7 @@ async fn test_fold_unfold_diff_hunk(executor: BackgroundExecutor, cx: &mut gpui: .unindent(), ); - cx.set_diff_base(Some(&diff_base)); + cx.set_diff_base(&diff_base); executor.run_until_parked(); cx.update_editor(|editor, cx| { @@ -12312,10 +12290,10 @@ async fn test_fold_unfold_diff_hunk(executor: BackgroundExecutor, cx: &mut gpui: }); executor.run_until_parked(); - cx.assert_diff_hunks( + cx.assert_state_with_diff( r#" - use some::mod1; - use some::mod2; + «use some::mod2; const A: u32 = 42; - const B: u32 = 42; @@ -12327,7 +12305,7 @@ async fn test_fold_unfold_diff_hunk(executor: BackgroundExecutor, cx: &mut gpui: println!("world"); + // - + // + + //ˇ» } fn another() { @@ -12347,9 +12325,9 @@ async fn test_fold_unfold_diff_hunk(executor: BackgroundExecutor, cx: &mut gpui: cx.executor().run_until_parked(); // Hunks are not shown if their position is within a fold - cx.assert_diff_hunks( + cx.assert_state_with_diff( r#" - use some::mod2; + «use some::mod2; const A: u32 = 42; const C: u32 = 42; @@ -12359,7 +12337,7 @@ async fn test_fold_unfold_diff_hunk(executor: BackgroundExecutor, cx: &mut gpui: println!("world"); // - // + //ˇ» } fn another() { @@ -12381,10 +12359,10 @@ async fn test_fold_unfold_diff_hunk(executor: BackgroundExecutor, cx: &mut gpui: cx.executor().run_until_parked(); // The deletions reappear when unfolding. - cx.assert_diff_hunks( + cx.assert_state_with_diff( r#" - use some::mod1; - use some::mod2; + «use some::mod2; const A: u32 = 42; - const B: u32 = 42; @@ -12407,7 +12385,7 @@ async fn test_fold_unfold_diff_hunk(executor: BackgroundExecutor, cx: &mut gpui: - fn another2() { println!("another2"); } - "# + ˇ»"# .unindent(), ); } @@ -12423,21 +12401,9 @@ async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut gpui::TestAppContext) let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!"; let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!"; - let buffer_1 = cx.new_model(|cx| { - let mut buffer = Buffer::local(file_1_new.to_string(), cx); - buffer.set_diff_base(Some(file_1_old.into()), cx); - buffer - }); - let buffer_2 = cx.new_model(|cx| { - let mut buffer = Buffer::local(file_2_new.to_string(), cx); - buffer.set_diff_base(Some(file_2_old.into()), cx); - buffer - }); - let buffer_3 = cx.new_model(|cx| { - let mut buffer = Buffer::local(file_3_new.to_string(), cx); - buffer.set_diff_base(Some(file_3_old.into()), cx); - buffer - }); + let buffer_1 = cx.new_model(|cx| Buffer::local(file_1_new.to_string(), cx)); + let buffer_2 = cx.new_model(|cx| Buffer::local(file_2_new.to_string(), cx)); + let buffer_3 = cx.new_model(|cx| Buffer::local(file_3_new.to_string(), cx)); let multi_buffer = cx.new_model(|cx| { let mut multibuffer = MultiBuffer::new(ReadWrite); @@ -12499,6 +12465,25 @@ async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut gpui::TestAppContext) }); let editor = cx.add_window(|cx| Editor::new(EditorMode::Full, multi_buffer, None, true, cx)); + editor + .update(cx, |editor, cx| { + for (buffer, diff_base) in [ + (buffer_1.clone(), file_1_old), + (buffer_2.clone(), file_2_old), + (buffer_3.clone(), file_3_old), + ] { + let change_set = cx.new_model(|cx| { + BufferChangeSet::new_with_base_text( + diff_base.to_string(), + buffer.read(cx).text_snapshot(), + cx, + ) + }); + editor.diff_map.add_change_set(change_set, cx) + } + }) + .unwrap(); + let mut cx = EditorTestContext::for_editor(editor, cx).await; cx.run_until_parked(); @@ -12538,9 +12523,9 @@ async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut gpui::TestAppContext) }); cx.executor().run_until_parked(); - cx.assert_diff_hunks( + cx.assert_state_with_diff( " - aaa + «aaa - bbb ccc ddd @@ -12566,8 +12551,8 @@ async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut gpui::TestAppContext) 777 000 - !!!" - .unindent(), + !!!ˇ»" + .unindent(), ); } @@ -12578,12 +12563,7 @@ async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut gpui::TestAppContext let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n"; let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\n"; - let buffer = cx.new_model(|cx| { - let mut buffer = Buffer::local(text.to_string(), cx); - buffer.set_diff_base(Some(base.into()), cx); - buffer - }); - + let buffer = cx.new_model(|cx| Buffer::local(text.to_string(), cx)); let multi_buffer = cx.new_model(|cx| { let mut multibuffer = MultiBuffer::new(ReadWrite); multibuffer.push_excerpts( @@ -12604,15 +12584,24 @@ async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut gpui::TestAppContext }); let editor = cx.add_window(|cx| Editor::new(EditorMode::Full, multi_buffer, None, true, cx)); + editor + .update(cx, |editor, cx| { + let buffer = buffer.read(cx).text_snapshot(); + let change_set = cx + .new_model(|cx| BufferChangeSet::new_with_base_text(base.to_string(), buffer, cx)); + editor.diff_map.add_change_set(change_set, cx) + }) + .unwrap(); + let mut cx = EditorTestContext::for_editor(editor, cx).await; cx.run_until_parked(); cx.update_editor(|editor, cx| editor.expand_all_hunk_diffs(&Default::default(), cx)); cx.executor().run_until_parked(); - cx.assert_diff_hunks( + cx.assert_state_with_diff( " - aaa + ˇaaa - bbb + BBB @@ -12667,7 +12656,7 @@ async fn test_edits_around_expanded_insertion_hunks( .unindent(), ); - cx.set_diff_base(Some(&diff_base)); + cx.set_diff_base(&diff_base); executor.run_until_parked(); cx.update_editor(|editor, cx| { @@ -12675,7 +12664,7 @@ async fn test_edits_around_expanded_insertion_hunks( }); executor.run_until_parked(); - cx.assert_diff_hunks( + cx.assert_state_with_diff( r#" use some::mod1; use some::mod2; @@ -12683,7 +12672,7 @@ async fn test_edits_around_expanded_insertion_hunks( const A: u32 = 42; + const B: u32 = 42; + const C: u32 = 42; - + + + ˇ fn main() { println!("hello"); @@ -12697,7 +12686,7 @@ async fn test_edits_around_expanded_insertion_hunks( cx.update_editor(|editor, cx| editor.handle_input("const D: u32 = 42;\n", cx)); executor.run_until_parked(); - cx.assert_diff_hunks( + cx.assert_state_with_diff( r#" use some::mod1; use some::mod2; @@ -12706,7 +12695,7 @@ async fn test_edits_around_expanded_insertion_hunks( + const B: u32 = 42; + const C: u32 = 42; + const D: u32 = 42; - + + + ˇ fn main() { println!("hello"); @@ -12720,7 +12709,7 @@ async fn test_edits_around_expanded_insertion_hunks( cx.update_editor(|editor, cx| editor.handle_input("const E: u32 = 42;\n", cx)); executor.run_until_parked(); - cx.assert_diff_hunks( + cx.assert_state_with_diff( r#" use some::mod1; use some::mod2; @@ -12730,7 +12719,7 @@ async fn test_edits_around_expanded_insertion_hunks( + const C: u32 = 42; + const D: u32 = 42; + const E: u32 = 42; - + + + ˇ fn main() { println!("hello"); @@ -12746,7 +12735,7 @@ async fn test_edits_around_expanded_insertion_hunks( }); executor.run_until_parked(); - cx.assert_diff_hunks( + cx.assert_state_with_diff( r#" use some::mod1; use some::mod2; @@ -12756,32 +12745,6 @@ async fn test_edits_around_expanded_insertion_hunks( + const C: u32 = 42; + const D: u32 = 42; + const E: u32 = 42; - - fn main() { - println!("hello"); - - println!("world"); - } - "# - .unindent(), - ); - - cx.update_editor(|editor, cx| { - editor.move_up(&MoveUp, cx); - editor.delete_line(&DeleteLine, cx); - editor.move_up(&MoveUp, cx); - editor.delete_line(&DeleteLine, cx); - editor.move_up(&MoveUp, cx); - editor.delete_line(&DeleteLine, cx); - }); - executor.run_until_parked(); - cx.assert_editor_state( - &r#" - use some::mod1; - use some::mod2; - - const A: u32 = 42; - const B: u32 = 42; ˇ fn main() { println!("hello"); @@ -12792,14 +12755,23 @@ async fn test_edits_around_expanded_insertion_hunks( .unindent(), ); - cx.assert_diff_hunks( + cx.update_editor(|editor, cx| { + editor.move_up(&MoveUp, cx); + editor.delete_line(&DeleteLine, cx); + editor.move_up(&MoveUp, cx); + editor.delete_line(&DeleteLine, cx); + editor.move_up(&MoveUp, cx); + editor.delete_line(&DeleteLine, cx); + }); + executor.run_until_parked(); + cx.assert_state_with_diff( r#" use some::mod1; use some::mod2; const A: u32 = 42; + const B: u32 = 42; - + ˇ fn main() { println!("hello"); @@ -12814,13 +12786,13 @@ async fn test_edits_around_expanded_insertion_hunks( editor.delete_line(&DeleteLine, cx); }); executor.run_until_parked(); - cx.assert_diff_hunks( + cx.assert_state_with_diff( r#" use some::mod1; - use some::mod2; - - const A: u32 = 42; - + ˇ fn main() { println!("hello"); @@ -12875,7 +12847,7 @@ async fn test_edits_around_expanded_deletion_hunks( .unindent(), ); - cx.set_diff_base(Some(&diff_base)); + cx.set_diff_base(&diff_base); executor.run_until_parked(); cx.update_editor(|editor, cx| { @@ -12883,13 +12855,13 @@ async fn test_edits_around_expanded_deletion_hunks( }); executor.run_until_parked(); - cx.assert_diff_hunks( + cx.assert_state_with_diff( r#" use some::mod1; use some::mod2; - const A: u32 = 42; - const B: u32 = 42; + ˇconst B: u32 = 42; const C: u32 = 42; @@ -12906,32 +12878,16 @@ async fn test_edits_around_expanded_deletion_hunks( editor.delete_line(&DeleteLine, cx); }); executor.run_until_parked(); - cx.assert_editor_state( - &r#" + cx.assert_state_with_diff( + r#" use some::mod1; use some::mod2; + - const A: u32 = 42; + - const B: u32 = 42; ˇconst C: u32 = 42; - fn main() { - println!("hello"); - - println!("world"); - } - "# - .unindent(), - ); - cx.assert_diff_hunks( - r#" - use some::mod1; - use some::mod2; - - - const A: u32 = 42; - - const B: u32 = 42; - const C: u32 = 42; - - fn main() { println!("hello"); @@ -12945,22 +12901,7 @@ async fn test_edits_around_expanded_deletion_hunks( editor.delete_line(&DeleteLine, cx); }); executor.run_until_parked(); - cx.assert_editor_state( - &r#" - use some::mod1; - use some::mod2; - - ˇ - - fn main() { - println!("hello"); - - println!("world"); - } - "# - .unindent(), - ); - cx.assert_diff_hunks( + cx.assert_state_with_diff( r#" use some::mod1; use some::mod2; @@ -12968,7 +12909,7 @@ async fn test_edits_around_expanded_deletion_hunks( - const A: u32 = 42; - const B: u32 = 42; - const C: u32 = 42; - + ˇ fn main() { println!("hello"); @@ -12983,22 +12924,7 @@ async fn test_edits_around_expanded_deletion_hunks( editor.handle_input("replacement", cx); }); executor.run_until_parked(); - cx.assert_editor_state( - &r#" - use some::mod1; - use some::mod2; - - replacementˇ - - fn main() { - println!("hello"); - - println!("world"); - } - "# - .unindent(), - ); - cx.assert_diff_hunks( + cx.assert_state_with_diff( r#" use some::mod1; use some::mod2; @@ -13007,7 +12933,7 @@ async fn test_edits_around_expanded_deletion_hunks( - const B: u32 = 42; - const C: u32 = 42; - - + replacement + + replacementˇ fn main() { println!("hello"); @@ -13064,14 +12990,14 @@ async fn test_edit_after_expanded_modification_hunk( .unindent(), ); - cx.set_diff_base(Some(&diff_base)); + cx.set_diff_base(&diff_base); executor.run_until_parked(); cx.update_editor(|editor, cx| { editor.expand_all_hunk_diffs(&ExpandAllHunkDiffs, cx); }); executor.run_until_parked(); - cx.assert_diff_hunks( + cx.assert_state_with_diff( r#" use some::mod1; use some::mod2; @@ -13079,7 +13005,7 @@ async fn test_edit_after_expanded_modification_hunk( const A: u32 = 42; const B: u32 = 42; - const C: u32 = 42; - + const C: u32 = 43 + + const C: u32 = 43ˇ const D: u32 = 42; @@ -13096,7 +13022,7 @@ async fn test_edit_after_expanded_modification_hunk( }); executor.run_until_parked(); - cx.assert_diff_hunks( + cx.assert_state_with_diff( r#" use some::mod1; use some::mod2; @@ -13106,7 +13032,7 @@ async fn test_edit_after_expanded_modification_hunk( - const C: u32 = 42; + const C: u32 = 43 + new_line - + + + ˇ const D: u32 = 42; @@ -14185,22 +14111,14 @@ fn assert_hunk_revert( cx: &mut EditorLspTestContext, ) { cx.set_state(not_reverted_text_with_selections); - cx.update_editor(|editor, cx| { - editor - .buffer() - .read(cx) - .as_singleton() - .unwrap() - .update(cx, |buffer, cx| { - buffer.set_diff_base(Some(base_text.into()), cx); - }); - }); + cx.set_diff_base(base_text); cx.executor().run_until_parked(); let reverted_hunk_statuses = cx.update_editor(|editor, cx| { - let snapshot = editor.buffer().read(cx).snapshot(cx); + let snapshot = editor.snapshot(cx); let reverted_hunk_statuses = snapshot - .git_diff_hunks_in_range(MultiBufferRow::MIN..MultiBufferRow::MAX) + .diff_map + .diff_hunks_in_range(0..snapshot.buffer_snapshot.len(), &snapshot.buffer_snapshot) .map(|hunk| hunk_status(&hunk)) .collect::>(); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 47de2609f7..198ecf6826 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1169,7 +1169,7 @@ impl EditorElement { let editor = self.editor.read(cx); let is_singleton = editor.is_singleton(cx); // Git - (is_singleton && scrollbar_settings.git_diff && snapshot.buffer_snapshot.has_git_diffs()) + (is_singleton && scrollbar_settings.git_diff && !snapshot.diff_map.is_empty()) || // Buffer Search Results (is_singleton && scrollbar_settings.search_results && editor.has_background_highlights::()) @@ -1320,17 +1320,8 @@ impl EditorElement { cx: &mut WindowContext, ) -> Vec<(DisplayDiffHunk, Option)> { let buffer_snapshot = &snapshot.buffer_snapshot; - - let buffer_start_row = MultiBufferRow( - DisplayPoint::new(display_rows.start, 0) - .to_point(snapshot) - .row, - ); - let buffer_end_row = MultiBufferRow( - DisplayPoint::new(display_rows.end, 0) - .to_point(snapshot) - .row, - ); + let buffer_start = DisplayPoint::new(display_rows.start, 0).to_point(snapshot); + let buffer_end = DisplayPoint::new(display_rows.end, 0).to_point(snapshot); let git_gutter_setting = ProjectSettings::get_global(cx) .git @@ -1338,7 +1329,7 @@ impl EditorElement { .unwrap_or_default(); self.editor.update(cx, |editor, cx| { - let expanded_hunks = &editor.expanded_hunks.hunks; + let expanded_hunks = &editor.diff_map.hunks; let expanded_hunks_start_ix = expanded_hunks .binary_search_by(|hunk| { hunk.hunk_range @@ -1349,8 +1340,10 @@ impl EditorElement { .unwrap_err(); let mut expanded_hunks = expanded_hunks[expanded_hunks_start_ix..].iter().peekable(); - let display_hunks = buffer_snapshot - .git_diff_hunks_in_range(buffer_start_row..buffer_end_row) + let mut display_hunks: Vec<(DisplayDiffHunk, Option)> = editor + .diff_map + .snapshot + .diff_hunks_in_range(buffer_start..buffer_end, &buffer_snapshot) .filter_map(|hunk| { let display_hunk = diff_hunk_to_display(&hunk, snapshot); @@ -1393,25 +1386,23 @@ impl EditorElement { Some(display_hunk) }) .dedup() - .map(|hunk| match git_gutter_setting { - GitGutterSetting::TrackedFiles => { - let hitbox = match hunk { - DisplayDiffHunk::Unfolded { .. } => { - let hunk_bounds = Self::diff_hunk_bounds( - snapshot, - line_height, - gutter_hitbox.bounds, - &hunk, - ); - Some(cx.insert_hitbox(hunk_bounds, true)) - } - DisplayDiffHunk::Folded { .. } => None, - }; - (hunk, hitbox) - } - GitGutterSetting::Hide => (hunk, None), - }) + .map(|hunk| (hunk, None)) .collect(); + + if let GitGutterSetting::TrackedFiles = git_gutter_setting { + for (hunk, hitbox) in &mut display_hunks { + if let DisplayDiffHunk::Unfolded { .. } = hunk { + let hunk_bounds = Self::diff_hunk_bounds( + snapshot, + line_height, + gutter_hitbox.bounds, + &hunk, + ); + *hitbox = Some(cx.insert_hitbox(hunk_bounds, true)); + }; + } + } + display_hunks }) } @@ -3755,10 +3746,8 @@ impl EditorElement { let mut marker_quads = Vec::new(); if scrollbar_settings.git_diff { let marker_row_ranges = snapshot - .buffer_snapshot - .git_diff_hunks_in_range( - MultiBufferRow::MIN..MultiBufferRow::MAX, - ) + .diff_map + .diff_hunks(&snapshot.buffer_snapshot) .map(|hunk| { let start_display_row = MultiBufferPoint::new(hunk.row_range.start.0, 0) @@ -5440,7 +5429,7 @@ impl Element for EditorElement { let expanded_add_hunks_by_rows = self.editor.update(cx, |editor, _| { editor - .expanded_hunks + .diff_map .hunks(false) .filter(|hunk| hunk.status == DiffHunkStatus::Added) .map(|expanded_hunk| { diff --git a/crates/editor/src/git/project_diff.rs b/crates/editor/src/git/project_diff.rs index 3e28e28a18..2c60ae4204 100644 --- a/crates/editor/src/git/project_diff.rs +++ b/crates/editor/src/git/project_diff.rs @@ -9,13 +9,15 @@ use std::{ use anyhow::Context as _; use collections::{BTreeMap, HashMap}; use feature_flags::FeatureFlagAppExt; -use futures::{stream::FuturesUnordered, StreamExt}; -use git::{diff::DiffHunk, repository::GitFileStatus}; +use git::{ + diff::{BufferDiff, DiffHunk}, + repository::GitFileStatus, +}; use gpui::{ actions, AnyElement, AnyView, AppContext, EventEmitter, FocusHandle, FocusableView, InteractiveElement, Model, Render, Subscription, Task, View, WeakView, }; -use language::{Buffer, BufferRow, BufferSnapshot}; +use language::{Buffer, BufferRow}; use multi_buffer::{ExcerptId, ExcerptRange, ExpandExcerptDirection, MultiBuffer}; use project::{Project, ProjectEntryId, ProjectPath, WorktreeId}; use text::{OffsetRangeExt, ToPoint}; @@ -215,54 +217,56 @@ impl ProjectDiffEditor { .ok() .flatten() .unwrap_or_default(); - let buffers_with_git_diff = cx - .background_executor() - .spawn(async move { - let mut open_tasks = open_tasks - .into_iter() - .map(|(status, entry_id, entry_path, open_task)| async move { - let (_, opened_model) = open_task.await.with_context(|| { - format!( - "loading buffer {} for git diff", - entry_path.path.display() - ) - })?; - let buffer = match opened_model.downcast::() { - Ok(buffer) => buffer, - Err(_model) => anyhow::bail!( - "Could not load {} as a buffer for git diff", - entry_path.path.display() - ), - }; - anyhow::Ok((status, entry_id, entry_path, buffer)) - }) - .collect::>(); - let mut buffers_with_git_diff = Vec::new(); - while let Some(opened_buffer) = open_tasks.next().await { - if let Some(opened_buffer) = opened_buffer.log_err() { - buffers_with_git_diff.push(opened_buffer); - } - } - buffers_with_git_diff - }) - .await; - - let Some((buffers, mut new_entries)) = cx - .update(|cx| { + let Some((buffers, mut new_entries, change_sets)) = cx + .spawn(|mut cx| async move { + let mut new_entries = Vec::new(); let mut buffers = HashMap::< ProjectEntryId, - (GitFileStatus, Model, BufferSnapshot), + ( + GitFileStatus, + text::BufferSnapshot, + Model, + BufferDiff, + ), >::default(); - let mut new_entries = Vec::new(); - for (status, entry_id, entry_path, buffer) in buffers_with_git_diff { - let buffer_snapshot = buffer.read(cx).snapshot(); - buffers.insert(entry_id, (status, buffer, buffer_snapshot)); + let mut change_sets = Vec::new(); + for (status, entry_id, entry_path, open_task) in open_tasks { + let (_, opened_model) = open_task.await.with_context(|| { + format!("loading buffer {} for git diff", entry_path.path.display()) + })?; + let buffer = match opened_model.downcast::() { + Ok(buffer) => buffer, + Err(_model) => anyhow::bail!( + "Could not load {} as a buffer for git diff", + entry_path.path.display() + ), + }; + let change_set = project + .update(&mut cx, |project, cx| { + project.open_unstaged_changes(buffer.clone(), cx) + })? + .await?; + + cx.update(|cx| { + buffers.insert( + entry_id, + ( + status, + buffer.read(cx).text_snapshot(), + buffer, + change_set.read(cx).diff_to_buffer.clone(), + ), + ); + })?; + change_sets.push(change_set); new_entries.push((entry_path, entry_id)); } - (buffers, new_entries) + + Ok((buffers, new_entries, change_sets)) }) - .ok() + .await + .log_err() else { return; }; @@ -271,14 +275,14 @@ impl ProjectDiffEditor { .background_executor() .spawn(async move { let mut new_changes = HashMap::::default(); - for (entry_id, (status, buffer, buffer_snapshot)) in buffers { + for (entry_id, (status, buffer_snapshot, buffer, buffer_diff)) in buffers { new_changes.insert( entry_id, Changes { _status: status, buffer, - hunks: buffer_snapshot - .git_diff_hunks_in_row_range(0..BufferRow::MAX) + hunks: buffer_diff + .hunks_in_row_range(0..BufferRow::MAX, &buffer_snapshot) .collect::>(), }, ); @@ -294,33 +298,16 @@ impl ProjectDiffEditor { }) .await; - let mut diff_recalculations = FuturesUnordered::new(); project_diff_editor .update(&mut cx, |project_diff_editor, cx| { project_diff_editor.update_excerpts(id, new_changes, new_entry_order, cx); - for buffer in project_diff_editor - .editor - .read(cx) - .buffer() - .read(cx) - .all_buffers() - { - buffer.update(cx, |buffer, cx| { - if let Some(diff_recalculation) = buffer.recalculate_diff(cx) { - diff_recalculations.push(diff_recalculation); - } + for change_set in change_sets { + project_diff_editor.editor.update(cx, |editor, cx| { + editor.diff_map.add_change_set(change_set, cx) }); } }) .ok(); - - cx.background_executor() - .spawn(async move { - while let Some(()) = diff_recalculations.next().await { - // another diff is calculated - } - }) - .await; }), ); } @@ -1100,13 +1087,13 @@ impl Render for ProjectDiffEditor { #[cfg(test)] mod tests { - use std::{ops::Deref as _, path::Path, sync::Arc}; + // use std::{ops::Deref as _, path::Path, sync::Arc}; - use fs::RealFs; - use gpui::{SemanticVersion, TestAppContext, VisualTestContext}; - use settings::SettingsStore; + // use fs::RealFs; + // use gpui::{SemanticVersion, TestAppContext, VisualTestContext}; + // use settings::SettingsStore; - use super::*; + // use super::*; // TODO finish // #[gpui::test] @@ -1122,114 +1109,114 @@ mod tests { // // Apply randomized changes to the project: select a random file, random change and apply to buffers // } - #[gpui::test] - async fn simple_edit_test(cx: &mut TestAppContext) { - cx.executor().allow_parking(); - init_test(cx); + // #[gpui::test] + // async fn simple_edit_test(cx: &mut TestAppContext) { + // cx.executor().allow_parking(); + // init_test(cx); - let dir = tempfile::tempdir().unwrap(); - let dst = dir.path(); + // let dir = tempfile::tempdir().unwrap(); + // let dst = dir.path(); - std::fs::write(dst.join("file_a"), "This is file_a").unwrap(); - std::fs::write(dst.join("file_b"), "This is file_b").unwrap(); + // std::fs::write(dst.join("file_a"), "This is file_a").unwrap(); + // std::fs::write(dst.join("file_b"), "This is file_b").unwrap(); - run_git(dst, &["init"]); - run_git(dst, &["add", "*"]); - run_git(dst, &["commit", "-m", "Initial commit"]); + // run_git(dst, &["init"]); + // run_git(dst, &["add", "*"]); + // run_git(dst, &["commit", "-m", "Initial commit"]); - let project = Project::test(Arc::new(RealFs::default()), [dst], cx).await; - let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); - let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx); + // let project = Project::test(Arc::new(RealFs::default()), [dst], cx).await; + // let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + // let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx); - let file_a_editor = workspace - .update(cx, |workspace, cx| { - let file_a_editor = workspace.open_abs_path(dst.join("file_a"), true, cx); - ProjectDiffEditor::deploy(workspace, &Deploy, cx); - file_a_editor - }) - .unwrap() - .await - .expect("did not open an item at all") - .downcast::() - .expect("did not open an editor for file_a"); + // let file_a_editor = workspace + // .update(cx, |workspace, cx| { + // let file_a_editor = workspace.open_abs_path(dst.join("file_a"), true, cx); + // ProjectDiffEditor::deploy(workspace, &Deploy, cx); + // file_a_editor + // }) + // .unwrap() + // .await + // .expect("did not open an item at all") + // .downcast::() + // .expect("did not open an editor for file_a"); - let project_diff_editor = workspace - .update(cx, |workspace, cx| { - workspace - .active_pane() - .read(cx) - .items() - .find_map(|item| item.downcast::()) - }) - .unwrap() - .expect("did not find a ProjectDiffEditor"); - project_diff_editor.update(cx, |project_diff_editor, cx| { - assert!( - project_diff_editor.editor.read(cx).text(cx).is_empty(), - "Should have no changes after opening the diff on no git changes" - ); - }); + // let project_diff_editor = workspace + // .update(cx, |workspace, cx| { + // workspace + // .active_pane() + // .read(cx) + // .items() + // .find_map(|item| item.downcast::()) + // }) + // .unwrap() + // .expect("did not find a ProjectDiffEditor"); + // project_diff_editor.update(cx, |project_diff_editor, cx| { + // assert!( + // project_diff_editor.editor.read(cx).text(cx).is_empty(), + // "Should have no changes after opening the diff on no git changes" + // ); + // }); - let old_text = file_a_editor.update(cx, |editor, cx| editor.text(cx)); - let change = "an edit after git add"; - file_a_editor - .update(cx, |file_a_editor, cx| { - file_a_editor.insert(change, cx); - file_a_editor.save(false, project.clone(), cx) - }) - .await - .expect("failed to save a file"); - cx.executor().advance_clock(Duration::from_secs(1)); - cx.run_until_parked(); + // let old_text = file_a_editor.update(cx, |editor, cx| editor.text(cx)); + // let change = "an edit after git add"; + // file_a_editor + // .update(cx, |file_a_editor, cx| { + // file_a_editor.insert(change, cx); + // file_a_editor.save(false, project.clone(), cx) + // }) + // .await + // .expect("failed to save a file"); + // cx.executor().advance_clock(Duration::from_secs(1)); + // cx.run_until_parked(); - // TODO does not work on Linux for some reason, returning a blank line - // hence disable the last check for now, and do some fiddling to avoid the warnings. - #[cfg(target_os = "linux")] - { - if true { - return; - } - } - project_diff_editor.update(cx, |project_diff_editor, cx| { - // TODO assert it better: extract added text (based on the background changes) and deleted text (based on the deleted blocks added) - assert_eq!( - project_diff_editor.editor.read(cx).text(cx), - format!("{change}{old_text}"), - "Should have a new change shown in the beginning, and the old text shown as deleted text afterwards" - ); - }); - } + // // TODO does not work on Linux for some reason, returning a blank line + // // hence disable the last check for now, and do some fiddling to avoid the warnings. + // #[cfg(target_os = "linux")] + // { + // if true { + // return; + // } + // } + // project_diff_editor.update(cx, |project_diff_editor, cx| { + // // TODO assert it better: extract added text (based on the background changes) and deleted text (based on the deleted blocks added) + // assert_eq!( + // project_diff_editor.editor.read(cx).text(cx), + // format!("{change}{old_text}"), + // "Should have a new change shown in the beginning, and the old text shown as deleted text afterwards" + // ); + // }); + // } - fn run_git(path: &Path, args: &[&str]) -> String { - let output = std::process::Command::new("git") - .args(args) - .current_dir(path) - .output() - .expect("git commit failed"); + // fn run_git(path: &Path, args: &[&str]) -> String { + // let output = std::process::Command::new("git") + // .args(args) + // .current_dir(path) + // .output() + // .expect("git commit failed"); - format!( - "Stdout: {}; stderr: {}", - String::from_utf8(output.stdout).unwrap(), - String::from_utf8(output.stderr).unwrap() - ) - } + // format!( + // "Stdout: {}; stderr: {}", + // String::from_utf8(output.stdout).unwrap(), + // String::from_utf8(output.stderr).unwrap() + // ) + // } - fn init_test(cx: &mut gpui::TestAppContext) { - if std::env::var("RUST_LOG").is_ok() { - env_logger::try_init().ok(); - } + // fn init_test(cx: &mut gpui::TestAppContext) { + // if std::env::var("RUST_LOG").is_ok() { + // env_logger::try_init().ok(); + // } - cx.update(|cx| { - assets::Assets.load_test_fonts(cx); - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - theme::init(theme::LoadThemes::JustBase, cx); - release_channel::init(SemanticVersion::default(), cx); - client::init_settings(cx); - language::init(cx); - Project::init_settings(cx); - workspace::init_settings(cx); - crate::init(cx); - }); - } + // cx.update(|cx| { + // assets::Assets.load_test_fonts(cx); + // let settings_store = SettingsStore::test(cx); + // cx.set_global(settings_store); + // theme::init(theme::LoadThemes::JustBase, cx); + // release_channel::init(SemanticVersion::default(), cx); + // client::init_settings(cx); + // language::init(cx); + // Project::init_settings(cx); + // workspace::init_settings(cx); + // crate::init(cx); + // }); + // } } diff --git a/crates/editor/src/hunk_diff.rs b/crates/editor/src/hunk_diff.rs index 3da005cd2c..3f798eaa58 100644 --- a/crates/editor/src/hunk_diff.rs +++ b/crates/editor/src/hunk_diff.rs @@ -1,12 +1,17 @@ -use collections::{hash_map, HashMap, HashSet}; +use collections::{HashMap, HashSet}; use git::diff::DiffHunkStatus; -use gpui::{Action, AnchorCorner, AppContext, CursorStyle, Hsla, Model, MouseButton, Task, View}; +use gpui::{ + Action, AnchorCorner, AppContext, CursorStyle, Hsla, Model, MouseButton, Subscription, Task, + View, +}; use language::{Buffer, BufferId, Point}; use multi_buffer::{ Anchor, AnchorRangeExt, ExcerptRange, MultiBuffer, MultiBufferDiffHunk, MultiBufferRow, - MultiBufferSnapshot, ToPoint, + MultiBufferSnapshot, ToOffset, ToPoint, }; +use project::buffer_store::BufferChangeSet; use std::{ops::Range, sync::Arc}; +use sum_tree::TreeMap; use text::OffsetRangeExt; use ui::{ prelude::*, ActiveTheme, ContextMenu, IconButtonShape, InteractiveElement, IntoElement, @@ -29,10 +34,11 @@ pub(super) struct HoveredHunk { pub diff_base_byte_range: Range, } -#[derive(Debug, Default)] -pub(super) struct ExpandedHunks { +#[derive(Default)] +pub(super) struct DiffMap { pub(crate) hunks: Vec, - diff_base: HashMap, + pub(crate) diff_bases: HashMap, + pub(crate) snapshot: DiffMapSnapshot, hunk_update_tasks: HashMap, Task<()>>, expand_all: bool, } @@ -46,10 +52,13 @@ pub(super) struct ExpandedHunk { pub folded: bool, } -#[derive(Debug)] -struct DiffBaseBuffer { - buffer: Model, - diff_base_version: usize, +#[derive(Clone, Debug, Default)] +pub(crate) struct DiffMapSnapshot(TreeMap); + +pub(crate) struct DiffBaseState { + pub(crate) change_set: Model, + pub(crate) last_version: Option, + _subscription: Subscription, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -66,7 +75,38 @@ pub enum DisplayDiffHunk { }, } -impl ExpandedHunks { +impl DiffMap { + pub fn snapshot(&self) -> DiffMapSnapshot { + self.snapshot.clone() + } + + pub fn add_change_set( + &mut self, + change_set: Model, + cx: &mut ViewContext, + ) { + let buffer_id = change_set.read(cx).buffer_id; + self.snapshot + .0 + .insert(buffer_id, change_set.read(cx).diff_to_buffer.clone()); + Editor::sync_expanded_diff_hunks(self, buffer_id, cx); + self.diff_bases.insert( + buffer_id, + DiffBaseState { + last_version: None, + _subscription: cx.observe(&change_set, move |editor, change_set, cx| { + editor + .diff_map + .snapshot + .0 + .insert(buffer_id, change_set.read(cx).diff_to_buffer.clone()); + Editor::sync_expanded_diff_hunks(&mut editor.diff_map, buffer_id, cx); + }), + change_set, + }, + ); + } + pub fn hunks(&self, include_folded: bool) -> impl Iterator { self.hunks .iter() @@ -74,9 +114,92 @@ impl ExpandedHunks { } } +impl DiffMapSnapshot { + pub fn is_empty(&self) -> bool { + self.0.values().all(|diff| diff.is_empty()) + } + + pub fn diff_hunks<'a>( + &'a self, + buffer_snapshot: &'a MultiBufferSnapshot, + ) -> impl Iterator + 'a { + self.diff_hunks_in_range(0..buffer_snapshot.len(), buffer_snapshot) + } + + pub fn diff_hunks_in_range<'a, T: ToOffset>( + &'a self, + range: Range, + buffer_snapshot: &'a MultiBufferSnapshot, + ) -> impl Iterator + 'a { + let range = range.start.to_offset(buffer_snapshot)..range.end.to_offset(buffer_snapshot); + buffer_snapshot + .excerpts_for_range(range.clone()) + .filter_map(move |excerpt| { + let buffer = excerpt.buffer(); + let buffer_id = buffer.remote_id(); + let diff = self.0.get(&buffer_id)?; + let buffer_range = excerpt.map_range_to_buffer(range.clone()); + let buffer_range = + buffer.anchor_before(buffer_range.start)..buffer.anchor_after(buffer_range.end); + Some( + diff.hunks_intersecting_range(buffer_range, excerpt.buffer()) + .map(move |hunk| { + let start = + excerpt.map_point_from_buffer(Point::new(hunk.row_range.start, 0)); + let end = + excerpt.map_point_from_buffer(Point::new(hunk.row_range.end, 0)); + MultiBufferDiffHunk { + row_range: MultiBufferRow(start.row)..MultiBufferRow(end.row), + buffer_id, + buffer_range: hunk.buffer_range.clone(), + diff_base_byte_range: hunk.diff_base_byte_range.clone(), + } + }), + ) + }) + .flatten() + } + + pub fn diff_hunks_in_range_rev<'a, T: ToOffset>( + &'a self, + range: Range, + buffer_snapshot: &'a MultiBufferSnapshot, + ) -> impl Iterator + 'a { + let range = range.start.to_offset(buffer_snapshot)..range.end.to_offset(buffer_snapshot); + buffer_snapshot + .excerpts_for_range_rev(range.clone()) + .filter_map(move |excerpt| { + let buffer = excerpt.buffer(); + let buffer_id = buffer.remote_id(); + let diff = self.0.get(&buffer_id)?; + let buffer_range = excerpt.map_range_to_buffer(range.clone()); + let buffer_range = + buffer.anchor_before(buffer_range.start)..buffer.anchor_after(buffer_range.end); + Some( + diff.hunks_intersecting_range_rev(buffer_range, excerpt.buffer()) + .map(move |hunk| { + let start_row = excerpt + .map_point_from_buffer(Point::new(hunk.row_range.start, 0)) + .row; + let end_row = excerpt + .map_point_from_buffer(Point::new(hunk.row_range.end, 0)) + .row; + MultiBufferDiffHunk { + row_range: MultiBufferRow(start_row)..MultiBufferRow(end_row), + buffer_id, + buffer_range: hunk.buffer_range.clone(), + diff_base_byte_range: hunk.diff_base_byte_range.clone(), + } + }), + ) + }) + .flatten() + } +} + impl Editor { pub fn set_expand_all_diff_hunks(&mut self) { - self.expanded_hunks.expand_all = true; + self.diff_map.expand_all = true; } pub(super) fn toggle_hovered_hunk( @@ -92,18 +215,15 @@ impl Editor { } pub fn toggle_hunk_diff(&mut self, _: &ToggleHunkDiff, cx: &mut ViewContext) { - let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx); - let selections = self.selections.disjoint_anchors(); - self.toggle_hunks_expanded( - hunks_for_selections(&multi_buffer_snapshot, &selections), - cx, - ); + let snapshot = self.snapshot(cx); + let selections = self.selections.all(cx); + self.toggle_hunks_expanded(hunks_for_selections(&snapshot, &selections), cx); } pub fn expand_all_hunk_diffs(&mut self, _: &ExpandAllHunkDiffs, cx: &mut ViewContext) { let snapshot = self.snapshot(cx); let display_rows_with_expanded_hunks = self - .expanded_hunks + .diff_map .hunks(false) .map(|hunk| &hunk.hunk_range) .map(|anchor_range| { @@ -119,10 +239,10 @@ impl Editor { ) }) .collect::>(); - let hunks = snapshot - .display_snapshot - .buffer_snapshot - .git_diff_hunks_in_range(MultiBufferRow::MIN..MultiBufferRow::MAX) + let hunks = self + .diff_map + .snapshot + .diff_hunks(&snapshot.display_snapshot.buffer_snapshot) .filter(|hunk| { let hunk_display_row_range = Point::new(hunk.row_range.start.0, 0) .to_display_point(&snapshot.display_snapshot) @@ -140,11 +260,11 @@ impl Editor { hunks_to_toggle: Vec, cx: &mut ViewContext, ) { - if self.expanded_hunks.expand_all { + if self.diff_map.expand_all { return; } - let previous_toggle_task = self.expanded_hunks.hunk_update_tasks.remove(&None); + let previous_toggle_task = self.diff_map.hunk_update_tasks.remove(&None); let new_toggle_task = cx.spawn(move |editor, mut cx| async move { if let Some(task) = previous_toggle_task { task.await; @@ -154,11 +274,10 @@ impl Editor { .update(&mut cx, |editor, cx| { let snapshot = editor.snapshot(cx); let mut hunks_to_toggle = hunks_to_toggle.into_iter().fuse().peekable(); - let mut highlights_to_remove = - Vec::with_capacity(editor.expanded_hunks.hunks.len()); + let mut highlights_to_remove = Vec::with_capacity(editor.diff_map.hunks.len()); let mut blocks_to_remove = HashSet::default(); let mut hunks_to_expand = Vec::new(); - editor.expanded_hunks.hunks.retain(|expanded_hunk| { + editor.diff_map.hunks.retain(|expanded_hunk| { if expanded_hunk.folded { return true; } @@ -238,7 +357,7 @@ impl Editor { .ok(); }); - self.expanded_hunks + self.diff_map .hunk_update_tasks .insert(None, cx.background_executor().spawn(new_toggle_task)); } @@ -252,30 +371,34 @@ impl Editor { let buffer = self.buffer.clone(); let multi_buffer_snapshot = buffer.read(cx).snapshot(cx); let hunk_range = hunk.multi_buffer_range.clone(); - let (diff_base_buffer, deleted_text_lines) = buffer.update(cx, |buffer, cx| { - let buffer = buffer.buffer(hunk_range.start.buffer_id?)?; - let diff_base_buffer = diff_base_buffer - .or_else(|| self.current_diff_base_buffer(&buffer, cx)) - .or_else(|| create_diff_base_buffer(&buffer, cx))?; - let deleted_text_lines = buffer.read(cx).diff_base().map(|diff_base| { - let diff_start_row = diff_base - .offset_to_point(hunk.diff_base_byte_range.start) - .row; - let diff_end_row = diff_base.offset_to_point(hunk.diff_base_byte_range.end).row; - diff_end_row - diff_start_row - })?; - Some((diff_base_buffer, deleted_text_lines)) + let buffer_id = hunk_range.start.buffer_id?; + let diff_base_buffer = diff_base_buffer.or_else(|| { + self.diff_map + .diff_bases + .get(&buffer_id)? + .change_set + .read(cx) + .base_text + .clone() })?; - let block_insert_index = match self.expanded_hunks.hunks.binary_search_by(|probe| { - probe - .hunk_range - .start - .cmp(&hunk_range.start, &multi_buffer_snapshot) - }) { - Ok(_already_present) => return None, - Err(ix) => ix, - }; + let diff_base = diff_base_buffer.read(cx); + let diff_start_row = diff_base + .offset_to_point(hunk.diff_base_byte_range.start) + .row; + let diff_end_row = diff_base.offset_to_point(hunk.diff_base_byte_range.end).row; + let deleted_text_lines = diff_end_row - diff_start_row; + + let block_insert_index = self + .diff_map + .hunks + .binary_search_by(|probe| { + probe + .hunk_range + .start + .cmp(&hunk_range.start, &multi_buffer_snapshot) + }) + .err()?; let blocks; match hunk.status { @@ -315,7 +438,7 @@ impl Editor { ); } }; - self.expanded_hunks.hunks.insert( + self.diff_map.hunks.insert( block_insert_index, ExpandedHunk { blocks, @@ -374,8 +497,8 @@ impl Editor { _: &ApplyDiffHunk, cx: &mut ViewContext, ) { - let snapshot = self.buffer.read(cx).snapshot(cx); - let hunks = hunks_for_selections(&snapshot, &self.selections.disjoint_anchors()); + let snapshot = self.snapshot(cx); + let hunks = hunks_for_selections(&snapshot, &self.selections.all(cx)); let mut ranges_by_buffer = HashMap::default(); self.transact(cx, |editor, cx| { for hunk in hunks { @@ -401,7 +524,7 @@ impl Editor { fn has_multiple_hunks(&self, cx: &AppContext) -> bool { let snapshot = self.buffer.read(cx).snapshot(cx); - let mut hunks = snapshot.git_diff_hunks_in_range(MultiBufferRow::MIN..MultiBufferRow::MAX); + let mut hunks = self.diff_map.snapshot.diff_hunks(&snapshot); hunks.nth(1).is_some() } @@ -415,7 +538,7 @@ impl Editor { .read(cx) .point_to_buffer_offset(hunk.multi_buffer_range.start, cx) .map_or(false, |(buffer, _, _)| { - buffer.read(cx).diff_base_buffer().is_some() + buffer.read(cx).base_buffer().is_some() }); let border_color = cx.theme().colors().border_variant; @@ -552,29 +675,9 @@ impl Editor { let editor = editor.clone(); let hunk = hunk.clone(); move |_event, cx| { - let multi_buffer = - editor.read(cx).buffer().clone(); - let multi_buffer_snapshot = - multi_buffer.read(cx).snapshot(cx); - let mut revert_changes = HashMap::default(); - if let Some(hunk) = - crate::hunk_diff::to_diff_hunk( - &hunk, - &multi_buffer_snapshot, - ) - { - Editor::prepare_revert_change( - &mut revert_changes, - &multi_buffer, - &hunk, - cx, - ); - } - if !revert_changes.is_empty() { - editor.update(cx, |editor, cx| { - editor.revert(revert_changes, cx) - }); - } + editor.update(cx, |editor, cx| { + editor.revert_hunk(hunk.clone(), cx); + }); } }), ) @@ -763,13 +866,13 @@ impl Editor { } pub(super) fn clear_expanded_diff_hunks(&mut self, cx: &mut ViewContext<'_, Editor>) -> bool { - if self.expanded_hunks.expand_all { + if self.diff_map.expand_all { return false; } - self.expanded_hunks.hunk_update_tasks.clear(); + self.diff_map.hunk_update_tasks.clear(); self.clear_row_highlights::(); let to_remove = self - .expanded_hunks + .diff_map .hunks .drain(..) .flat_map(|expanded_hunk| expanded_hunk.blocks.into_iter()) @@ -783,48 +886,39 @@ impl Editor { } pub(super) fn sync_expanded_diff_hunks( - &mut self, - buffer: Model, + diff_map: &mut DiffMap, + buffer_id: BufferId, cx: &mut ViewContext<'_, Self>, ) { - let buffer_id = buffer.read(cx).remote_id(); - let buffer_diff_base_version = buffer.read(cx).diff_base_version(); - self.expanded_hunks - .hunk_update_tasks - .remove(&Some(buffer_id)); - let diff_base_buffer = self.current_diff_base_buffer(&buffer, cx); + let diff_base_state = diff_map.diff_bases.get_mut(&buffer_id); + let mut diff_base_buffer = None; + let mut diff_base_buffer_unchanged = true; + if let Some(diff_base_state) = diff_base_state { + diff_base_state.change_set.update(cx, |change_set, _| { + if diff_base_state.last_version != Some(change_set.base_text_version) { + diff_base_state.last_version = Some(change_set.base_text_version); + diff_base_buffer_unchanged = false; + } + diff_base_buffer = change_set.base_text.clone(); + }) + } + + diff_map.hunk_update_tasks.remove(&Some(buffer_id)); + let new_sync_task = cx.spawn(move |editor, mut cx| async move { - let diff_base_buffer_unchanged = diff_base_buffer.is_some(); - let Ok(diff_base_buffer) = - cx.update(|cx| diff_base_buffer.or_else(|| create_diff_base_buffer(&buffer, cx))) - else { - return; - }; editor .update(&mut cx, |editor, cx| { - if let Some(diff_base_buffer) = &diff_base_buffer { - editor.expanded_hunks.diff_base.insert( - buffer_id, - DiffBaseBuffer { - buffer: diff_base_buffer.clone(), - diff_base_version: buffer_diff_base_version, - }, - ); - } - let snapshot = editor.snapshot(cx); let mut recalculated_hunks = snapshot - .buffer_snapshot - .git_diff_hunks_in_range(MultiBufferRow::MIN..MultiBufferRow::MAX) + .diff_map + .diff_hunks(&snapshot.buffer_snapshot) .filter(|hunk| hunk.buffer_id == buffer_id) .fuse() .peekable(); - let mut highlights_to_remove = - Vec::with_capacity(editor.expanded_hunks.hunks.len()); + let mut highlights_to_remove = Vec::with_capacity(editor.diff_map.hunks.len()); let mut blocks_to_remove = HashSet::default(); - let mut hunks_to_reexpand = - Vec::with_capacity(editor.expanded_hunks.hunks.len()); - editor.expanded_hunks.hunks.retain_mut(|expanded_hunk| { + let mut hunks_to_reexpand = Vec::with_capacity(editor.diff_map.hunks.len()); + editor.diff_map.hunks.retain_mut(|expanded_hunk| { if expanded_hunk.hunk_range.start.buffer_id != Some(buffer_id) { return true; }; @@ -874,7 +968,7 @@ impl Editor { > hunk_display_range.end { recalculated_hunks.next(); - if editor.expanded_hunks.expand_all { + if editor.diff_map.expand_all { hunks_to_reexpand.push(HoveredHunk { status, multi_buffer_range, @@ -917,7 +1011,7 @@ impl Editor { retain }); - if editor.expanded_hunks.expand_all { + if editor.diff_map.expand_all { for hunk in recalculated_hunks { match diff_hunk_to_display(&hunk, &snapshot) { DisplayDiffHunk::Folded { .. } => {} @@ -935,6 +1029,8 @@ impl Editor { } } } + } else { + drop(recalculated_hunks); } editor.remove_highlighted_rows::(highlights_to_remove, cx); @@ -949,32 +1045,12 @@ impl Editor { .ok(); }); - self.expanded_hunks.hunk_update_tasks.insert( + diff_map.hunk_update_tasks.insert( Some(buffer_id), cx.background_executor().spawn(new_sync_task), ); } - fn current_diff_base_buffer( - &mut self, - buffer: &Model, - cx: &mut AppContext, - ) -> Option> { - buffer.update(cx, |buffer, _| { - match self.expanded_hunks.diff_base.entry(buffer.remote_id()) { - hash_map::Entry::Occupied(o) => { - if o.get().diff_base_version != buffer.diff_base_version() { - o.remove(); - None - } else { - Some(o.get().buffer.clone()) - } - } - hash_map::Entry::Vacant(_) => None, - } - }) - } - fn go_to_subsequent_hunk(&mut self, position: Anchor, cx: &mut ViewContext) { let snapshot = self.snapshot(cx); let position = position.to_point(&snapshot.buffer_snapshot); @@ -1021,7 +1097,7 @@ impl Editor { } } -fn to_diff_hunk( +pub(crate) fn to_diff_hunk( hovered_hunk: &HoveredHunk, multi_buffer_snapshot: &MultiBufferSnapshot, ) -> Option { @@ -1043,24 +1119,6 @@ fn to_diff_hunk( }) } -fn create_diff_base_buffer(buffer: &Model, cx: &mut AppContext) -> Option> { - buffer - .update(cx, |buffer, _| { - let language = buffer.language().cloned(); - let diff_base = buffer.diff_base()?.clone(); - Some((buffer.line_ending(), diff_base, language)) - }) - .map(|(line_ending, diff_base, language)| { - cx.new_model(|cx| { - let buffer = Buffer::local_normalized(diff_base, line_ending, cx); - match language { - Some(language) => buffer.with_language(language, cx), - None => buffer, - } - }) - }) -} - fn added_hunk_color(cx: &AppContext) -> Hsla { let mut created_color = cx.theme().status().git().created; created_color.fade_out(0.7); @@ -1118,51 +1176,27 @@ fn editor_with_deleted_text( }); })]); - let original_multi_buffer_range = hunk.multi_buffer_range.clone(); - let diff_base_range = hunk.diff_base_byte_range.clone(); editor .register_action::({ + let hunk = hunk.clone(); let parent_editor = parent_editor.clone(); move |_, cx| { parent_editor - .update(cx, |editor, cx| { - let Some((buffer, original_text)) = - editor.buffer().update(cx, |buffer, cx| { - let (_, buffer, _) = buffer.excerpt_containing( - original_multi_buffer_range.start, - cx, - )?; - let original_text = - buffer.read(cx).diff_base()?.slice(diff_base_range.clone()); - Some((buffer, Arc::from(original_text.to_string()))) - }) - else { - return; - }; - buffer.update(cx, |buffer, cx| { - buffer.edit( - Some(( - original_multi_buffer_range.start.text_anchor - ..original_multi_buffer_range.end.text_anchor, - original_text, - )), - None, - cx, - ) - }); - }) + .update(cx, |editor, cx| editor.revert_hunk(hunk.clone(), cx)) .ok(); } }) .detach(); - let hunk = hunk.clone(); editor - .register_action::(move |_, cx| { - parent_editor - .update(cx, |editor, cx| { - editor.toggle_hovered_hunk(&hunk, cx); - }) - .ok(); + .register_action::({ + let hunk = hunk.clone(); + move |_, cx| { + parent_editor + .update(cx, |editor, cx| { + editor.toggle_hovered_hunk(&hunk, cx); + }) + .ok(); + } }) .detach(); editor @@ -1272,78 +1306,57 @@ mod tests { let project = Project::test(fs, [], cx).await; // buffer has two modified hunks with two rows each - let buffer_1 = project.update(cx, |project, cx| { - project.create_local_buffer( - " - 1.zero - 1.ONE - 1.TWO - 1.three - 1.FOUR - 1.FIVE - 1.six - " - .unindent() - .as_str(), - None, - cx, - ) - }); - buffer_1.update(cx, |buffer, cx| { - buffer.set_diff_base( - Some( - " - 1.zero - 1.one - 1.two - 1.three - 1.four - 1.five - 1.six - " - .unindent(), - ), - cx, - ); - }); + let diff_base_1 = " + 1.zero + 1.one + 1.two + 1.three + 1.four + 1.five + 1.six + " + .unindent(); + + let text_1 = " + 1.zero + 1.ONE + 1.TWO + 1.three + 1.FOUR + 1.FIVE + 1.six + " + .unindent(); // buffer has a deletion hunk and an insertion hunk - let buffer_2 = project.update(cx, |project, cx| { - project.create_local_buffer( - " - 2.zero - 2.one - 2.two - 2.three - 2.four - 2.five - 2.six - " - .unindent() - .as_str(), - None, - cx, - ) - }); - buffer_2.update(cx, |buffer, cx| { - buffer.set_diff_base( - Some( - " - 2.zero - 2.one - 2.one-and-a-half - 2.two - 2.three - 2.four - 2.six - " - .unindent(), - ), - cx, - ); - }); + let diff_base_2 = " + 2.zero + 2.one + 2.one-and-a-half + 2.two + 2.three + 2.four + 2.six + " + .unindent(); - cx.background_executor.run_until_parked(); + let text_2 = " + 2.zero + 2.one + 2.two + 2.three + 2.four + 2.five + 2.six + " + .unindent(); + + let buffer_1 = project.update(cx, |project, cx| { + project.create_local_buffer(text_1.as_str(), None, cx) + }); + let buffer_2 = project.update(cx, |project, cx| { + project.create_local_buffer(text_2.as_str(), None, cx) + }); let multibuffer = cx.new_model(|cx| { let mut multibuffer = MultiBuffer::new(ReadWrite); @@ -1392,10 +1405,30 @@ mod tests { multibuffer }); - let snapshot = multibuffer.read_with(cx, |b, cx| b.snapshot(cx)); + let editor = cx.add_window(|cx| Editor::for_multibuffer(multibuffer, None, false, cx)); + editor + .update(cx, |editor, cx| { + for (buffer, diff_base) in [ + (buffer_1.clone(), diff_base_1), + (buffer_2.clone(), diff_base_2), + ] { + let change_set = cx.new_model(|cx| { + BufferChangeSet::new_with_base_text( + diff_base.to_string(), + buffer.read(cx).text_snapshot(), + cx, + ) + }); + editor.diff_map.add_change_set(change_set, cx) + } + }) + .unwrap(); + cx.background_executor.run_until_parked(); + + let snapshot = editor.update(cx, |editor, cx| editor.snapshot(cx)).unwrap(); assert_eq!( - snapshot.text(), + snapshot.buffer_snapshot.text(), " 1.zero 1.ONE @@ -1438,7 +1471,8 @@ mod tests { assert_eq!( snapshot - .git_diff_hunks_in_range(MultiBufferRow(0)..MultiBufferRow(12)) + .diff_map + .diff_hunks_in_range(Point::zero()..Point::new(12, 0), &snapshot.buffer_snapshot) .map(|hunk| (hunk_status(&hunk), hunk.row_range)) .collect::>(), &expected, @@ -1446,7 +1480,11 @@ mod tests { assert_eq!( snapshot - .git_diff_hunks_in_range_rev(MultiBufferRow(0)..MultiBufferRow(12)) + .diff_map + .diff_hunks_in_range_rev( + Point::zero()..Point::new(12, 0), + &snapshot.buffer_snapshot + ) .map(|hunk| (hunk_status(&hunk), hunk.row_range)) .collect::>(), expected diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 2f2eb493bb..298ef5a3f0 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -737,7 +737,7 @@ impl Item for Editor { let buffers = self.buffer().clone().read(cx).all_buffers(); let buffers = buffers .into_iter() - .map(|handle| handle.read(cx).diff_base_buffer().unwrap_or(handle.clone())) + .map(|handle| handle.read(cx).base_buffer().unwrap_or(handle.clone())) .collect::>(); cx.spawn(|this, mut cx| async move { if format { diff --git a/crates/editor/src/proposed_changes_editor.rs b/crates/editor/src/proposed_changes_editor.rs index ac97fe18da..f4934c32b0 100644 --- a/crates/editor/src/proposed_changes_editor.rs +++ b/crates/editor/src/proposed_changes_editor.rs @@ -4,7 +4,7 @@ use futures::{channel::mpsc, future::join_all}; use gpui::{AppContext, EventEmitter, FocusableView, Model, Render, Subscription, Task, View}; use language::{Buffer, BufferEvent, Capability}; use multi_buffer::{ExcerptRange, MultiBuffer}; -use project::Project; +use project::{buffer_store::BufferChangeSet, Project}; use smol::stream::StreamExt; use std::{any::TypeId, ops::Range, rc::Rc, time::Duration}; use text::ToOffset; @@ -75,7 +75,7 @@ impl ProposedChangesEditor { title: title.into(), buffer_entries: Vec::new(), recalculate_diffs_tx, - _recalculate_diffs_task: cx.spawn(|_, mut cx| async move { + _recalculate_diffs_task: cx.spawn(|this, mut cx| async move { let mut buffers_to_diff = HashSet::default(); while let Some(mut recalculate_diff) = recalculate_diffs_rx.next().await { buffers_to_diff.insert(recalculate_diff.buffer); @@ -96,12 +96,37 @@ impl ProposedChangesEditor { } } - join_all(buffers_to_diff.drain().filter_map(|buffer| { - buffer - .update(&mut cx, |buffer, cx| buffer.recalculate_diff(cx)) - .ok()? - })) - .await; + let recalculate_diff_futures = this + .update(&mut cx, |this, cx| { + buffers_to_diff + .drain() + .filter_map(|buffer| { + let buffer = buffer.read(cx); + let base_buffer = buffer.base_buffer()?; + let buffer = buffer.text_snapshot(); + let change_set = this.editor.update(cx, |editor, _| { + Some( + editor + .diff_map + .diff_bases + .get(&buffer.remote_id())? + .change_set + .clone(), + ) + })?; + Some(change_set.update(cx, |change_set, cx| { + change_set.set_base_text( + base_buffer.read(cx).text(), + buffer, + cx, + ) + })) + }) + .collect::>() + }) + .ok()?; + + join_all(recalculate_diff_futures).await; } None }), @@ -154,6 +179,7 @@ impl ProposedChangesEditor { }); let mut buffer_entries = Vec::new(); + let mut new_change_sets = Vec::new(); for location in locations { let branch_buffer; if let Some(ix) = self @@ -166,6 +192,15 @@ impl ProposedChangesEditor { buffer_entries.push(entry); } else { branch_buffer = location.buffer.update(cx, |buffer, cx| buffer.branch(cx)); + new_change_sets.push(cx.new_model(|cx| { + let mut change_set = BufferChangeSet::new(branch_buffer.read(cx)); + let _ = change_set.set_base_text( + location.buffer.read(cx).text(), + branch_buffer.read(cx).text_snapshot(), + cx, + ); + change_set + })); buffer_entries.push(BufferEntry { branch: branch_buffer.clone(), base: location.buffer.clone(), @@ -187,7 +222,10 @@ impl ProposedChangesEditor { self.buffer_entries = buffer_entries; self.editor.update(cx, |editor, cx| { - editor.change_selections(None, cx, |selections| selections.refresh()) + editor.change_selections(None, cx, |selections| selections.refresh()); + for change_set in new_change_sets { + editor.diff_map.add_change_set(change_set, cx) + } }); } @@ -217,14 +255,14 @@ impl ProposedChangesEditor { }) .ok(); } - BufferEvent::DiffBaseChanged => { - self.recalculate_diffs_tx - .unbounded_send(RecalculateDiff { - buffer, - debounce: false, - }) - .ok(); - } + // BufferEvent::DiffBaseChanged => { + // self.recalculate_diffs_tx + // .unbounded_send(RecalculateDiff { + // buffer, + // debounce: false, + // }) + // .ok(); + // } _ => (), } } @@ -373,7 +411,7 @@ impl BranchBufferSemanticsProvider { positions: &[text::Anchor], cx: &AppContext, ) -> Option> { - let base_buffer = buffer.read(cx).diff_base_buffer()?; + let base_buffer = buffer.read(cx).base_buffer()?; let version = base_buffer.read(cx).version(); if positions .iter() diff --git a/crates/editor/src/test/editor_lsp_test_context.rs b/crates/editor/src/test/editor_lsp_test_context.rs index b43d78bc99..fd890b839d 100644 --- a/crates/editor/src/test/editor_lsp_test_context.rs +++ b/crates/editor/src/test/editor_lsp_test_context.rs @@ -113,7 +113,15 @@ impl EditorLspTestContext { app_state .fs .as_fake() - .insert_tree(root, json!({ "dir": { file_name.clone(): "" }})) + .insert_tree( + root, + json!({ + ".git": {}, + "dir": { + file_name.clone(): "" + } + }), + ) .await; let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index de5065d265..11b14e8122 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -42,16 +42,16 @@ pub struct EditorTestContext { impl EditorTestContext { pub async fn new(cx: &mut gpui::TestAppContext) -> EditorTestContext { let fs = FakeFs::new(cx.executor()); - // fs.insert_file("/file", "".to_owned()).await; let root = Self::root_path(); fs.insert_tree( root, serde_json::json!({ + ".git": {}, "file": "", }), ) .await; - let project = Project::test(fs, [root], cx).await; + let project = Project::test(fs.clone(), [root], cx).await; let buffer = project .update(cx, |project, cx| { project.open_local_buffer(root.join("file"), cx) @@ -65,6 +65,8 @@ impl EditorTestContext { editor }); let editor_view = editor.root_view(cx).unwrap(); + + cx.run_until_parked(); Self { cx: VisualTestContext::from_window(*editor.deref(), cx), window: editor.into(), @@ -276,8 +278,16 @@ impl EditorTestContext { snapshot.anchor_before(ranges[0].start)..snapshot.anchor_after(ranges[0].end) } - pub fn set_diff_base(&mut self, diff_base: Option<&str>) { - self.update_buffer(|buffer, cx| buffer.set_diff_base(diff_base.map(ToOwned::to_owned), cx)); + pub fn set_diff_base(&mut self, diff_base: &str) { + self.cx.run_until_parked(); + let fs = self + .update_editor(|editor, cx| editor.project.as_ref().unwrap().read(cx).fs().as_fake()); + let path = self.update_buffer(|buffer, _| buffer.file().unwrap().path().clone()); + fs.set_index_for_repo( + &Self::root_path().join(".git"), + &[(path.as_ref(), diff_base.to_string())], + ); + self.cx.run_until_parked(); } /// Change the editor's text and selections using a string containing @@ -319,10 +329,12 @@ impl EditorTestContext { state_context } + /// Assert about the text of the editor, the selections, and the expanded + /// diff hunks. + /// + /// Diff hunks are indicated by lines starting with `+` and `-`. #[track_caller] - pub fn assert_diff_hunks(&mut self, expected_diff: String) { - // Normalize the expected diff. If it has no diff markers, then insert blank markers - // before each line. Strip any whitespace-only lines. + pub fn assert_state_with_diff(&mut self, expected_diff: String) { let has_diff_markers = expected_diff .lines() .any(|line| line.starts_with("+") || line.starts_with("-")); @@ -340,11 +352,14 @@ impl EditorTestContext { }) .join("\n"); + let actual_selections = self.editor_selections(); + let actual_marked_text = + generate_marked_text(&self.buffer_text(), &actual_selections, true); + // Read the actual diff from the editor's row highlights and block // decorations. let actual_diff = self.editor.update(&mut self.cx, |editor, cx| { let snapshot = editor.snapshot(cx); - let text = editor.text(cx); let insertions = editor .highlighted_rows::() .map(|(range, _)| { @@ -354,7 +369,7 @@ impl EditorTestContext { }) .collect::>(); let deletions = editor - .expanded_hunks + .diff_map .hunks .iter() .filter_map(|hunk| { @@ -371,10 +386,20 @@ impl EditorTestContext { .read(cx) .excerpt_containing(hunk.hunk_range.start, cx) .expect("no excerpt for expanded buffer's hunk start"); - let deleted_text = buffer - .read(cx) - .diff_base() + let buffer_id = buffer.read(cx).remote_id(); + let change_set = &editor + .diff_map + .diff_bases + .get(&buffer_id) .expect("should have a diff base for expanded hunk") + .change_set; + let deleted_text = change_set + .read(cx) + .base_text + .as_ref() + .expect("no base text for expanded hunk") + .read(cx) + .as_rope() .slice(hunk.diff_base_byte_range.clone()) .to_string(); if let DiffHunkStatus::Modified | DiffHunkStatus::Removed = hunk.status { @@ -384,7 +409,7 @@ impl EditorTestContext { } }) .collect::>(); - format_diff(text, deletions, insertions) + format_diff(actual_marked_text, deletions, insertions) }); pretty_assertions::assert_eq!(actual_diff, expected_diff_text, "unexpected diff state"); diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 37525db7d9..17571de76b 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -132,7 +132,7 @@ pub trait Fs: Send + Sync { async fn is_case_sensitive(&self) -> Result; #[cfg(any(test, feature = "test-support"))] - fn as_fake(&self) -> &FakeFs { + fn as_fake(&self) -> Arc { panic!("called as_fake on a real fs"); } } @@ -840,6 +840,7 @@ impl Watcher for RealWatcher { #[cfg(any(test, feature = "test-support"))] pub struct FakeFs { + this: std::sync::Weak, // Use an unfair lock to ensure tests are deterministic. state: Mutex, executor: gpui::BackgroundExecutor, @@ -1022,7 +1023,8 @@ impl FakeFs { pub fn new(executor: gpui::BackgroundExecutor) -> Arc { let (tx, mut rx) = smol::channel::bounded::(10); - let this = Arc::new(Self { + let this = Arc::new_cyclic(|this| Self { + this: this.clone(), executor: executor.clone(), state: Mutex::new(FakeFsState { root: Arc::new(Mutex::new(FakeFsEntry::Dir { @@ -1474,7 +1476,8 @@ struct FakeHandle { #[cfg(any(test, feature = "test-support"))] impl FileHandle for FakeHandle { fn current_path(&self, fs: &Arc) -> Result { - let state = fs.as_fake().state.lock(); + let fs = fs.as_fake(); + let state = fs.state.lock(); let Some(target) = state.moves.get(&self.inode) else { anyhow::bail!("fake fd not moved") }; @@ -1970,8 +1973,8 @@ impl Fs for FakeFs { } #[cfg(any(test, feature = "test-support"))] - fn as_fake(&self) -> &FakeFs { - self + fn as_fake(&self) -> Arc { + self.this.upgrade().unwrap() } } diff --git a/crates/git/Cargo.toml b/crates/git/Cargo.toml index 8723e41ce4..c0f43e08a8 100644 --- a/crates/git/Cargo.toml +++ b/crates/git/Cargo.toml @@ -14,7 +14,6 @@ path = "src/git.rs" [dependencies] anyhow.workspace = true async-trait.workspace = true -clock.workspace = true collections.workspace = true derive_more.workspace = true git2.workspace = true diff --git a/crates/git/src/diff.rs b/crates/git/src/diff.rs index 23e9388a28..d468603663 100644 --- a/crates/git/src/diff.rs +++ b/crates/git/src/diff.rs @@ -64,18 +64,33 @@ impl sum_tree::Summary for DiffHunkSummary { #[derive(Debug, Clone)] pub struct BufferDiff { - last_buffer_version: Option, tree: SumTree, } impl BufferDiff { pub fn new(buffer: &BufferSnapshot) -> BufferDiff { BufferDiff { - last_buffer_version: None, tree: SumTree::new(buffer), } } + pub async fn build(diff_base: &str, buffer: &text::BufferSnapshot) -> Self { + let mut tree = SumTree::new(buffer); + + let buffer_text = buffer.as_rope().to_string(); + let patch = Self::diff(diff_base, &buffer_text); + + if let Some(patch) = patch { + let mut divergence = 0; + for hunk_index in 0..patch.num_hunks() { + let hunk = Self::process_patch_hunk(&patch, hunk_index, buffer, &mut divergence); + tree.push(hunk, buffer); + } + } + + Self { tree } + } + pub fn is_empty(&self) -> bool { self.tree.is_empty() } @@ -168,27 +183,11 @@ impl BufferDiff { #[cfg(test)] fn clear(&mut self, buffer: &text::BufferSnapshot) { - self.last_buffer_version = Some(buffer.version().clone()); self.tree = SumTree::new(buffer); } pub async fn update(&mut self, diff_base: &Rope, buffer: &text::BufferSnapshot) { - let mut tree = SumTree::new(buffer); - - let diff_base_text = diff_base.to_string(); - let buffer_text = buffer.as_rope().to_string(); - let patch = Self::diff(&diff_base_text, &buffer_text); - - if let Some(patch) = patch { - let mut divergence = 0; - for hunk_index in 0..patch.num_hunks() { - let hunk = Self::process_patch_hunk(&patch, hunk_index, buffer, &mut divergence); - tree.push(hunk, buffer); - } - } - - self.tree = tree; - self.last_buffer_version = Some(buffer.version().clone()); + *self = Self::build(&diff_base.to_string(), buffer).await; } #[cfg(test)] diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index 8b97d4a95f..d3cb1cfda2 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -34,7 +34,6 @@ ec4rs.workspace = true fs.workspace = true futures.workspace = true fuzzy.workspace = true -git.workspace = true globset.workspace = true gpui.workspace = true http_client.workspace = true diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index e39d4523d7..833a71c899 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -90,22 +90,11 @@ pub enum Capability { pub type BufferRow = u32; -#[derive(Clone)] -enum BufferDiffBase { - Git(Rope), - PastBufferVersion { - buffer: Model, - rope: Rope, - merged_operations: Vec, - }, -} - /// An in-memory representation of a source code file, including its text, /// syntax trees, git status, and diagnostics. pub struct Buffer { text: TextBuffer, - diff_base: Option, - git_diff: git::diff::BufferDiff, + branch_state: Option, /// Filesystem state, `None` when there is no path. file: Option>, /// The mtime of the file when this buffer was last loaded from @@ -135,7 +124,6 @@ pub struct Buffer { deferred_ops: OperationQueue, capability: Capability, has_conflict: bool, - diff_base_version: usize, /// Memoize calls to has_changes_since(saved_version). /// The contents of a cell are (self.version, has_changes) at the time of a last call. has_unsaved_edits: Cell<(clock::Global, bool)>, @@ -148,11 +136,15 @@ pub enum ParseStatus { Parsing, } +struct BufferBranchState { + base_buffer: Model, + merged_operations: Vec, +} + /// An immutable, cheaply cloneable representation of a fixed /// state of a buffer. pub struct BufferSnapshot { text: text::BufferSnapshot, - git_diff: git::diff::BufferDiff, pub(crate) syntax: SyntaxSnapshot, file: Option>, diagnostics: SmallVec<[(LanguageServerId, DiagnosticSet); 2]>, @@ -345,10 +337,6 @@ pub enum BufferEvent { Reloaded, /// The buffer is in need of a reload ReloadNeeded, - /// The buffer's diff_base changed. - DiffBaseChanged, - /// Buffer's excerpts for a certain diff base were recalculated. - DiffUpdated, /// The buffer's language was changed. LanguageChanged, /// The buffer's syntax trees were updated. @@ -626,7 +614,6 @@ impl Buffer { Self::build( TextBuffer::new(0, cx.entity_id().as_non_zero_u64().into(), base_text.into()), None, - None, Capability::ReadWrite, ) } @@ -645,7 +632,6 @@ impl Buffer { base_text_normalized, ), None, - None, Capability::ReadWrite, ) } @@ -660,7 +646,6 @@ impl Buffer { Self::build( TextBuffer::new(replica_id, remote_id, base_text.into()), None, - None, capability, ) } @@ -676,7 +661,7 @@ impl Buffer { let buffer_id = BufferId::new(message.id) .with_context(|| anyhow!("Could not deserialize buffer_id"))?; let buffer = TextBuffer::new(replica_id, buffer_id, message.base_text); - let mut this = Self::build(buffer, message.diff_base, file, capability); + let mut this = Self::build(buffer, file, capability); this.text.set_line_ending(proto::deserialize_line_ending( rpc::proto::LineEnding::from_i32(message.line_ending) .ok_or_else(|| anyhow!("missing line_ending"))?, @@ -692,7 +677,6 @@ impl Buffer { id: self.remote_id().into(), file: self.file.as_ref().map(|f| f.to_proto(cx)), base_text: self.base_text().to_string(), - diff_base: self.diff_base().as_ref().map(|h| h.to_string()), line_ending: proto::serialize_line_ending(self.line_ending()) as i32, saved_version: proto::serialize_version(&self.saved_version), saved_mtime: self.saved_mtime.map(|time| time.into()), @@ -766,15 +750,9 @@ impl Buffer { } /// Builds a [`Buffer`] with the given underlying [`TextBuffer`], diff base, [`File`] and [`Capability`]. - pub fn build( - buffer: TextBuffer, - diff_base: Option, - file: Option>, - capability: Capability, - ) -> Self { + pub fn build(buffer: TextBuffer, file: Option>, capability: Capability) -> Self { let saved_mtime = file.as_ref().and_then(|file| file.disk_state().mtime()); let snapshot = buffer.snapshot(); - let git_diff = git::diff::BufferDiff::new(&snapshot); let syntax_map = Mutex::new(SyntaxMap::new(&snapshot)); Self { saved_mtime, @@ -785,12 +763,7 @@ impl Buffer { was_dirty_before_starting_transaction: None, has_unsaved_edits: Cell::new((buffer.version(), false)), text: buffer, - diff_base: diff_base.map(|mut raw_diff_base| { - LineEnding::normalize(&mut raw_diff_base); - BufferDiffBase::Git(Rope::from(raw_diff_base)) - }), - diff_base_version: 0, - git_diff, + branch_state: None, file, capability, syntax_map, @@ -824,7 +797,6 @@ impl Buffer { BufferSnapshot { text, syntax, - git_diff: self.git_diff.clone(), file: self.file.clone(), remote_selections: self.remote_selections.clone(), diagnostics: self.diagnostics.clone(), @@ -837,21 +809,15 @@ impl Buffer { let this = cx.handle(); cx.new_model(|cx| { let mut branch = Self { - diff_base: Some(BufferDiffBase::PastBufferVersion { - buffer: this.clone(), - rope: self.as_rope().clone(), + branch_state: Some(BufferBranchState { + base_buffer: this.clone(), merged_operations: Default::default(), }), language: self.language.clone(), has_conflict: self.has_conflict, has_unsaved_edits: Cell::new(self.has_unsaved_edits.get_mut().clone()), _subscriptions: vec![cx.subscribe(&this, Self::on_base_buffer_event)], - ..Self::build( - self.text.branch(), - None, - self.file.clone(), - self.capability(), - ) + ..Self::build(self.text.branch(), self.file.clone(), self.capability()) }; if let Some(language_registry) = self.language_registry() { branch.set_language_registry(language_registry); @@ -870,7 +836,7 @@ impl Buffer { /// If `ranges` is empty, then all changes will be applied. This buffer must /// be a branch buffer to call this method. pub fn merge_into_base(&mut self, ranges: Vec>, cx: &mut ModelContext) { - let Some(base_buffer) = self.diff_base_buffer() else { + let Some(base_buffer) = self.base_buffer() else { debug_panic!("not a branch buffer"); return; }; @@ -906,14 +872,14 @@ impl Buffer { } let operation = base_buffer.update(cx, |base_buffer, cx| { - cx.emit(BufferEvent::DiffBaseChanged); + // cx.emit(BufferEvent::DiffBaseChanged); base_buffer.edit(edits, None, cx) }); if let Some(operation) = operation { - if let Some(BufferDiffBase::PastBufferVersion { + if let Some(BufferBranchState { merged_operations, .. - }) = &mut self.diff_base + }) = &mut self.branch_state { merged_operations.push(operation); } @@ -929,9 +895,9 @@ impl Buffer { let BufferEvent::Operation { operation, .. } = event else { return; }; - let Some(BufferDiffBase::PastBufferVersion { + let Some(BufferBranchState { merged_operations, .. - }) = &mut self.diff_base + }) = &mut self.branch_state else { return; }; @@ -950,8 +916,6 @@ impl Buffer { let counts = [(timestamp, u32::MAX)].into_iter().collect(); self.undo_operations(counts, cx); } - - self.diff_base_version += 1; } #[cfg(test)] @@ -1123,74 +1087,8 @@ impl Buffer { } } - /// Returns the current diff base, see [`Buffer::set_diff_base`]. - pub fn diff_base(&self) -> Option<&Rope> { - match self.diff_base.as_ref()? { - BufferDiffBase::Git(rope) | BufferDiffBase::PastBufferVersion { rope, .. } => { - Some(rope) - } - } - } - - /// Sets the text that will be used to compute a Git diff - /// against the buffer text. - pub fn set_diff_base(&mut self, diff_base: Option, cx: &ModelContext) { - self.diff_base = diff_base.map(|mut raw_diff_base| { - LineEnding::normalize(&mut raw_diff_base); - BufferDiffBase::Git(Rope::from(raw_diff_base)) - }); - self.diff_base_version += 1; - if let Some(recalc_task) = self.recalculate_diff(cx) { - cx.spawn(|buffer, mut cx| async move { - recalc_task.await; - buffer - .update(&mut cx, |_, cx| { - cx.emit(BufferEvent::DiffBaseChanged); - }) - .ok(); - }) - .detach(); - } - } - - /// Returns a number, unique per diff base set to the buffer. - pub fn diff_base_version(&self) -> usize { - self.diff_base_version - } - - pub fn diff_base_buffer(&self) -> Option> { - match self.diff_base.as_ref()? { - BufferDiffBase::Git(_) => None, - BufferDiffBase::PastBufferVersion { buffer, .. } => Some(buffer.clone()), - } - } - - /// Recomputes the diff. - pub fn recalculate_diff(&self, cx: &ModelContext) -> Option> { - let diff_base_rope = match self.diff_base.as_ref()? { - BufferDiffBase::Git(rope) => rope.clone(), - BufferDiffBase::PastBufferVersion { buffer, .. } => buffer.read(cx).as_rope().clone(), - }; - - let snapshot = self.snapshot(); - let mut diff = self.git_diff.clone(); - let diff = cx.background_executor().spawn(async move { - diff.update(&diff_base_rope, &snapshot).await; - (diff, diff_base_rope) - }); - - Some(cx.spawn(|this, mut cx| async move { - let (buffer_diff, diff_base_rope) = diff.await; - this.update(&mut cx, |this, cx| { - this.git_diff = buffer_diff; - this.non_text_state_update_count += 1; - if let Some(BufferDiffBase::PastBufferVersion { rope, .. }) = &mut this.diff_base { - *rope = diff_base_rope; - } - cx.emit(BufferEvent::DiffUpdated); - }) - .ok(); - })) + pub fn base_buffer(&self) -> Option> { + Some(self.branch_state.as_ref()?.base_buffer.clone()) } /// Returns the primary [`Language`] assigned to this [`Buffer`]. @@ -3992,37 +3890,6 @@ impl BufferSnapshot { }) } - /// Whether the buffer contains any Git changes. - pub fn has_git_diff(&self) -> bool { - !self.git_diff.is_empty() - } - - /// Returns all the Git diff hunks intersecting the given row range. - pub fn git_diff_hunks_in_row_range( - &self, - range: Range, - ) -> impl '_ + Iterator { - self.git_diff.hunks_in_row_range(range, self) - } - - /// Returns all the Git diff hunks intersecting the given - /// range. - pub fn git_diff_hunks_intersecting_range( - &self, - range: Range, - ) -> impl '_ + Iterator { - self.git_diff.hunks_intersecting_range(range, self) - } - - /// Returns all the Git diff hunks intersecting the given - /// range, in reverse order. - pub fn git_diff_hunks_intersecting_range_rev( - &self, - range: Range, - ) -> impl '_ + Iterator { - self.git_diff.hunks_intersecting_range_rev(range, self) - } - /// Returns if the buffer contains any diagnostics. pub fn has_diagnostics(&self) -> bool { !self.diagnostics.is_empty() @@ -4167,7 +4034,6 @@ impl Clone for BufferSnapshot { fn clone(&self) -> Self { Self { text: self.text.clone(), - git_diff: self.git_diff.clone(), syntax: self.syntax.clone(), file: self.file.clone(), remote_selections: self.remote_selections.clone(), diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index 3eab3aaed7..a1d1a57f13 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -6,7 +6,6 @@ use crate::Buffer; use clock::ReplicaId; use collections::BTreeMap; use futures::FutureExt as _; -use git::diff::assert_hunks; use gpui::{AppContext, BorrowAppContext, Model}; use gpui::{Context, TestAppContext}; use indoc::indoc; @@ -2608,15 +2607,6 @@ fn test_branch_and_merge(cx: &mut TestAppContext) { ); }); - // The branch buffer maintains a diff with respect to its base buffer. - start_recalculating_diff(&branch, cx); - cx.run_until_parked(); - assert_diff_hunks( - &branch, - cx, - &[(1..2, "", "1.5\n"), (3..4, "three\n", "THREE\n")], - ); - // Edits to the base are applied to the branch. base.update(cx, |buffer, cx| { buffer.edit([(Point::new(0, 0)..Point::new(0, 0), "ZERO\n")], None, cx) @@ -2626,21 +2616,6 @@ fn test_branch_and_merge(cx: &mut TestAppContext) { assert_eq!(buffer.text(), "ZERO\none\n1.5\ntwo\nTHREE\n"); }); - // Until the git diff recalculation is complete, the git diff references - // the previous content of the base buffer, so that it stays in sync. - start_recalculating_diff(&branch, cx); - assert_diff_hunks( - &branch, - cx, - &[(2..3, "", "1.5\n"), (4..5, "three\n", "THREE\n")], - ); - cx.run_until_parked(); - assert_diff_hunks( - &branch, - cx, - &[(2..3, "", "1.5\n"), (4..5, "three\n", "THREE\n")], - ); - // Edits to any replica of the base are applied to the branch. base_replica.update(cx, |buffer, cx| { buffer.edit([(Point::new(2, 0)..Point::new(2, 0), "2.5\n")], None, cx) @@ -2731,29 +2706,6 @@ fn test_undo_after_merge_into_base(cx: &mut TestAppContext) { branch.read_with(cx, |branch, _| assert_eq!(branch.text(), "ABCdefgHIjk")); } -fn start_recalculating_diff(buffer: &Model, cx: &mut TestAppContext) { - buffer - .update(cx, |buffer, cx| buffer.recalculate_diff(cx).unwrap()) - .detach(); -} - -#[track_caller] -fn assert_diff_hunks( - buffer: &Model, - cx: &mut TestAppContext, - expected_hunks: &[(Range, &str, &str)], -) { - let (snapshot, diff_base) = buffer.read_with(cx, |buffer, _| { - (buffer.snapshot(), buffer.diff_base().unwrap().to_string()) - }); - assert_hunks( - snapshot.git_diff_hunks_intersecting_range(Anchor::MIN..Anchor::MAX), - &snapshot, - &diff_base, - expected_hunks, - ); -} - #[gpui::test(iterations = 100)] fn test_random_collaboration(cx: &mut AppContext, mut rng: StdRng) { let min_peers = env::var("MIN_PEERS") diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index d52d65bca2..60b01bc65f 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -95,10 +95,7 @@ pub enum Event { }, Reloaded, ReloadNeeded, - DiffBaseChanged, - DiffUpdated { - buffer: Model, - }, + LanguageChanged(BufferId), CapabilityChanged, Reparsed(BufferId), @@ -257,6 +254,7 @@ struct Excerpt { pub struct MultiBufferExcerpt<'a> { excerpt: &'a Excerpt, excerpt_offset: usize, + excerpt_position: Point, } #[derive(Clone, Debug)] @@ -1824,8 +1822,6 @@ impl MultiBuffer { language::BufferEvent::FileHandleChanged => Event::FileHandleChanged, language::BufferEvent::Reloaded => Event::Reloaded, language::BufferEvent::ReloadNeeded => Event::ReloadNeeded, - language::BufferEvent::DiffBaseChanged => Event::DiffBaseChanged, - language::BufferEvent::DiffUpdated => Event::DiffUpdated { buffer }, language::BufferEvent::LanguageChanged => { Event::LanguageChanged(buffer.read(cx).remote_id()) } @@ -3424,47 +3420,86 @@ impl MultiBufferSnapshot { .map(|excerpt| (excerpt.id, &excerpt.buffer, excerpt.range.clone())) } - fn excerpts_for_range( + pub fn all_excerpts(&self) -> impl Iterator { + let mut cursor = self.excerpts.cursor::<(usize, Point)>(&()); + cursor.next(&()); + std::iter::from_fn(move || { + let excerpt = cursor.item()?; + let excerpt = MultiBufferExcerpt::new(excerpt, *cursor.start()); + cursor.next(&()); + Some(excerpt) + }) + } + + pub fn excerpts_for_range( &self, range: Range, - ) -> impl Iterator + '_ { + ) -> impl Iterator + '_ { let range = range.start.to_offset(self)..range.end.to_offset(self); - let mut cursor = self.excerpts.cursor::(&()); + let mut cursor = self.excerpts.cursor::<(usize, Point)>(&()); cursor.seek(&range.start, Bias::Right, &()); cursor.prev(&()); iter::from_fn(move || { cursor.next(&()); - if cursor.start() < &range.end { - cursor.item().map(|item| (item, *cursor.start())) + if cursor.start().0 < range.end { + cursor + .item() + .map(|item| MultiBufferExcerpt::new(item, *cursor.start())) } else { None } }) } + pub fn excerpts_for_range_rev( + &self, + range: Range, + ) -> impl Iterator + '_ { + let range = range.start.to_offset(self)..range.end.to_offset(self); + + let mut cursor = self.excerpts.cursor::<(usize, Point)>(&()); + cursor.seek(&range.end, Bias::Left, &()); + if cursor.item().is_none() { + cursor.prev(&()); + } + + std::iter::from_fn(move || { + let excerpt = cursor.item()?; + let excerpt = MultiBufferExcerpt::new(excerpt, *cursor.start()); + cursor.prev(&()); + Some(excerpt) + }) + } + pub fn excerpt_before(&self, id: ExcerptId) -> Option> { let start_locator = self.excerpt_locator_for_id(id); - let mut cursor = self.excerpts.cursor::>(&()); - cursor.seek(&Some(start_locator), Bias::Left, &()); + let mut cursor = self.excerpts.cursor::(&()); + cursor.seek(start_locator, Bias::Left, &()); cursor.prev(&()); let excerpt = cursor.item()?; + let excerpt_offset = cursor.start().text.len; + let excerpt_position = cursor.start().text.lines; Some(MultiBufferExcerpt { excerpt, - excerpt_offset: 0, + excerpt_offset, + excerpt_position, }) } pub fn excerpt_after(&self, id: ExcerptId) -> Option> { let start_locator = self.excerpt_locator_for_id(id); - let mut cursor = self.excerpts.cursor::>(&()); - cursor.seek(&Some(start_locator), Bias::Left, &()); + let mut cursor = self.excerpts.cursor::(&()); + cursor.seek(start_locator, Bias::Left, &()); cursor.next(&()); let excerpt = cursor.item()?; + let excerpt_offset = cursor.start().text.len; + let excerpt_position = cursor.start().text.lines; Some(MultiBufferExcerpt { excerpt, - excerpt_offset: 0, + excerpt_offset, + excerpt_position, }) } @@ -3647,22 +3682,12 @@ impl MultiBufferSnapshot { ) -> impl Iterator> + 'a { let range = range.start.to_offset(self)..range.end.to_offset(self); self.excerpts_for_range(range.clone()) - .filter(move |&(excerpt, _)| redaction_enabled(excerpt.buffer.file())) - .flat_map(move |(excerpt, excerpt_offset)| { - let excerpt_buffer_start = excerpt.range.context.start.to_offset(&excerpt.buffer); - + .filter(move |excerpt| redaction_enabled(excerpt.buffer().file())) + .flat_map(move |excerpt| { excerpt - .buffer - .redacted_ranges(excerpt.range.context.clone()) - .map(move |mut redacted_range| { - // Re-base onto the excerpts coordinates in the multibuffer - redacted_range.start = excerpt_offset - + redacted_range.start.saturating_sub(excerpt_buffer_start); - redacted_range.end = excerpt_offset - + redacted_range.end.saturating_sub(excerpt_buffer_start); - - redacted_range - }) + .buffer() + .redacted_ranges(excerpt.buffer_range().clone()) + .map(move |redacted_range| excerpt.map_range_from_buffer(redacted_range)) .skip_while(move |redacted_range| redacted_range.end < range.start) .take_while(move |redacted_range| redacted_range.start < range.end) }) @@ -3674,12 +3699,13 @@ impl MultiBufferSnapshot { ) -> impl Iterator + '_ { let range = range.start.to_offset(self)..range.end.to_offset(self); self.excerpts_for_range(range.clone()) - .flat_map(move |(excerpt, excerpt_offset)| { - let excerpt_buffer_start = excerpt.range.context.start.to_offset(&excerpt.buffer); + .flat_map(move |excerpt| { + let excerpt_buffer_start = + excerpt.buffer_range().start.to_offset(&excerpt.buffer()); excerpt - .buffer - .runnable_ranges(excerpt.range.context.clone()) + .buffer() + .runnable_ranges(excerpt.buffer_range()) .filter_map(move |mut runnable| { // Re-base onto the excerpts coordinates in the multibuffer // @@ -3688,15 +3714,14 @@ impl MultiBufferSnapshot { if runnable.run_range.start < excerpt_buffer_start { return None; } - if language::ToPoint::to_point(&runnable.run_range.end, &excerpt.buffer).row - > excerpt.max_buffer_row + if language::ToPoint::to_point(&runnable.run_range.end, &excerpt.buffer()) + .row + > excerpt.max_buffer_row() { return None; } - runnable.run_range.start = - excerpt_offset + runnable.run_range.start - excerpt_buffer_start; - runnable.run_range.end = - excerpt_offset + runnable.run_range.end - excerpt_buffer_start; + runnable.run_range = excerpt.map_range_from_buffer(runnable.run_range); + Some(runnable) }) .skip_while(move |runnable| runnable.run_range.end < range.start) @@ -3730,15 +3755,15 @@ impl MultiBufferSnapshot { let range = range.start.to_offset(self)..range.end.to_offset(self); self.excerpts_for_range(range.clone()) - .flat_map(move |(excerpt, excerpt_offset)| { + .flat_map(move |excerpt| { let excerpt_buffer_start_row = - excerpt.range.context.start.to_point(&excerpt.buffer).row; - let excerpt_offset_row = crate::ToPoint::to_point(&excerpt_offset, self).row; + excerpt.buffer_range().start.to_point(&excerpt.buffer()).row; + let excerpt_offset_row = excerpt.start_point().row; excerpt - .buffer + .buffer() .indent_guides_in_range( - excerpt.range.context.clone(), + excerpt.buffer_range(), ignore_disabled_for_language, cx, ) @@ -3856,151 +3881,6 @@ impl MultiBufferSnapshot { }) } - pub fn has_git_diffs(&self) -> bool { - for excerpt in self.excerpts.iter() { - if excerpt.buffer.has_git_diff() { - return true; - } - } - false - } - - pub fn git_diff_hunks_in_range_rev( - &self, - row_range: Range, - ) -> impl Iterator + '_ { - let mut cursor = self.excerpts.cursor::(&()); - - cursor.seek(&Point::new(row_range.end.0, 0), Bias::Left, &()); - if cursor.item().is_none() { - cursor.prev(&()); - } - - std::iter::from_fn(move || { - let excerpt = cursor.item()?; - let multibuffer_start = *cursor.start(); - let multibuffer_end = multibuffer_start + excerpt.text_summary.lines; - if multibuffer_start.row >= row_range.end.0 { - return None; - } - - let mut buffer_start = excerpt.range.context.start; - let mut buffer_end = excerpt.range.context.end; - let excerpt_start_point = buffer_start.to_point(&excerpt.buffer); - let excerpt_end_point = excerpt_start_point + excerpt.text_summary.lines; - - if row_range.start.0 > multibuffer_start.row { - let buffer_start_point = - excerpt_start_point + Point::new(row_range.start.0 - multibuffer_start.row, 0); - buffer_start = excerpt.buffer.anchor_before(buffer_start_point); - } - - if row_range.end.0 < multibuffer_end.row { - let buffer_end_point = - excerpt_start_point + Point::new(row_range.end.0 - multibuffer_start.row, 0); - buffer_end = excerpt.buffer.anchor_before(buffer_end_point); - } - - let buffer_hunks = excerpt - .buffer - .git_diff_hunks_intersecting_range_rev(buffer_start..buffer_end) - .map(move |hunk| { - let start = multibuffer_start.row - + hunk.row_range.start.saturating_sub(excerpt_start_point.row); - let end = multibuffer_start.row - + hunk - .row_range - .end - .min(excerpt_end_point.row + 1) - .saturating_sub(excerpt_start_point.row); - - MultiBufferDiffHunk { - row_range: MultiBufferRow(start)..MultiBufferRow(end), - diff_base_byte_range: hunk.diff_base_byte_range.clone(), - buffer_range: hunk.buffer_range.clone(), - buffer_id: excerpt.buffer_id, - } - }); - - cursor.prev(&()); - - Some(buffer_hunks) - }) - .flatten() - } - - pub fn git_diff_hunks_in_range( - &self, - row_range: Range, - ) -> impl Iterator + '_ { - let mut cursor = self.excerpts.cursor::(&()); - - cursor.seek(&Point::new(row_range.start.0, 0), Bias::Left, &()); - - std::iter::from_fn(move || { - let excerpt = cursor.item()?; - let multibuffer_start = *cursor.start(); - let multibuffer_end = multibuffer_start + excerpt.text_summary.lines; - let mut buffer_start = excerpt.range.context.start; - let mut buffer_end = excerpt.range.context.end; - - let excerpt_rows = match multibuffer_start.row.cmp(&row_range.end.0) { - cmp::Ordering::Less => { - let excerpt_start_point = buffer_start.to_point(&excerpt.buffer); - let excerpt_end_point = excerpt_start_point + excerpt.text_summary.lines; - - if row_range.start.0 > multibuffer_start.row { - let buffer_start_point = excerpt_start_point - + Point::new(row_range.start.0 - multibuffer_start.row, 0); - buffer_start = excerpt.buffer.anchor_before(buffer_start_point); - } - - if row_range.end.0 < multibuffer_end.row { - let buffer_end_point = excerpt_start_point - + Point::new(row_range.end.0 - multibuffer_start.row, 0); - buffer_end = excerpt.buffer.anchor_before(buffer_end_point); - } - excerpt_start_point.row..excerpt_end_point.row - } - cmp::Ordering::Equal if row_range.end.0 == 0 => { - buffer_end = buffer_start; - 0..0 - } - cmp::Ordering::Greater | cmp::Ordering::Equal => return None, - }; - - let buffer_hunks = excerpt - .buffer - .git_diff_hunks_intersecting_range(buffer_start..buffer_end) - .map(move |hunk| { - let buffer_range = if excerpt_rows.start == 0 && excerpt_rows.end == 0 { - MultiBufferRow(0)..MultiBufferRow(1) - } else { - let start = multibuffer_start.row - + hunk.row_range.start.saturating_sub(excerpt_rows.start); - let end = multibuffer_start.row - + hunk - .row_range - .end - .min(excerpt_rows.end + 1) - .saturating_sub(excerpt_rows.start); - MultiBufferRow(start)..MultiBufferRow(end) - }; - MultiBufferDiffHunk { - row_range: buffer_range, - diff_base_byte_range: hunk.diff_base_byte_range.clone(), - buffer_range: hunk.buffer_range.clone(), - buffer_id: excerpt.buffer_id, - } - }); - - cursor.next(&()); - - Some(buffer_hunks) - }) - .flatten() - } - pub fn range_for_syntax_ancestor(&self, range: Range) -> Option> { let range = range.start.to_offset(self)..range.end.to_offset(self); let excerpt = self.excerpt_containing(range.clone())?; @@ -4179,7 +4059,7 @@ impl MultiBufferSnapshot { pub fn excerpt_containing(&self, range: Range) -> Option { let range = range.start.to_offset(self)..range.end.to_offset(self); - let mut cursor = self.excerpts.cursor::(&()); + let mut cursor = self.excerpts.cursor::<(usize, Point)>(&()); cursor.seek(&range.start, Bias::Right, &()); let start_excerpt = cursor.item()?; @@ -4204,12 +4084,12 @@ impl MultiBufferSnapshot { I: IntoIterator> + 'a, { let mut ranges = ranges.into_iter().map(|range| range.to_offset(self)); - let mut cursor = self.excerpts.cursor::(&()); + let mut cursor = self.excerpts.cursor::<(usize, Point)>(&()); cursor.next(&()); let mut current_range = ranges.next(); iter::from_fn(move || { let range = current_range.clone()?; - if range.start >= cursor.end(&()) { + if range.start >= cursor.end(&()).0 { cursor.seek_forward(&range.start, Bias::Right, &()); if range.start == self.len() { cursor.prev(&()); @@ -4217,11 +4097,11 @@ impl MultiBufferSnapshot { } let excerpt = cursor.item()?; - let range_start_in_excerpt = cmp::max(range.start, *cursor.start()); + let range_start_in_excerpt = cmp::max(range.start, cursor.start().0); let range_end_in_excerpt = if excerpt.has_trailing_newline { - cmp::min(range.end, cursor.end(&()) - 1) + cmp::min(range.end, cursor.end(&()).0 - 1) } else { - cmp::min(range.end, cursor.end(&())) + cmp::min(range.end, cursor.end(&()).0) }; let buffer_range = MultiBufferExcerpt::new(excerpt, *cursor.start()) .map_range_to_buffer(range_start_in_excerpt..range_end_in_excerpt); @@ -4237,7 +4117,7 @@ impl MultiBufferSnapshot { text_anchor: excerpt.buffer.anchor_after(buffer_range.end), }; - if range.end > cursor.end(&()) { + if range.end > cursor.end(&()).0 { cursor.next(&()); } else { current_range = ranges.next(); @@ -4256,12 +4136,12 @@ impl MultiBufferSnapshot { ranges: impl IntoIterator>, ) -> impl Iterator)> { let mut ranges = ranges.into_iter().map(|range| range.to_offset(self)); - let mut cursor = self.excerpts.cursor::(&()); + let mut cursor = self.excerpts.cursor::<(usize, Point)>(&()); cursor.next(&()); let mut current_range = ranges.next(); iter::from_fn(move || { let range = current_range.clone()?; - if range.start >= cursor.end(&()) { + if range.start >= cursor.end(&()).0 { cursor.seek_forward(&range.start, Bias::Right, &()); if range.start == self.len() { cursor.prev(&()); @@ -4269,16 +4149,16 @@ impl MultiBufferSnapshot { } let excerpt = cursor.item()?; - let range_start_in_excerpt = cmp::max(range.start, *cursor.start()); + let range_start_in_excerpt = cmp::max(range.start, cursor.start().0); let range_end_in_excerpt = if excerpt.has_trailing_newline { - cmp::min(range.end, cursor.end(&()) - 1) + cmp::min(range.end, cursor.end(&()).0 - 1) } else { - cmp::min(range.end, cursor.end(&())) + cmp::min(range.end, cursor.end(&()).0) }; let buffer_range = MultiBufferExcerpt::new(excerpt, *cursor.start()) .map_range_to_buffer(range_start_in_excerpt..range_end_in_excerpt); - if range.end > cursor.end(&()) { + if range.end > cursor.end(&()).0 { cursor.next(&()); } else { current_range = ranges.next(); @@ -4702,6 +4582,11 @@ impl Excerpt { self.range.context.start.to_offset(&self.buffer) } + /// The [`Excerpt`]'s start point in its [`Buffer`] + fn buffer_start_point(&self) -> Point { + self.range.context.start.to_point(&self.buffer) + } + /// The [`Excerpt`]'s end offset in its [`Buffer`] fn buffer_end_offset(&self) -> usize { self.buffer_start_offset() + self.text_summary.len @@ -4709,10 +4594,11 @@ impl Excerpt { } impl<'a> MultiBufferExcerpt<'a> { - fn new(excerpt: &'a Excerpt, excerpt_offset: usize) -> Self { + fn new(excerpt: &'a Excerpt, (excerpt_offset, excerpt_position): (usize, Point)) -> Self { MultiBufferExcerpt { excerpt, excerpt_offset, + excerpt_position, } } @@ -4740,9 +4626,32 @@ impl<'a> MultiBufferExcerpt<'a> { &self.excerpt.buffer } + pub fn buffer_range(&self) -> Range { + self.excerpt.range.context.clone() + } + + pub fn start_offset(&self) -> usize { + self.excerpt_offset + } + + pub fn start_point(&self) -> Point { + self.excerpt_position + } + /// Maps an offset within the [`MultiBuffer`] to an offset within the [`Buffer`] pub fn map_offset_to_buffer(&self, offset: usize) -> usize { - self.excerpt.buffer_start_offset() + offset.saturating_sub(self.excerpt_offset) + self.excerpt.buffer_start_offset() + + offset + .saturating_sub(self.excerpt_offset) + .min(self.excerpt.text_summary.len) + } + + /// Maps a point within the [`MultiBuffer`] to a point within the [`Buffer`] + pub fn map_point_to_buffer(&self, point: Point) -> Point { + self.excerpt.buffer_start_point() + + point + .saturating_sub(self.excerpt_position) + .min(self.excerpt.text_summary.lines) } /// Maps a range within the [`MultiBuffer`] to a range within the [`Buffer`] @@ -4752,14 +4661,20 @@ impl<'a> MultiBufferExcerpt<'a> { /// Map an offset within the [`Buffer`] to an offset within the [`MultiBuffer`] pub fn map_offset_from_buffer(&self, buffer_offset: usize) -> usize { - let mut buffer_offset_in_excerpt = - buffer_offset.saturating_sub(self.excerpt.buffer_start_offset()); - buffer_offset_in_excerpt = - cmp::min(buffer_offset_in_excerpt, self.excerpt.text_summary.len); - + let buffer_offset_in_excerpt = buffer_offset + .saturating_sub(self.excerpt.buffer_start_offset()) + .min(self.excerpt.text_summary.len); self.excerpt_offset + buffer_offset_in_excerpt } + /// Map a point within the [`Buffer`] to a point within the [`MultiBuffer`] + pub fn map_point_from_buffer(&self, buffer_position: Point) -> Point { + let position_in_excerpt = buffer_position.saturating_sub(self.excerpt.buffer_start_point()); + let position_in_excerpt = + position_in_excerpt.min(self.excerpt.text_summary.lines + Point::new(1, 0)); + self.excerpt_position + position_in_excerpt + } + /// Map a range within the [`Buffer`] to a range within the [`MultiBuffer`] pub fn map_range_from_buffer(&self, buffer_range: Range) -> Range { self.map_offset_from_buffer(buffer_range.start) @@ -4771,6 +4686,10 @@ impl<'a> MultiBufferExcerpt<'a> { range.start >= self.excerpt.buffer_start_offset() && range.end <= self.excerpt.buffer_end_offset() } + + pub fn max_buffer_row(&self) -> u32 { + self.excerpt.max_buffer_row + } } impl ExcerptId { diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index 7a54f7cc47..a4c6231206 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -8,8 +8,8 @@ use anyhow::{anyhow, Context as _, Result}; use client::Client; use collections::{hash_map, HashMap, HashSet}; use fs::Fs; -use futures::{channel::oneshot, stream::FuturesUnordered, StreamExt}; -use git::blame::Blame; +use futures::{channel::oneshot, future::Shared, Future, FutureExt as _, StreamExt}; +use git::{blame::Blame, diff::BufferDiff}; use gpui::{ AppContext, AsyncAppContext, Context as _, EventEmitter, Model, ModelContext, Subscription, Task, WeakModel, @@ -25,7 +25,7 @@ use language::{ use rpc::{proto, AnyProtoClient, ErrorExt as _, TypedEnvelope}; use smol::channel::Receiver; use std::{io, ops::Range, path::Path, str::FromStr as _, sync::Arc, time::Instant}; -use text::BufferId; +use text::{BufferId, LineEnding, Rope}; use util::{debug_panic, maybe, ResultExt as _, TryFutureExt}; use worktree::{File, PathChange, ProjectEntryId, UpdatedGitRepositoriesSet, Worktree, WorktreeId}; @@ -33,14 +33,29 @@ use worktree::{File, PathChange, ProjectEntryId, UpdatedGitRepositoriesSet, Work pub struct BufferStore { state: BufferStoreState, #[allow(clippy::type_complexity)] - loading_buffers_by_path: HashMap< - ProjectPath, - postage::watch::Receiver, Arc>>>, - >, + loading_buffers: HashMap, Arc>>>>, + #[allow(clippy::type_complexity)] + loading_change_sets: + HashMap, Arc>>>>, worktree_store: Model, opened_buffers: HashMap, downstream_client: Option<(AnyProtoClient, u64)>, - shared_buffers: HashMap>>, + shared_buffers: HashMap>, +} + +#[derive(Hash, Eq, PartialEq, Clone)] +struct SharedBuffer { + buffer: Model, + unstaged_changes: Option>, +} + +pub struct BufferChangeSet { + pub buffer_id: BufferId, + pub base_text: Option>, + pub diff_to_buffer: git::diff::BufferDiff, + pub recalculate_diff_task: Option>>, + pub diff_updated_futures: Vec>, + pub base_text_version: usize, } enum BufferStoreState { @@ -66,7 +81,10 @@ struct LocalBufferStore { } enum OpenBuffer { - Buffer(WeakModel), + Complete { + buffer: WeakModel, + unstaged_changes: Option>, + }, Operations(Vec), } @@ -85,6 +103,23 @@ pub struct ProjectTransaction(pub HashMap, language::Transaction>) impl EventEmitter for BufferStore {} impl RemoteBufferStore { + fn load_staged_text( + &self, + buffer_id: BufferId, + cx: &AppContext, + ) -> Task>> { + let project_id = self.project_id; + let client = self.upstream_client.clone(); + cx.background_executor().spawn(async move { + Ok(client + .request(proto::GetStagedText { + project_id, + buffer_id: buffer_id.to_proto(), + }) + .await? + .staged_text) + }) + } pub fn wait_for_remote_buffer( &mut self, id: BufferId, @@ -352,6 +387,27 @@ impl RemoteBufferStore { } impl LocalBufferStore { + fn load_staged_text( + &self, + buffer: &Model, + cx: &AppContext, + ) -> Task>> { + let Some(file) = buffer.read(cx).file() else { + return Task::ready(Err(anyhow!("buffer has no file"))); + }; + let worktree_id = file.worktree_id(cx); + let path = file.path().clone(); + let Some(worktree) = self + .worktree_store + .read(cx) + .worktree_for_id(worktree_id, cx) + else { + return Task::ready(Err(anyhow!("no such worktree"))); + }; + + worktree.read(cx).load_staged_file(path.as_ref(), cx) + } + fn save_local_buffer( &self, buffer_handle: Model, @@ -463,94 +519,71 @@ impl LocalBufferStore { ) { debug_assert!(worktree_handle.read(cx).is_local()); - // Identify the loading buffers whose containing repository that has changed. - let future_buffers = this - .loading_buffers() - .filter_map(|(project_path, receiver)| { - if project_path.worktree_id != worktree_handle.read(cx).id() { - return None; - } - let path = &project_path.path; - changed_repos - .iter() - .find(|(work_dir, _)| path.starts_with(work_dir))?; - let path = path.clone(); - Some(async move { - BufferStore::wait_for_loading_buffer(receiver) - .await - .ok() - .map(|buffer| (buffer, path)) - }) - }) - .collect::>(); - - // Identify the current buffers whose containing repository has changed. - let current_buffers = this - .buffers() + let buffer_change_sets = this + .opened_buffers + .values() .filter_map(|buffer| { - let file = File::from_dyn(buffer.read(cx).file())?; - if file.worktree != worktree_handle { - return None; + if let OpenBuffer::Complete { + buffer, + unstaged_changes, + } = buffer + { + let buffer = buffer.upgrade()?.read(cx); + let file = File::from_dyn(buffer.file())?; + if file.worktree != worktree_handle { + return None; + } + changed_repos + .iter() + .find(|(work_dir, _)| file.path.starts_with(work_dir))?; + let unstaged_changes = unstaged_changes.as_ref()?.upgrade()?; + let snapshot = buffer.text_snapshot(); + Some((unstaged_changes, snapshot, file.path.clone())) + } else { + None } - changed_repos - .iter() - .find(|(work_dir, _)| file.path.starts_with(work_dir))?; - Some((buffer, file.path.clone())) }) .collect::>(); - if future_buffers.len() + current_buffers.len() == 0 { + if buffer_change_sets.is_empty() { return; } cx.spawn(move |this, mut cx| async move { - // Wait for all of the buffers to load. - let future_buffers = future_buffers.collect::>().await; - - // Reload the diff base for every buffer whose containing git repository has changed. let snapshot = worktree_handle.update(&mut cx, |tree, _| tree.as_local().unwrap().snapshot())?; let diff_bases_by_buffer = cx .background_executor() .spawn(async move { - let mut diff_base_tasks = future_buffers + buffer_change_sets .into_iter() - .flatten() - .chain(current_buffers) - .filter_map(|(buffer, path)| { + .filter_map(|(change_set, buffer_snapshot, path)| { let (repo_entry, local_repo_entry) = snapshot.repo_for_path(&path)?; let relative_path = repo_entry.relativize(&snapshot, &path).ok()?; - Some(async move { - let base_text = - local_repo_entry.repo().load_index_text(&relative_path); - Some((buffer, base_text)) - }) + let base_text = local_repo_entry.repo().load_index_text(&relative_path); + Some((change_set, buffer_snapshot, base_text)) }) - .collect::>(); - - let mut diff_bases = Vec::with_capacity(diff_base_tasks.len()); - while let Some(diff_base) = diff_base_tasks.next().await { - if let Some(diff_base) = diff_base { - diff_bases.push(diff_base); - } - } - diff_bases + .collect::>() }) .await; this.update(&mut cx, |this, cx| { - // Assign the new diff bases on all of the buffers. - for (buffer, diff_base) in diff_bases_by_buffer { - let buffer_id = buffer.update(cx, |buffer, cx| { - buffer.set_diff_base(diff_base.clone(), cx); - buffer.remote_id().to_proto() + for (change_set, buffer_snapshot, staged_text) in diff_bases_by_buffer { + change_set.update(cx, |change_set, cx| { + if let Some(staged_text) = staged_text.clone() { + let _ = + change_set.set_base_text(staged_text, buffer_snapshot.clone(), cx); + } else { + change_set.unset_base_text(buffer_snapshot.clone(), cx); + } }); + if let Some((client, project_id)) = &this.downstream_client.clone() { client .send(proto::UpdateDiffBase { project_id: *project_id, - buffer_id, - diff_base, + buffer_id: buffer_snapshot.remote_id().to_proto(), + staged_text, }) .log_err(); } @@ -759,12 +792,7 @@ impl LocalBufferStore { .spawn(async move { text::Buffer::new(0, buffer_id, loaded.text) }) .await; cx.insert_model(reservation, |_| { - Buffer::build( - text_buffer, - loaded.diff_base, - Some(loaded.file), - Capability::ReadWrite, - ) + Buffer::build(text_buffer, Some(loaded.file), Capability::ReadWrite) }) }) }); @@ -777,7 +805,6 @@ impl LocalBufferStore { let text_buffer = text::Buffer::new(0, buffer_id, "".into()); Buffer::build( text_buffer, - None, Some(Arc::new(File { worktree, path, @@ -861,11 +888,12 @@ impl BufferStore { client.add_model_message_handler(Self::handle_buffer_reloaded); client.add_model_message_handler(Self::handle_buffer_saved); client.add_model_message_handler(Self::handle_update_buffer_file); - client.add_model_message_handler(Self::handle_update_diff_base); client.add_model_request_handler(Self::handle_save_buffer); client.add_model_request_handler(Self::handle_blame_buffer); client.add_model_request_handler(Self::handle_reload_buffers); client.add_model_request_handler(Self::handle_get_permalink_to_line); + client.add_model_request_handler(Self::handle_get_staged_text); + client.add_model_message_handler(Self::handle_update_diff_base); } /// Creates a buffer store, optionally retaining its buffers. @@ -885,7 +913,8 @@ impl BufferStore { downstream_client: None, opened_buffers: Default::default(), shared_buffers: Default::default(), - loading_buffers_by_path: Default::default(), + loading_buffers: Default::default(), + loading_change_sets: Default::default(), worktree_store, } } @@ -907,7 +936,8 @@ impl BufferStore { }), downstream_client: None, opened_buffers: Default::default(), - loading_buffers_by_path: Default::default(), + loading_buffers: Default::default(), + loading_change_sets: Default::default(), shared_buffers: Default::default(), worktree_store, } @@ -939,55 +969,125 @@ impl BufferStore { project_path: ProjectPath, cx: &mut ModelContext, ) -> Task>> { - let existing_buffer = self.get_by_path(&project_path, cx); - if let Some(existing_buffer) = existing_buffer { - return Task::ready(Ok(existing_buffer)); + if let Some(buffer) = self.get_by_path(&project_path, cx) { + return Task::ready(Ok(buffer)); } - let Some(worktree) = self - .worktree_store - .read(cx) - .worktree_for_id(project_path.worktree_id, cx) - else { - return Task::ready(Err(anyhow!("no such worktree"))); - }; - - let loading_watch = match self.loading_buffers_by_path.entry(project_path.clone()) { - // If the given path is already being loaded, then wait for that existing - // task to complete and return the same buffer. + let task = match self.loading_buffers.entry(project_path.clone()) { hash_map::Entry::Occupied(e) => e.get().clone(), - - // Otherwise, record the fact that this path is now being loaded. hash_map::Entry::Vacant(entry) => { - let (mut tx, rx) = postage::watch::channel(); - entry.insert(rx.clone()); - let path = project_path.path.clone(); + let Some(worktree) = self + .worktree_store + .read(cx) + .worktree_for_id(project_path.worktree_id, cx) + else { + return Task::ready(Err(anyhow!("no such worktree"))); + }; let load_buffer = match &self.state { BufferStoreState::Local(this) => this.open_buffer(path, worktree, cx), BufferStoreState::Remote(this) => this.open_buffer(path, worktree, cx), }; - cx.spawn(move |this, mut cx| async move { - let load_result = load_buffer.await; - *tx.borrow_mut() = Some(this.update(&mut cx, |this, _cx| { - // Record the fact that the buffer is no longer loading. - this.loading_buffers_by_path.remove(&project_path); - let buffer = load_result.map_err(Arc::new)?; - Ok(buffer) - })?); - anyhow::Ok(()) - }) - .detach(); - rx + entry + .insert( + cx.spawn(move |this, mut cx| async move { + let load_result = load_buffer.await; + this.update(&mut cx, |this, _cx| { + // Record the fact that the buffer is no longer loading. + this.loading_buffers.remove(&project_path); + }) + .ok(); + load_result.map_err(Arc::new) + }) + .shared(), + ) + .clone() } }; - cx.background_executor().spawn(async move { - Self::wait_for_loading_buffer(loading_watch) + cx.background_executor() + .spawn(async move { task.await.map_err(|e| anyhow!("{e}")) }) + } + + pub fn open_unstaged_changes( + &mut self, + buffer: Model, + cx: &mut ModelContext, + ) -> Task>> { + let buffer_id = buffer.read(cx).remote_id(); + if let Some(change_set) = self.get_unstaged_changes(buffer_id) { + return Task::ready(Ok(change_set)); + } + + let task = match self.loading_change_sets.entry(buffer_id) { + hash_map::Entry::Occupied(e) => e.get().clone(), + hash_map::Entry::Vacant(entry) => { + let load = match &self.state { + BufferStoreState::Local(this) => this.load_staged_text(&buffer, cx), + BufferStoreState::Remote(this) => this.load_staged_text(buffer_id, cx), + }; + + entry + .insert( + cx.spawn(move |this, cx| async move { + Self::open_unstaged_changes_internal(this, load.await, buffer, cx) + .await + .map_err(Arc::new) + }) + .shared(), + ) + .clone() + } + }; + + cx.background_executor() + .spawn(async move { task.await.map_err(|e| anyhow!("{e}")) }) + } + + pub async fn open_unstaged_changes_internal( + this: WeakModel, + text: Result>, + buffer: Model, + mut cx: AsyncAppContext, + ) -> Result> { + let text = match text { + Err(e) => { + this.update(&mut cx, |this, cx| { + let buffer_id = buffer.read(cx).remote_id(); + this.loading_change_sets.remove(&buffer_id); + })?; + return Err(e); + } + Ok(text) => text, + }; + + let change_set = buffer.update(&mut cx, |buffer, cx| { + cx.new_model(|_| BufferChangeSet::new(buffer)) + })?; + + if let Some(text) = text { + change_set + .update(&mut cx, |change_set, cx| { + let snapshot = buffer.read(cx).text_snapshot(); + change_set.set_base_text(text, snapshot, cx) + })? .await - .map_err(|e| e.cloned()) - }) + .ok(); + } + + this.update(&mut cx, |this, cx| { + let buffer_id = buffer.read(cx).remote_id(); + this.loading_change_sets.remove(&buffer_id); + if let Some(OpenBuffer::Complete { + unstaged_changes, .. + }) = this.opened_buffers.get_mut(&buffer.read(cx).remote_id()) + { + *unstaged_changes = Some(change_set.downgrade()); + } + })?; + + Ok(change_set) } pub fn create_buffer(&mut self, cx: &mut ModelContext) -> Task>> { @@ -1166,7 +1266,10 @@ impl BufferStore { fn add_buffer(&mut self, buffer: Model, cx: &mut ModelContext) -> Result<()> { let remote_id = buffer.read(cx).remote_id(); let is_remote = buffer.read(cx).replica_id() != 0; - let open_buffer = OpenBuffer::Buffer(buffer.downgrade()); + let open_buffer = OpenBuffer::Complete { + buffer: buffer.downgrade(), + unstaged_changes: None, + }; let handle = cx.handle().downgrade(); buffer.update(cx, move |_, cx| { @@ -1212,15 +1315,11 @@ impl BufferStore { pub fn loading_buffers( &self, - ) -> impl Iterator< - Item = ( - &ProjectPath, - postage::watch::Receiver, Arc>>>, - ), - > { - self.loading_buffers_by_path - .iter() - .map(|(path, rx)| (path, rx.clone())) + ) -> impl Iterator>>)> { + self.loading_buffers.iter().map(|(path, task)| { + let task = task.clone(); + (path, async move { task.await.map_err(|e| anyhow!("{e}")) }) + }) } pub fn get_by_path(&self, path: &ProjectPath, cx: &AppContext) -> Option> { @@ -1235,9 +1334,7 @@ impl BufferStore { } pub fn get(&self, buffer_id: BufferId) -> Option> { - self.opened_buffers - .get(&buffer_id) - .and_then(|buffer| buffer.upgrade()) + self.opened_buffers.get(&buffer_id)?.upgrade() } pub fn get_existing(&self, buffer_id: BufferId) -> Result> { @@ -1252,6 +1349,17 @@ impl BufferStore { }) } + pub fn get_unstaged_changes(&self, buffer_id: BufferId) -> Option> { + if let OpenBuffer::Complete { + unstaged_changes, .. + } = self.opened_buffers.get(&buffer_id)? + { + unstaged_changes.as_ref()?.upgrade() + } else { + None + } + } + pub fn buffer_version_info( &self, cx: &AppContext, @@ -1366,6 +1474,35 @@ impl BufferStore { rx } + pub fn recalculate_buffer_diffs( + &mut self, + buffers: Vec>, + cx: &mut ModelContext, + ) -> impl Future { + let mut futures = Vec::new(); + for buffer in buffers { + let buffer = buffer.read(cx).text_snapshot(); + if let Some(OpenBuffer::Complete { + unstaged_changes, .. + }) = self.opened_buffers.get_mut(&buffer.remote_id()) + { + if let Some(unstaged_changes) = unstaged_changes + .as_ref() + .and_then(|changes| changes.upgrade()) + { + unstaged_changes.update(cx, |unstaged_changes, cx| { + futures.push(unstaged_changes.recalculate_diff(buffer.clone(), cx)); + }); + } else { + unstaged_changes.take(); + } + } + } + async move { + futures::future::join_all(futures).await; + } + } + fn on_buffer_event( &mut self, buffer: Model, @@ -1413,7 +1550,7 @@ impl BufferStore { match this.opened_buffers.entry(buffer_id) { hash_map::Entry::Occupied(mut e) => match e.get_mut() { OpenBuffer::Operations(operations) => operations.extend_from_slice(&ops), - OpenBuffer::Buffer(buffer) => { + OpenBuffer::Complete { buffer, .. } => { if let Some(buffer) = buffer.upgrade() { buffer.update(cx, |buffer, cx| buffer.apply_ops(ops, cx)); } @@ -1449,7 +1586,11 @@ impl BufferStore { self.shared_buffers .entry(guest_id) .or_default() - .insert(buffer.clone()); + .entry(buffer_id) + .or_insert_with(|| SharedBuffer { + buffer: buffer.clone(), + unstaged_changes: None, + }); let buffer = buffer.read(cx); response.buffers.push(proto::BufferVersion { @@ -1469,13 +1610,14 @@ impl BufferStore { .log_err(); } - client - .send(proto::UpdateDiffBase { - project_id, - buffer_id: buffer_id.into(), - diff_base: buffer.diff_base().map(ToString::to_string), - }) - .log_err(); + // todo!(max): do something + // client + // .send(proto::UpdateStagedText { + // project_id, + // buffer_id: buffer_id.into(), + // diff_base: buffer.diff_base().map(ToString::to_string), + // }) + // .log_err(); client .send(proto::BufferReloaded { @@ -1579,32 +1721,6 @@ impl BufferStore { })? } - pub async fn handle_update_diff_base( - this: Model, - envelope: TypedEnvelope, - mut cx: AsyncAppContext, - ) -> Result<()> { - this.update(&mut cx, |this, cx| { - let buffer_id = envelope.payload.buffer_id; - let buffer_id = BufferId::new(buffer_id)?; - if let Some(buffer) = this.get_possibly_incomplete(buffer_id) { - buffer.update(cx, |buffer, cx| { - buffer.set_diff_base(envelope.payload.diff_base.clone(), cx) - }); - } - if let Some((downstream_client, project_id)) = this.downstream_client.as_ref() { - downstream_client - .send(proto::UpdateDiffBase { - project_id: *project_id, - buffer_id: buffer_id.into(), - diff_base: envelope.payload.diff_base, - }) - .log_err(); - } - Ok(()) - })? - } - pub async fn handle_save_buffer( this: Model, envelope: TypedEnvelope, @@ -1654,16 +1770,14 @@ impl BufferStore { let peer_id = envelope.sender_id; let buffer_id = BufferId::new(envelope.payload.buffer_id)?; this.update(&mut cx, |this, _| { - if let Some(buffer) = this.get(buffer_id) { - if let Some(shared) = this.shared_buffers.get_mut(&peer_id) { - if shared.remove(&buffer) { - if shared.is_empty() { - this.shared_buffers.remove(&peer_id); - } - return; + if let Some(shared) = this.shared_buffers.get_mut(&peer_id) { + if shared.remove(&buffer_id).is_some() { + if shared.is_empty() { + this.shared_buffers.remove(&peer_id); } + return; } - }; + } debug_panic!( "peer_id {} closed buffer_id {} which was either not open or already closed", peer_id, @@ -1779,18 +1893,66 @@ impl BufferStore { }) } - pub async fn wait_for_loading_buffer( - mut receiver: postage::watch::Receiver, Arc>>>, - ) -> Result, Arc> { - loop { - if let Some(result) = receiver.borrow().as_ref() { - match result { - Ok(buffer) => return Ok(buffer.to_owned()), - Err(e) => return Err(e.to_owned()), - } + pub async fn handle_get_staged_text( + this: Model, + request: TypedEnvelope, + mut cx: AsyncAppContext, + ) -> Result { + let buffer_id = BufferId::new(request.payload.buffer_id)?; + let change_set = this + .update(&mut cx, |this, cx| { + let buffer = this.get(buffer_id)?; + Some(this.open_unstaged_changes(buffer, cx)) + })? + .ok_or_else(|| anyhow!("no such buffer"))? + .await?; + this.update(&mut cx, |this, _| { + let shared_buffers = this + .shared_buffers + .entry(request.original_sender_id.unwrap_or(request.sender_id)) + .or_default(); + debug_assert!(shared_buffers.contains_key(&buffer_id)); + if let Some(shared) = shared_buffers.get_mut(&buffer_id) { + shared.unstaged_changes = Some(change_set.clone()); } - receiver.next().await; - } + })?; + let staged_text = change_set.read_with(&cx, |change_set, cx| { + change_set + .base_text + .as_ref() + .map(|buffer| buffer.read(cx).text()) + })?; + Ok(proto::GetStagedTextResponse { staged_text }) + } + + pub async fn handle_update_diff_base( + this: Model, + request: TypedEnvelope, + mut cx: AsyncAppContext, + ) -> Result<()> { + let buffer_id = BufferId::new(request.payload.buffer_id)?; + let Some((buffer, change_set)) = this.update(&mut cx, |this, _| { + if let OpenBuffer::Complete { + unstaged_changes, + buffer, + } = this.opened_buffers.get(&buffer_id)? + { + Some((buffer.upgrade()?, unstaged_changes.as_ref()?.upgrade()?)) + } else { + None + } + })? + else { + return Ok(()); + }; + change_set.update(&mut cx, |change_set, cx| { + if let Some(staged_text) = request.payload.staged_text { + let _ = change_set.set_base_text(staged_text, buffer.read(cx).text_snapshot(), cx); + } else { + change_set.unset_base_text(buffer.read(cx).text_snapshot(), cx) + } + })?; + Ok(()) } pub fn reload_buffers( @@ -1839,14 +2001,17 @@ impl BufferStore { cx: &mut ModelContext, ) -> Task> { let buffer_id = buffer.read(cx).remote_id(); - if !self - .shared_buffers - .entry(peer_id) - .or_default() - .insert(buffer.clone()) - { + let shared_buffers = self.shared_buffers.entry(peer_id).or_default(); + if shared_buffers.contains_key(&buffer_id) { return Task::ready(Ok(())); } + shared_buffers.insert( + buffer_id, + SharedBuffer { + buffer: buffer.clone(), + unstaged_changes: None, + }, + ); let Some((client, project_id)) = self.downstream_client.clone() else { return Task::ready(Ok(())); @@ -1909,8 +2074,8 @@ impl BufferStore { } } - pub fn shared_buffers(&self) -> &HashMap>> { - &self.shared_buffers + pub fn has_shared_buffers(&self) -> bool { + !self.shared_buffers.is_empty() } pub fn create_local_buffer( @@ -1998,10 +2163,129 @@ impl BufferStore { } } +impl BufferChangeSet { + pub fn new(buffer: &text::BufferSnapshot) -> Self { + Self { + buffer_id: buffer.remote_id(), + base_text: None, + diff_to_buffer: git::diff::BufferDiff::new(buffer), + recalculate_diff_task: None, + diff_updated_futures: Vec::new(), + base_text_version: 0, + } + } + + #[cfg(any(test, feature = "test-support"))] + pub fn new_with_base_text( + base_text: String, + buffer: text::BufferSnapshot, + cx: &mut ModelContext, + ) -> Self { + let mut this = Self::new(&buffer); + let _ = this.set_base_text(base_text, buffer, cx); + this + } + + pub fn diff_hunks_intersecting_range<'a>( + &'a self, + range: Range, + buffer_snapshot: &'a text::BufferSnapshot, + ) -> impl 'a + Iterator { + self.diff_to_buffer + .hunks_intersecting_range(range, buffer_snapshot) + } + + pub fn diff_hunks_intersecting_range_rev<'a>( + &'a self, + range: Range, + buffer_snapshot: &'a text::BufferSnapshot, + ) -> impl 'a + Iterator { + self.diff_to_buffer + .hunks_intersecting_range_rev(range, buffer_snapshot) + } + + #[cfg(any(test, feature = "test-support"))] + pub fn base_text_string(&self, cx: &AppContext) -> Option { + self.base_text.as_ref().map(|buffer| buffer.read(cx).text()) + } + + pub fn set_base_text( + &mut self, + mut base_text: String, + buffer_snapshot: text::BufferSnapshot, + cx: &mut ModelContext, + ) -> oneshot::Receiver<()> { + LineEnding::normalize(&mut base_text); + self.recalculate_diff_internal(base_text, buffer_snapshot, true, cx) + } + + pub fn unset_base_text( + &mut self, + buffer_snapshot: text::BufferSnapshot, + cx: &mut ModelContext, + ) { + if self.base_text.is_some() { + self.base_text = None; + self.diff_to_buffer = BufferDiff::new(&buffer_snapshot); + self.recalculate_diff_task.take(); + self.base_text_version += 1; + cx.notify(); + } + } + + pub fn recalculate_diff( + &mut self, + buffer_snapshot: text::BufferSnapshot, + cx: &mut ModelContext, + ) -> oneshot::Receiver<()> { + if let Some(base_text) = self.base_text.clone() { + self.recalculate_diff_internal(base_text.read(cx).text(), buffer_snapshot, false, cx) + } else { + oneshot::channel().1 + } + } + + fn recalculate_diff_internal( + &mut self, + base_text: String, + buffer_snapshot: text::BufferSnapshot, + base_text_changed: bool, + cx: &mut ModelContext, + ) -> oneshot::Receiver<()> { + let (tx, rx) = oneshot::channel(); + self.diff_updated_futures.push(tx); + self.recalculate_diff_task = Some(cx.spawn(|this, mut cx| async move { + let (base_text, diff) = cx + .background_executor() + .spawn(async move { + let diff = BufferDiff::build(&base_text, &buffer_snapshot).await; + (base_text, diff) + }) + .await; + this.update(&mut cx, |this, cx| { + if base_text_changed { + this.base_text_version += 1; + this.base_text = Some(cx.new_model(|cx| { + Buffer::local_normalized(Rope::from(base_text), LineEnding::default(), cx) + })); + } + this.diff_to_buffer = diff; + this.recalculate_diff_task.take(); + for tx in this.diff_updated_futures.drain(..) { + tx.send(()).ok(); + } + cx.notify(); + })?; + Ok(()) + })); + rx + } +} + impl OpenBuffer { fn upgrade(&self) -> Option> { match self { - OpenBuffer::Buffer(handle) => handle.upgrade(), + OpenBuffer::Complete { buffer, .. } => buffer.upgrade(), OpenBuffer::Operations(_) => None, } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 74bd065c32..84aedab92b 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -25,7 +25,7 @@ pub mod search_history; mod yarn; use anyhow::{anyhow, Context as _, Result}; -use buffer_store::{BufferStore, BufferStoreEvent}; +use buffer_store::{BufferChangeSet, BufferStore, BufferStoreEvent}; use client::{proto, Client, Collaborator, PendingEntitySubscription, TypedEnvelope, UserStore}; use clock::ReplicaId; use collections::{BTreeSet, HashMap, HashSet}; @@ -1821,6 +1821,20 @@ impl Project { }) } + pub fn open_unstaged_changes( + &mut self, + buffer: Model, + cx: &mut ModelContext, + ) -> Task>> { + if self.is_disconnected(cx) { + return Task::ready(Err(anyhow!(ErrorCode::Disconnected))); + } + + self.buffer_store.update(cx, |buffer_store, cx| { + buffer_store.open_unstaged_changes(buffer, cx) + }) + } + pub fn open_buffer_by_id( &mut self, id: BufferId, @@ -2269,10 +2283,7 @@ impl Project { event: &BufferEvent, cx: &mut ModelContext, ) -> Option<()> { - if matches!( - event, - BufferEvent::Edited { .. } | BufferEvent::Reloaded | BufferEvent::DiffBaseChanged - ) { + if matches!(event, BufferEvent::Edited { .. } | BufferEvent::Reloaded) { self.request_buffer_diff_recalculation(&buffer, cx); } @@ -2369,34 +2380,32 @@ impl Project { } fn recalculate_buffer_diffs(&mut self, cx: &mut ModelContext) -> Task<()> { - let buffers = self.buffers_needing_diff.drain().collect::>(); cx.spawn(move |this, mut cx| async move { - let tasks: Vec<_> = buffers - .iter() - .filter_map(|buffer| { - let buffer = buffer.upgrade()?; - buffer - .update(&mut cx, |buffer, cx| buffer.recalculate_diff(cx)) - .ok() - .flatten() - }) - .collect(); - - futures::future::join_all(tasks).await; - - this.update(&mut cx, |this, cx| { - if this.buffers_needing_diff.is_empty() { - // TODO: Would a `ModelContext.notify()` suffice here? - for buffer in buffers { - if let Some(buffer) = buffer.upgrade() { - buffer.update(cx, |_, cx| cx.notify()); + loop { + let task = this + .update(&mut cx, |this, cx| { + let buffers = this + .buffers_needing_diff + .drain() + .filter_map(|buffer| buffer.upgrade()) + .collect::>(); + if buffers.is_empty() { + None + } else { + Some(this.buffer_store.update(cx, |buffer_store, cx| { + buffer_store.recalculate_buffer_diffs(buffers, cx) + })) } - } + }) + .ok() + .flatten(); + + if let Some(task) = task { + task.await; } else { - this.recalculate_buffer_diffs(cx).detach(); + break; } - }) - .ok(); + } }) } @@ -4149,6 +4158,10 @@ impl Project { .read(cx) .language_servers_for_buffer(buffer, cx) } + + pub fn buffer_store(&self) -> &Model { + &self.buffer_store + } } fn deserialize_code_actions(code_actions: &HashMap) -> Vec { diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 2704259306..26537503dc 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -1,6 +1,7 @@ use crate::{Event, *}; use fs::FakeFs; use futures::{future, StreamExt}; +use git::diff::assert_hunks; use gpui::{AppContext, SemanticVersion, UpdateGlobal}; use http_client::Url; use language::{ @@ -5396,6 +5397,98 @@ async fn test_reordering_worktrees(cx: &mut gpui::TestAppContext) { }); } +#[gpui::test] +async fn test_unstaged_changes_for_buffer(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let staged_contents = r#" + fn main() { + println!("hello world"); + } + "# + .unindent(); + let file_contents = r#" + // print goodbye + fn main() { + println!("goodbye world"); + } + "# + .unindent(); + + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/dir", + json!({ + ".git": {}, + "src": { + "main.rs": file_contents, + } + }), + ) + .await; + + fs.set_index_for_repo( + Path::new("/dir/.git"), + &[(Path::new("src/main.rs"), staged_contents)], + ); + + let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + + let buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/dir/src/main.rs", cx) + }) + .await + .unwrap(); + let unstaged_changes = project + .update(cx, |project, cx| { + project.open_unstaged_changes(buffer.clone(), cx) + }) + .await + .unwrap(); + + cx.run_until_parked(); + unstaged_changes.update(cx, |unstaged_changes, cx| { + let snapshot = buffer.read(cx).snapshot(); + assert_hunks( + unstaged_changes.diff_hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot), + &snapshot, + &unstaged_changes.base_text.as_ref().unwrap().read(cx).text(), + &[ + (0..1, "", "// print goodbye\n"), + ( + 2..3, + " println!(\"hello world\");\n", + " println!(\"goodbye world\");\n", + ), + ], + ); + }); + + let staged_contents = r#" + // print goodbye + fn main() { + } + "# + .unindent(); + + fs.set_index_for_repo( + Path::new("/dir/.git"), + &[(Path::new("src/main.rs"), staged_contents)], + ); + + cx.run_until_parked(); + unstaged_changes.update(cx, |unstaged_changes, cx| { + let snapshot = buffer.read(cx).snapshot(); + assert_hunks( + unstaged_changes.diff_hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot), + &snapshot, + &unstaged_changes.base_text.as_ref().unwrap().read(cx).text(), + &[(2..3, "", " println!(\"goodbye world\");\n")], + ); + }); +} + async fn search( project: &Model, query: SearchQuery, diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 178d88ad26..f0d8f27131 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -301,7 +301,10 @@ message Envelope { SyncExtensions sync_extensions = 285; SyncExtensionsResponse sync_extensions_response = 286; - InstallExtension install_extension = 287; // current max + InstallExtension install_extension = 287; + + GetStagedText get_staged_text = 288; + GetStagedTextResponse get_staged_text_response = 289; // current max } reserved 87 to 88; @@ -1788,11 +1791,12 @@ message BufferState { uint64 id = 1; optional File file = 2; string base_text = 3; - optional string diff_base = 4; LineEnding line_ending = 5; repeated VectorClockEntry saved_version = 6; - reserved 7; Timestamp saved_mtime = 8; + + reserved 7; + reserved 4; } message BufferChunk { @@ -1983,7 +1987,16 @@ message WorktreeMetadata { message UpdateDiffBase { uint64 project_id = 1; uint64 buffer_id = 2; - optional string diff_base = 3; + optional string staged_text = 3; +} + +message GetStagedText { + uint64 project_id = 1; + uint64 buffer_id = 2; +} + +message GetStagedTextResponse { + optional string staged_text = 1; } message GetNotifications { diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 0810a561b9..6a417e6b2a 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -216,6 +216,8 @@ messages!( (GetImplementationResponse, Background), (GetLlmToken, Background), (GetLlmTokenResponse, Background), + (GetStagedText, Foreground), + (GetStagedTextResponse, Foreground), (GetUsers, Foreground), (Hello, Foreground), (IncomingCall, Foreground), @@ -411,6 +413,7 @@ request_messages!( (GetProjectSymbols, GetProjectSymbolsResponse), (GetReferences, GetReferencesResponse), (GetSignatureHelp, GetSignatureHelpResponse), + (GetStagedText, GetStagedTextResponse), (GetSupermavenApiKey, GetSupermavenApiKeyResponse), (GetTypeDefinition, GetTypeDefinitionResponse), (LinkedEditingRange, LinkedEditingRangeResponse), @@ -525,6 +528,7 @@ entity_messages!( GetProjectSymbols, GetReferences, GetSignatureHelp, + GetStagedText, GetTypeDefinition, InlayHints, JoinProject, diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index bdb862c5af..711b3c29bd 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -78,13 +78,22 @@ async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut Test }) .await .unwrap(); + let change_set = project + .update(cx, |project, cx| { + project.open_unstaged_changes(buffer.clone(), cx) + }) + .await + .unwrap(); + + change_set.update(cx, |change_set, cx| { + assert_eq!( + change_set.base_text_string(cx).unwrap(), + "fn one() -> usize { 0 }" + ); + }); buffer.update(cx, |buffer, cx| { assert_eq!(buffer.text(), "fn one() -> usize { 1 }"); - assert_eq!( - buffer.diff_base().unwrap().to_string(), - "fn one() -> usize { 0 }" - ); let ix = buffer.text().find('1').unwrap(); buffer.edit([(ix..ix + 1, "100")], None, cx); }); @@ -140,9 +149,9 @@ async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut Test &[(Path::new("src/lib2.rs"), "fn one() -> usize { 100 }".into())], ); cx.executor().run_until_parked(); - buffer.update(cx, |buffer, _| { + change_set.update(cx, |change_set, cx| { assert_eq!( - buffer.diff_base().unwrap().to_string(), + change_set.base_text_string(cx).unwrap(), "fn one() -> usize { 100 }" ); }); @@ -213,7 +222,7 @@ async fn test_remote_project_search(cx: &mut TestAppContext, server_cx: &mut Tes // test that the headless server is tracking which buffers we have open correctly. cx.run_until_parked(); headless.update(server_cx, |headless, cx| { - assert!(!headless.buffer_store.read(cx).shared_buffers().is_empty()) + assert!(headless.buffer_store.read(cx).has_shared_buffers()) }); do_search(&project, cx.clone()).await; @@ -222,7 +231,7 @@ async fn test_remote_project_search(cx: &mut TestAppContext, server_cx: &mut Tes }); cx.run_until_parked(); headless.update(server_cx, |headless, cx| { - assert!(headless.buffer_store.read(cx).shared_buffers().is_empty()) + assert!(!headless.buffer_store.read(cx).has_shared_buffers()) }); do_search(&project, cx.clone()).await; diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index e856bbf7de..a9762b942b 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -104,7 +104,6 @@ pub enum CreatedEntry { pub struct LoadedFile { pub file: Arc, pub text: String, - pub diff_base: Option, } pub struct LoadedBinaryFile { @@ -707,6 +706,30 @@ impl Worktree { } } + pub fn load_staged_file(&self, path: &Path, cx: &AppContext) -> Task>> { + match self { + Worktree::Local(this) => { + let path = Arc::from(path); + let snapshot = this.snapshot(); + cx.background_executor().spawn(async move { + if let Some(repo) = snapshot.repository_for_path(&path) { + if let Some(repo_path) = repo.relativize(&snapshot, &path).log_err() { + if let Some(git_repo) = + snapshot.git_repositories.get(&*repo.work_directory) + { + return Ok(git_repo.repo_ptr.load_index_text(&repo_path)); + } + } + } + Ok(None) + }) + } + Worktree::Remote(_) => { + Task::ready(Err(anyhow!("remote worktrees can't yet load staged files"))) + } + } + } + pub fn load_binary_file( &self, path: &Path, @@ -1362,28 +1385,9 @@ impl LocalWorktree { let entry = self.refresh_entry(path.clone(), None, cx); let is_private = self.is_path_private(path.as_ref()); - cx.spawn(|this, mut cx| async move { + cx.spawn(|this, _cx| async move { let abs_path = abs_path?; let text = fs.load(&abs_path).await?; - let mut index_task = None; - let snapshot = this.update(&mut cx, |this, _| this.as_local().unwrap().snapshot())?; - if let Some(repo) = snapshot.repository_for_path(&path) { - if let Some(repo_path) = repo.relativize(&snapshot, &path).log_err() { - if let Some(git_repo) = snapshot.git_repositories.get(&*repo.work_directory) { - let git_repo = git_repo.repo_ptr.clone(); - index_task = Some( - cx.background_executor() - .spawn(async move { git_repo.load_index_text(&repo_path) }), - ); - } - } - } - - let diff_base = if let Some(index_task) = index_task { - index_task.await - } else { - None - }; let worktree = this .upgrade() @@ -1413,11 +1417,7 @@ impl LocalWorktree { } }; - Ok(LoadedFile { - file, - text, - diff_base, - }) + Ok(LoadedFile { file, text }) }) } From 5b169fa535efe798f0e5b76f0fd8a87dfa83f912 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 4 Dec 2024 18:04:24 -0500 Subject: [PATCH 293/886] Update Rust crate anyhow to v1.0.94 (#21552) 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 | |---|---|---|---| | [anyhow](https://redirect.github.com/dtolnay/anyhow) | workspace.dependencies | patch | `1.0.93` -> `1.0.94` | --- ### Release Notes
dtolnay/anyhow (anyhow) ### [`v1.0.94`](https://redirect.github.com/dtolnay/anyhow/releases/tag/1.0.94) [Compare Source](https://redirect.github.com/dtolnay/anyhow/compare/1.0.93...1.0.94) - Documentation improvements
--- ### 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 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 820f52a150..c148c5c7a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "activity_indicator" @@ -257,9 +257,9 @@ checksum = "34cd60c5e3152cef0a592f1b296f1cc93715d89d2551d85315828c3a09575ff4" [[package]] name = "anyhow" -version = "1.0.93" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" +checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" [[package]] name = "approx" From 31c976d8d9988fac0607040e8f135ed55f14ab00 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 4 Dec 2024 18:07:45 -0500 Subject: [PATCH 294/886] Update Rust crate cargo_metadata to v0.19.1 (#21556) 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 | patch | `0.19.0` -> `0.19.1` | --- ### Release Notes
oli-obk/cargo_metadata (cargo_metadata) ### [`v0.19.1`](https://redirect.github.com/oli-obk/cargo_metadata/compare/0.19.0...0.19.1) [Compare Source](https://redirect.github.com/oli-obk/cargo_metadata/compare/0.19.0...0.19.1)
--- ### 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 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c148c5c7a1..6a6bbc36aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2159,16 +2159,16 @@ dependencies = [ [[package]] name = "cargo_metadata" -version = "0.19.0" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afc309ed89476c8957c50fb818f56fe894db857866c3e163335faa91dc34eb85" +checksum = "8769706aad5d996120af43197bf46ef6ad0fda35216b4505f926a365a232d924" dependencies = [ "camino", "cargo-platform", "semver", "serde", "serde_json", - "thiserror 1.0.69", + "thiserror 2.0.3", ] [[package]] From b9c390c22e196ac06fac8bdd1d32aef519f233c1 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Wed, 4 Dec 2024 19:26:09 -0500 Subject: [PATCH 295/886] Revert "Open folds containing selections when jumping from multibuffer (#21433)" (#21566) This reverts commit dc32ab25a0f76280ff0f1485333a729523840e27. This has been causing panics, backing it out while figuring out what's up. Release Notes: - N/A --- crates/editor/src/editor.rs | 1 - crates/editor/src/editor_tests.rs | 70 +------------------------------ 2 files changed, 1 insertion(+), 70 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index c5d09ed1bf..c20282fa02 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -12925,7 +12925,6 @@ impl Editor { None => Autoscroll::newest(), }; let nav_history = editor.nav_history.take(); - editor.unfold_ranges(&ranges, false, true, cx); editor.change_selections(Some(autoscroll), cx, |s| { s.select_ranges(ranges); }); diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 7f900e2c39..7561c31f13 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -11651,7 +11651,7 @@ async fn test_multibuffer_reverts(cx: &mut gpui::TestAppContext) { } #[gpui::test] -async fn test_multibuffer_in_navigation_history(cx: &mut gpui::TestAppContext) { +async fn test_mutlibuffer_in_navigation_history(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); let cols = 4; @@ -11940,74 +11940,6 @@ async fn test_multibuffer_in_navigation_history(cx: &mut gpui::TestAppContext) { .unwrap(); } -#[gpui::test] -async fn test_multibuffer_unfold_on_jump(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let texts = ["{\n\tx\n}".to_owned(), "y".to_owned()]; - let buffers = texts - .clone() - .map(|txt| cx.new_model(|cx| Buffer::local(txt, cx))); - let multi_buffer = cx.new_model(|cx| { - let mut multi_buffer = MultiBuffer::new(ReadWrite); - for i in 0..2 { - multi_buffer.push_excerpts( - buffers[i].clone(), - [ExcerptRange { - context: 0..texts[i].len(), - primary: None, - }], - cx, - ); - } - multi_buffer - }); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/project", - json!({ - "x": &texts[0], - "y": &texts[1], - }), - ) - .await; - let project = Project::test(fs, ["/project".as_ref()], cx).await; - let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); - let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx); - - let multi_buffer_editor = cx.new_view(|cx| { - Editor::for_multibuffer(multi_buffer.clone(), Some(project.clone()), true, cx) - }); - let buffer_editor = - cx.new_view(|cx| Editor::for_buffer(buffers[0].clone(), Some(project.clone()), cx)); - workspace - .update(cx, |workspace, cx| { - workspace.add_item_to_active_pane( - Box::new(multi_buffer_editor.clone()), - None, - true, - cx, - ); - workspace.add_item_to_active_pane(Box::new(buffer_editor.clone()), None, false, cx); - }) - .unwrap(); - cx.executor().run_until_parked(); - buffer_editor.update(cx, |buffer_editor, cx| { - buffer_editor.fold_at_level(&FoldAtLevel { level: 1 }, cx); - assert!(buffer_editor.snapshot(cx).fold_count() == 1); - }); - cx.executor().run_until_parked(); - multi_buffer_editor.update(cx, |multi_buffer_editor, cx| { - multi_buffer_editor.change_selections(None, cx, |s| s.select_ranges([3..4])); - multi_buffer_editor.open_excerpts(&OpenExcerpts, cx); - }); - cx.executor().run_until_parked(); - buffer_editor.update(cx, |buffer_editor, cx| { - assert!(buffer_editor.snapshot(cx).fold_count() == 0); - }); -} - #[gpui::test] async fn test_toggle_hunk_diff(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); From 9487fffc55f47562b361c4f66273c73b8ef8041e Mon Sep 17 00:00:00 2001 From: tims <0xtimsb@gmail.com> Date: Thu, 5 Dec 2024 13:31:35 +0530 Subject: [PATCH 296/886] Fix snippet completion will be trigger, when certain symbols are pressed (#21578) Closes #21576 This issue is caused by the fuzzy matching for snippets I added [here](https://github.com/zed-industries/zed/pull/21524). When encountering symbols such as `:`, `(`, `.`, etc., the `last_word` becomes empty, which results in an empty string being passed to `fuzzy_match`, leading to the return of all templates. This fix adds an early return when `last_word` is empty. Release Notes: - N/A --- crates/editor/src/editor.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index c20282fa02..bb4a2788a7 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -13873,6 +13873,11 @@ fn snippet_completions( .take_while(|c| classifier.is_word(*c)) .collect::(); last_word = last_word.chars().rev().collect(); + + if last_word.is_empty() { + return Ok(vec![]); + } + let as_offset = text::ToOffset::to_offset(&buffer_position, &snapshot); let to_lsp = |point: &text::Anchor| { let end = text::ToPointUtf16::to_point_utf16(point, &snapshot); From 78fea0dd8ef4d84ad6a287f4a1fd40a4fbcb4966 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 5 Dec 2024 11:55:06 +0200 Subject: [PATCH 297/886] Defer is_staff check for the project_diff::Deploy action (#21582) During workspace registration, it's too early to check for the `is_staff` flag due to no connection being established yet. As a compromise, allow the action to appear and be registered, but do nothing for non-staff users. Release Notes: - N/A --- crates/editor/src/git/project_diff.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/editor/src/git/project_diff.rs b/crates/editor/src/git/project_diff.rs index 2c60ae4204..e3d9f6abd6 100644 --- a/crates/editor/src/git/project_diff.rs +++ b/crates/editor/src/git/project_diff.rs @@ -62,13 +62,15 @@ struct Changes { } impl ProjectDiffEditor { - fn register(workspace: &mut Workspace, cx: &mut ViewContext) { - if cx.is_staff() { - workspace.register_action(Self::deploy); - } + fn register(workspace: &mut Workspace, _: &mut ViewContext) { + workspace.register_action(Self::deploy); } fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { + if !cx.is_staff() { + return; + } + if let Some(existing) = workspace.item_of_type::(cx) { workspace.activate_item(&existing, true, true, cx); } else { From 7335f211fdc8503666cf11bfb14fd3ff2b288db1 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Thu, 5 Dec 2024 08:07:13 -0500 Subject: [PATCH 298/886] Add Project Panel navigation actions in netrw mode (#20941) Release Notes: - Added "[ c" & "] c" To select prev/next git modified file within the project panel - Added "[ d" & "] d" To select prev/next file with diagnostics from an LSP within the project panel - Added "{" & "}" To select prev/next directory within the project panel Note: I wanted to extend project panel's functionality when netrw is active so I added some shortcuts that I believe will be helpful for most users. I tried to keep the default key mappings for the shortcuts inline with Zed's vim mode. ## Selecting prev/next modified git file https://github.com/user-attachments/assets/a9c057c7-1015-444f-b273-6d52ac54aa9c ## Selecting prev/next diagnostics https://github.com/user-attachments/assets/d1fb04ac-02c6-477c-b751-90a11bb42a78 ## Selecting prev/next directories (Only works with visible directoires) https://github.com/user-attachments/assets/9e96371e-105f-4fe9-bbf7-58f4a529f0dd --- assets/keymaps/vim.json | 6 + crates/project_panel/src/project_panel.rs | 512 +++++++++++++++++++++- crates/project_panel/src/utils.rs | 42 ++ 3 files changed, 559 insertions(+), 1 deletion(-) create mode 100644 crates/project_panel/src/utils.rs diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index c80a6912cc..8931ad0dca 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -659,6 +659,12 @@ "p": "project_panel::Open", "x": "project_panel::RevealInFileManager", "s": "project_panel::OpenWithSystem", + "] c": "project_panel::SelectNextGitEntry", + "[ c": "project_panel::SelectPrevGitEntry", + "] d": "project_panel::SelectNextDiagnostic", + "[ d": "project_panel::SelectPrevDiagnostic", + "}": "project_panel::SelectNextDirectory", + "{": "project_panel::SelectPrevDirectory", "shift-g": "menu::SelectLast", "g g": "menu::SelectFirst", "-": "project_panel::SelectParent", diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 3ef9f1905d..d263c75ca7 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1,4 +1,5 @@ mod project_panel_settings; +mod utils; use client::{ErrorCode, ErrorExt}; use language::DiagnosticSeverity; @@ -56,7 +57,7 @@ use ui::{ IndentGuideColors, IndentGuideLayout, KeyBinding, Label, ListItem, Scrollbar, ScrollbarState, Tooltip, }; -use util::{maybe, paths::compare_paths, ResultExt, TryFutureExt}; +use util::{maybe, paths::compare_paths, ResultExt, TakeUntilExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, notifications::{DetachAndPromptErr, NotifyTaskExt}, @@ -192,6 +193,12 @@ actions!( UnfoldDirectory, FoldDirectory, SelectParent, + SelectNextGitEntry, + SelectPrevGitEntry, + SelectNextDiagnostic, + SelectPrevDiagnostic, + SelectNextDirectory, + SelectPrevDirectory, ] ); @@ -1489,6 +1496,176 @@ impl ProjectPanel { } } + fn select_prev_diagnostic(&mut self, _: &SelectPrevDiagnostic, cx: &mut ViewContext) { + let selection = self.find_entry( + self.selection.as_ref(), + true, + |entry, worktree_id| { + (self.selection.is_none() + || self.selection.is_some_and(|selection| { + if selection.worktree_id == worktree_id { + selection.entry_id != entry.id + } else { + true + } + })) + && entry.is_file() + && self + .diagnostics + .contains_key(&(worktree_id, entry.path.to_path_buf())) + }, + cx, + ); + + if let Some(selection) = selection { + self.selection = Some(selection); + self.expand_entry(selection.worktree_id, selection.entry_id, cx); + self.update_visible_entries(Some((selection.worktree_id, selection.entry_id)), cx); + self.autoscroll(cx); + cx.notify(); + } + } + + fn select_next_diagnostic(&mut self, _: &SelectNextDiagnostic, cx: &mut ViewContext) { + let selection = self.find_entry( + self.selection.as_ref(), + false, + |entry, worktree_id| { + (self.selection.is_none() + || self.selection.is_some_and(|selection| { + if selection.worktree_id == worktree_id { + selection.entry_id != entry.id + } else { + true + } + })) + && entry.is_file() + && self + .diagnostics + .contains_key(&(worktree_id, entry.path.to_path_buf())) + }, + cx, + ); + + if let Some(selection) = selection { + self.selection = Some(selection); + self.expand_entry(selection.worktree_id, selection.entry_id, cx); + self.update_visible_entries(Some((selection.worktree_id, selection.entry_id)), cx); + self.autoscroll(cx); + cx.notify(); + } + } + + fn select_prev_git_entry(&mut self, _: &SelectPrevGitEntry, cx: &mut ViewContext) { + let selection = self.find_entry( + self.selection.as_ref(), + true, + |entry, worktree_id| { + (self.selection.is_none() + || self.selection.is_some_and(|selection| { + if selection.worktree_id == worktree_id { + selection.entry_id != entry.id + } else { + true + } + })) + && entry.is_file() + && entry + .git_status + .is_some_and(|status| matches!(status, GitFileStatus::Modified)) + }, + cx, + ); + + if let Some(selection) = selection { + self.selection = Some(selection); + self.expand_entry(selection.worktree_id, selection.entry_id, cx); + self.update_visible_entries(Some((selection.worktree_id, selection.entry_id)), cx); + self.autoscroll(cx); + cx.notify(); + } + } + + fn select_prev_directory(&mut self, _: &SelectPrevDirectory, cx: &mut ViewContext) { + let selection = self.find_visible_entry( + self.selection.as_ref(), + true, + |entry, worktree_id| { + (self.selection.is_none() + || self.selection.is_some_and(|selection| { + if selection.worktree_id == worktree_id { + selection.entry_id != entry.id + } else { + true + } + })) + && entry.is_dir() + }, + cx, + ); + + if let Some(selection) = selection { + self.selection = Some(selection); + self.autoscroll(cx); + cx.notify(); + } + } + + fn select_next_directory(&mut self, _: &SelectNextDirectory, cx: &mut ViewContext) { + let selection = self.find_visible_entry( + self.selection.as_ref(), + false, + |entry, worktree_id| { + (self.selection.is_none() + || self.selection.is_some_and(|selection| { + if selection.worktree_id == worktree_id { + selection.entry_id != entry.id + } else { + true + } + })) + && entry.is_dir() + }, + cx, + ); + + if let Some(selection) = selection { + self.selection = Some(selection); + self.autoscroll(cx); + cx.notify(); + } + } + + fn select_next_git_entry(&mut self, _: &SelectNextGitEntry, cx: &mut ViewContext) { + let selection = self.find_entry( + self.selection.as_ref(), + true, + |entry, worktree_id| { + (self.selection.is_none() + || self.selection.is_some_and(|selection| { + if selection.worktree_id == worktree_id { + selection.entry_id != entry.id + } else { + true + } + })) + && entry.is_file() + && entry + .git_status + .is_some_and(|status| matches!(status, GitFileStatus::Modified)) + }, + cx, + ); + + if let Some(selection) = selection { + self.selection = Some(selection); + self.expand_entry(selection.worktree_id, selection.entry_id, cx); + self.update_visible_entries(Some((selection.worktree_id, selection.entry_id)), cx); + self.autoscroll(cx); + cx.notify(); + } + } + fn select_parent(&mut self, _: &SelectParent, cx: &mut ViewContext) { if let Some((worktree, entry)) = self.selected_sub_entry(cx) { if let Some(parent) = entry.path.parent() { @@ -2705,6 +2882,232 @@ impl ProjectPanel { } } + fn find_entry_in_worktree( + &self, + worktree_id: WorktreeId, + reverse_search: bool, + only_visible_entries: bool, + predicate: impl Fn(&Entry, WorktreeId) -> bool, + cx: &mut ViewContext, + ) -> Option { + if only_visible_entries { + let entries = self + .visible_entries + .iter() + .find_map(|(tree_id, entries, _)| { + if worktree_id == *tree_id { + Some(entries) + } else { + None + } + })? + .clone(); + + return utils::ReversibleIterable::new(entries.iter(), reverse_search) + .find(|ele| predicate(ele, worktree_id)) + .cloned(); + } + + let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?; + worktree.update(cx, |tree, _| { + utils::ReversibleIterable::new(tree.entries(true, 0usize), reverse_search) + .find_single_ended(|ele| predicate(ele, worktree_id)) + .cloned() + }) + } + + fn find_entry( + &self, + start: Option<&SelectedEntry>, + reverse_search: bool, + predicate: impl Fn(&Entry, WorktreeId) -> bool, + cx: &mut ViewContext, + ) -> Option { + let mut worktree_ids: Vec<_> = self + .visible_entries + .iter() + .map(|(worktree_id, _, _)| *worktree_id) + .collect(); + + let mut last_found: Option = None; + + if let Some(start) = start { + let worktree = self + .project + .read(cx) + .worktree_for_id(start.worktree_id, cx)?; + + let search = worktree.update(cx, |tree, _| { + let entry = tree.entry_for_id(start.entry_id)?; + let root_entry = tree.root_entry()?; + let tree_id = tree.id(); + + let mut first_iter = tree.traverse_from_path(true, true, true, entry.path.as_ref()); + + if reverse_search { + first_iter.next(); + } + + let first = first_iter + .enumerate() + .take_until(|(count, ele)| *ele == root_entry && *count != 0usize) + .map(|(_, ele)| ele) + .find(|ele| predicate(ele, tree_id)) + .cloned(); + + let second_iter = tree.entries(true, 0usize); + + let second = if reverse_search { + second_iter + .take_until(|ele| ele.id == start.entry_id) + .filter(|ele| predicate(ele, tree_id)) + .last() + .cloned() + } else { + second_iter + .take_while(|ele| ele.id != start.entry_id) + .filter(|ele| predicate(ele, tree_id)) + .last() + .cloned() + }; + + if reverse_search { + Some((second, first)) + } else { + Some((first, second)) + } + }); + + if let Some((first, second)) = search { + let first = first.map(|entry| SelectedEntry { + worktree_id: start.worktree_id, + entry_id: entry.id, + }); + + let second = second.map(|entry| SelectedEntry { + worktree_id: start.worktree_id, + entry_id: entry.id, + }); + + if first.is_some() { + return first; + } + last_found = second; + + let idx = worktree_ids + .iter() + .enumerate() + .find(|(_, ele)| **ele == start.worktree_id) + .map(|(idx, _)| idx); + + if let Some(idx) = idx { + worktree_ids.rotate_left(idx + 1usize); + worktree_ids.pop(); + } + } + } + + for tree_id in worktree_ids.into_iter() { + if let Some(found) = + self.find_entry_in_worktree(tree_id, reverse_search, false, &predicate, cx) + { + return Some(SelectedEntry { + worktree_id: tree_id, + entry_id: found.id, + }); + } + } + + last_found + } + + fn find_visible_entry( + &self, + start: Option<&SelectedEntry>, + reverse_search: bool, + predicate: impl Fn(&Entry, WorktreeId) -> bool, + cx: &mut ViewContext, + ) -> Option { + let mut worktree_ids: Vec<_> = self + .visible_entries + .iter() + .map(|(worktree_id, _, _)| *worktree_id) + .collect(); + + let mut last_found: Option = None; + + if let Some(start) = start { + let entries = self + .visible_entries + .iter() + .find(|(worktree_id, _, _)| *worktree_id == start.worktree_id) + .map(|(_, entries, _)| entries)?; + + let mut start_idx = entries + .iter() + .enumerate() + .find(|(_, ele)| ele.id == start.entry_id) + .map(|(idx, _)| idx)?; + + if reverse_search { + start_idx = start_idx.saturating_add(1usize); + } + + let (left, right) = entries.split_at_checked(start_idx)?; + + let (first_iter, second_iter) = if reverse_search { + ( + utils::ReversibleIterable::new(left.iter(), reverse_search), + utils::ReversibleIterable::new(right.iter(), reverse_search), + ) + } else { + ( + utils::ReversibleIterable::new(right.iter(), reverse_search), + utils::ReversibleIterable::new(left.iter(), reverse_search), + ) + }; + + let first_search = first_iter.find(|ele| predicate(ele, start.worktree_id)); + let second_search = second_iter.find(|ele| predicate(ele, start.worktree_id)); + + if first_search.is_some() { + return first_search.map(|entry| SelectedEntry { + worktree_id: start.worktree_id, + entry_id: entry.id, + }); + } + + last_found = second_search.map(|entry| SelectedEntry { + worktree_id: start.worktree_id, + entry_id: entry.id, + }); + + let idx = worktree_ids + .iter() + .enumerate() + .find(|(_, ele)| **ele == start.worktree_id) + .map(|(idx, _)| idx); + + if let Some(idx) = idx { + worktree_ids.rotate_left(idx + 1usize); + worktree_ids.pop(); + } + } + + for tree_id in worktree_ids.into_iter() { + if let Some(found) = + self.find_entry_in_worktree(tree_id, reverse_search, true, &predicate, cx) + { + return Some(SelectedEntry { + worktree_id: tree_id, + entry_id: found.id, + }); + } + } + + last_found + } + fn calculate_depth_and_difference( entry: &Entry, visible_worktree_entries: &HashSet>, @@ -3482,6 +3885,12 @@ impl Render for ProjectPanel { .on_action(cx.listener(Self::select_first)) .on_action(cx.listener(Self::select_last)) .on_action(cx.listener(Self::select_parent)) + .on_action(cx.listener(Self::select_next_git_entry)) + .on_action(cx.listener(Self::select_prev_git_entry)) + .on_action(cx.listener(Self::select_next_diagnostic)) + .on_action(cx.listener(Self::select_prev_diagnostic)) + .on_action(cx.listener(Self::select_next_directory)) + .on_action(cx.listener(Self::select_prev_directory)) .on_action(cx.listener(Self::expand_selected_entry)) .on_action(cx.listener(Self::collapse_selected_entry)) .on_action(cx.listener(Self::collapse_all_entries)) @@ -5606,6 +6015,107 @@ mod tests { ); } + #[gpui::test] + async fn test_select_directory(cx: &mut gpui::TestAppContext) { + init_test_with_editor(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/project_root", + json!({ + "dir_1": { + "nested_dir": { + "file_a.py": "# File contents", + } + }, + "file_1.py": "# File contents", + "dir_2": { + + }, + "dir_3": { + + }, + "file_2.py": "# File contents", + "dir_4": { + + }, + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/project_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(); + + panel.update(cx, |panel, cx| panel.open(&Open, cx)); + cx.executor().run_until_parked(); + select_path(&panel, "project_root/dir_1", cx); + cx.executor().run_until_parked(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v project_root", + " > dir_1 <== selected", + " > dir_2", + " > dir_3", + " > dir_4", + " file_1.py", + " file_2.py", + ] + ); + panel.update(cx, |panel, cx| { + panel.select_prev_directory(&SelectPrevDirectory, cx) + }); + + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v project_root <== selected", + " > dir_1", + " > dir_2", + " > dir_3", + " > dir_4", + " file_1.py", + " file_2.py", + ] + ); + + panel.update(cx, |panel, cx| { + panel.select_prev_directory(&SelectPrevDirectory, cx) + }); + + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v project_root", + " > dir_1", + " > dir_2", + " > dir_3", + " > dir_4 <== selected", + " file_1.py", + " file_2.py", + ] + ); + + panel.update(cx, |panel, cx| { + panel.select_next_directory(&SelectNextDirectory, cx) + }); + + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v project_root <== selected", + " > dir_1", + " > dir_2", + " > dir_3", + " > dir_4", + " file_1.py", + " file_2.py", + ] + ); + } + #[gpui::test] async fn test_dir_toggle_collapse(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); diff --git a/crates/project_panel/src/utils.rs b/crates/project_panel/src/utils.rs new file mode 100644 index 0000000000..486def9b84 --- /dev/null +++ b/crates/project_panel/src/utils.rs @@ -0,0 +1,42 @@ +pub(crate) struct ReversibleIterable { + pub(crate) it: It, + pub(crate) reverse: bool, +} + +impl ReversibleIterable { + pub(crate) fn new(it: T, reverse: bool) -> Self { + Self { it, reverse } + } +} + +impl ReversibleIterable +where + It: Iterator, +{ + pub(crate) fn find_single_ended(mut self, pred: F) -> Option + where + F: FnMut(&Item) -> bool, + { + if self.reverse { + self.it.filter(pred).last() + } else { + self.it.find(pred) + } + } +} + +impl ReversibleIterable +where + It: DoubleEndedIterator, +{ + pub(crate) fn find(mut self, mut pred: F) -> Option + where + F: FnMut(&Item) -> bool, + { + if self.reverse { + self.it.rfind(|x| pred(x)) + } else { + self.it.find(|x| pred(x)) + } + } +} From 92dea066dd88f567130ce9b5f47967f9ff44ada7 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Thu, 5 Dec 2024 09:33:46 -0500 Subject: [PATCH 299/886] Extend filtering of backtrace frames a bit (#21573) Both rust_begin_unwind and _rust_begin_unwind appear in practice, not sure why. Release Notes: - N/A --- crates/zed/src/reliability.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/zed/src/reliability.rs b/crates/zed/src/reliability.rs index 681cc9834f..837db9df60 100644 --- a/crates/zed/src/reliability.rs +++ b/crates/zed/src/reliability.rs @@ -85,7 +85,7 @@ pub fn init_panic_hook( // Strip out leading stack frames for rust panic-handling. if let Some(ix) = backtrace .iter() - .position(|name| name == "rust_begin_unwind") + .position(|name| name == "rust_begin_unwind" || name == "_rust_begin_unwind") { backtrace.drain(0..=ix); } From 6ebd6c28931640ae71123ecd052b78907115e421 Mon Sep 17 00:00:00 2001 From: Nils Koch Date: Thu, 5 Dec 2024 15:43:04 +0100 Subject: [PATCH 300/886] Show error and warning indicators in tabs (#21383) Closes #21179 Release Notes: - Add setting to display error and warning indicators in tabs. demo_with_icons demo_without_icons --- Cargo.lock | 1 - assets/settings/default.json | 12 ++- crates/workspace/Cargo.toml | 1 - crates/workspace/src/item.rs | 15 ++++ crates/workspace/src/pane.rs | 140 ++++++++++++++++++++++++++--------- 5 files changed, 131 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6a6bbc36aa..be4e11263d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15195,7 +15195,6 @@ dependencies = [ "env_logger 0.11.5", "fs", "futures 0.3.31", - "git", "gpui", "http_client", "itertools 0.13.0", diff --git a/assets/settings/default.json b/assets/settings/default.json index db3b7130e0..dd9098e0c0 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -567,7 +567,17 @@ // "History" // 2. Activate the neighbour tab (prefers the right one, if present) // "Neighbour" - "activate_on_close": "history" + "activate_on_close": "history", + /// Which files containing diagnostic errors/warnings to mark in the tabs. + /// This setting can take the following three values: + /// + /// 1. Do not mark any files: + /// "off" + /// 2. Only mark files with errors: + /// "errors" + /// 3. Mark files with errors and warnings: + /// "all" + "show_diagnostics": "all" }, // Settings related to preview tabs. "preview_tabs": { diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 1fa4db2af8..3b17ed8dab 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -38,7 +38,6 @@ db.workspace = true derive_more.workspace = true fs.workspace = true futures.workspace = true -git.workspace = true gpui.workspace = true http_client.workspace = true itertools.workspace = true diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index eab3ddc755..97c27b52a1 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -42,6 +42,7 @@ pub struct ItemSettings { pub close_position: ClosePosition, pub activate_on_close: ActivateOnClose, pub file_icons: bool, + pub show_diagnostics: ShowDiagnostics, pub always_show_close_button: bool, } @@ -60,6 +61,15 @@ pub enum ClosePosition { Right, } +#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ShowDiagnostics { + Off, + Errors, + #[default] + All, +} + #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "lowercase")] pub enum ActivateOnClose { @@ -86,6 +96,11 @@ pub struct ItemSettingsContent { /// /// Default: history pub activate_on_close: Option, + /// Which files containing diagnostic errors/warnings to mark in the tabs. + /// This setting can take the following three values: + /// + /// Default: all + show_diagnostics: Option, /// Whether to always show the close button on tabs. /// /// Default: false diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index a2c63addd8..c0a80cc943 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1,7 +1,7 @@ use crate::{ item::{ ActivateOnClose, ClosePosition, Item, ItemHandle, ItemSettings, PreviewTabsSettings, - TabContentParams, WeakItemHandle, + ShowDiagnostics, TabContentParams, WeakItemHandle, }, move_item, notifications::NotifyResultExt, @@ -13,7 +13,6 @@ use crate::{ use anyhow::Result; use collections::{BTreeSet, HashMap, HashSet, VecDeque}; use futures::{stream::FuturesUnordered, StreamExt}; -use git::repository::GitFileStatus; use gpui::{ actions, anchored, deferred, impl_actions, prelude::*, Action, AnchorCorner, AnyElement, AppContext, AsyncWindowContext, ClickEvent, ClipboardItem, Div, DragMoveEvent, EntityId, @@ -23,6 +22,7 @@ use gpui::{ WindowContext, }; use itertools::Itertools; +use language::DiagnosticSeverity; use parking_lot::Mutex; use project::{Project, ProjectEntryId, ProjectPath, WorktreeId}; use serde::Deserialize; @@ -39,10 +39,10 @@ use std::{ }, }; use theme::ThemeSettings; - use ui::{ - prelude::*, right_click_menu, ButtonSize, Color, IconButton, IconButtonShape, IconName, - IconSize, Indicator, Label, PopoverMenu, PopoverMenuHandle, Tab, TabBar, TabPosition, Tooltip, + prelude::*, right_click_menu, ButtonSize, Color, DecoratedIcon, IconButton, IconButtonShape, + IconDecoration, IconDecorationKind, IconName, IconSize, Indicator, Label, PopoverMenu, + PopoverMenuHandle, Tab, TabBar, TabPosition, Tooltip, }; use ui::{v_flex, ContextMenu}; use util::{debug_panic, maybe, truncate_and_remove_front, ResultExt}; @@ -305,6 +305,7 @@ pub struct Pane { pub new_item_context_menu_handle: PopoverMenuHandle, pub split_item_context_menu_handle: PopoverMenuHandle, pinned_tab_count: usize, + diagnostics: HashMap, } pub struct ActivationHistoryEntry { @@ -381,6 +382,7 @@ impl Pane { cx.on_focus_in(&focus_handle, Pane::focus_in), cx.on_focus_out(&focus_handle, Pane::focus_out), cx.observe_global::(Self::settings_changed), + cx.subscribe(&project, Self::project_events), ]; let handle = cx.view().downgrade(); @@ -504,6 +506,7 @@ impl Pane { split_item_context_menu_handle: Default::default(), new_item_context_menu_handle: Default::default(), pinned_tab_count: 0, + diagnostics: Default::default(), } } @@ -598,6 +601,47 @@ impl Pane { cx.notify(); } + fn project_events( + this: &mut Pane, + _project: Model, + event: &project::Event, + cx: &mut ViewContext, + ) { + match event { + project::Event::DiskBasedDiagnosticsFinished { .. } + | project::Event::DiagnosticsUpdated { .. } => { + if ItemSettings::get_global(cx).show_diagnostics != ShowDiagnostics::Off { + this.update_diagnostics(cx); + cx.notify(); + } + } + _ => {} + } + } + + fn update_diagnostics(&mut self, cx: &mut ViewContext) { + let show_diagnostics = ItemSettings::get_global(cx).show_diagnostics; + self.diagnostics = if show_diagnostics != ShowDiagnostics::Off { + self.project + .read(cx) + .diagnostic_summaries(false, cx) + .filter_map(|(project_path, _, diagnostic_summary)| { + if diagnostic_summary.error_count > 0 { + Some((project_path, DiagnosticSeverity::ERROR)) + } else if diagnostic_summary.warning_count > 0 + && show_diagnostics != ShowDiagnostics::Errors + { + Some((project_path, DiagnosticSeverity::WARNING)) + } else { + None + } + }) + .collect::>() + } else { + Default::default() + } + } + fn settings_changed(&mut self, cx: &mut ViewContext) { if let Some(display_nav_history_buttons) = self.display_nav_history_buttons.as_mut() { *display_nav_history_buttons = TabBarSettings::get_global(cx).show_nav_history_buttons; @@ -605,6 +649,7 @@ impl Pane { if !PreviewTabsSettings::get_global(cx).enabled { self.preview_item_id = None; } + self.update_diagnostics(cx); cx.notify(); } @@ -1839,23 +1884,6 @@ impl Pane { } } - pub fn git_aware_icon_color( - git_status: Option, - ignored: bool, - selected: bool, - ) -> Color { - if ignored { - Color::Ignored - } else { - match git_status { - Some(GitFileStatus::Added) => Color::Created, - Some(GitFileStatus::Modified) => Color::Modified, - Some(GitFileStatus::Conflict) => Color::Conflict, - None => Self::icon_color(selected), - } - } - } - fn toggle_pin_tab(&mut self, _: &TogglePinTab, cx: &mut ViewContext<'_, Self>) { if self.items.is_empty() { return; @@ -1919,8 +1947,6 @@ impl Pane { focus_handle: &FocusHandle, cx: &mut ViewContext<'_, Pane>, ) -> impl IntoElement { - let project_path = item.project_path(cx); - let is_active = ix == self.active_item_index; let is_preview = self .preview_item_id @@ -1936,19 +1962,57 @@ impl Pane { cx, ); - let icon_color = if ItemSettings::get_global(cx).git_status { - project_path - .as_ref() - .and_then(|path| self.project.read(cx).entry_for_path(path, cx)) - .map(|entry| { - Self::git_aware_icon_color(entry.git_status, entry.is_ignored, is_active) - }) - .unwrap_or_else(|| Self::icon_color(is_active)) + let item_diagnostic = item + .project_path(cx) + .map_or(None, |project_path| self.diagnostics.get(&project_path)); + + let decorated_icon = item_diagnostic.map_or(None, |diagnostic| { + let icon = match item.tab_icon(cx) { + Some(icon) => icon, + None => return None, + }; + + let knockout_item_color = if is_active { + cx.theme().colors().tab_active_background + } else { + cx.theme().colors().tab_bar_background + }; + + let (icon_decoration, icon_color) = if matches!(diagnostic, &DiagnosticSeverity::ERROR) + { + (IconDecorationKind::X, Color::Error) + } else { + (IconDecorationKind::Triangle, Color::Warning) + }; + + Some(DecoratedIcon::new( + icon.size(IconSize::Small).color(Color::Muted), + Some( + IconDecoration::new(icon_decoration, knockout_item_color, cx) + .color(icon_color.color(cx)) + .position(Point { + x: px(-2.), + y: px(-2.), + }), + ), + )) + }); + + let icon = if decorated_icon.is_none() { + match item_diagnostic { + Some(&DiagnosticSeverity::ERROR) => { + Some(Icon::new(IconName::X).color(Color::Error)) + } + Some(&DiagnosticSeverity::WARNING) => { + Some(Icon::new(IconName::Triangle).color(Color::Warning)) + } + _ => item.tab_icon(cx).map(|icon| icon.color(Color::Muted)), + } + .map(|icon| icon.size(IconSize::Small)) } else { - Self::icon_color(is_active) + None }; - let icon = item.tab_icon(cx); let settings = ItemSettings::get_global(cx); let close_side = &settings.close_position; let always_show_close_button = settings.always_show_close_button; @@ -2078,7 +2142,13 @@ impl Pane { .child( h_flex() .gap_1() - .children(icon.map(|icon| icon.size(IconSize::Small).color(icon_color))) + .child(if let Some(decorated_icon) = decorated_icon { + div().child(decorated_icon.into_any_element()) + } else if let Some(icon) = icon { + div().child(icon.into_any_element()) + } else { + div() + }) .child(label), ); From 2d43ad12e6358c9a9b4c67bec201a6e13c09894e Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Thu, 5 Dec 2024 18:55:40 +0100 Subject: [PATCH 301/886] git: Make worktrees work for bare git repositories (#21596) Fixes #21210 by ensuring that Zed can open worktrees of bare git repositories. Co-authored-by: Peter Tripp --- crates/worktree/src/worktree.rs | 33 ++++++++++++++++++++++----- crates/worktree/src/worktree_tests.rs | 22 ++++++++++++++---- 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index a9762b942b..86981687ce 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -3110,12 +3110,8 @@ impl BackgroundScannerState { let repository = fs.open_repo(&dot_git_abs_path)?; let actual_repo_path = repository.path(); - let actual_dot_git_dir_abs_path: Arc = Arc::from( - actual_repo_path - .ancestors() - .find(|ancestor| ancestor.file_name() == Some(&*DOT_GIT))?, - ); + let actual_dot_git_dir_abs_path = smol::block_on(find_git_dir(&actual_repo_path, fs))?; watcher.add(&actual_repo_path).log_err()?; let dot_git_worktree_abs_path = if actual_dot_git_dir_abs_path.as_ref() == dot_git_abs_path @@ -3161,6 +3157,31 @@ impl BackgroundScannerState { } } +async fn is_git_dir(path: &Path, fs: &dyn Fs) -> bool { + if path.file_name() == Some(&*DOT_GIT) { + return true; + } + + // If we're in a bare repository, we are not inside a `.git` folder. In a + // bare repository, the root folder contains what would normally be in the + // `.git` folder. + let head_metadata = fs.metadata(&path.join("HEAD")).await; + if !matches!(head_metadata, Ok(Some(_))) { + return false; + } + let config_metadata = fs.metadata(&path.join("config")).await; + matches!(config_metadata, Ok(Some(_))) +} + +async fn find_git_dir(path: &Path, fs: &dyn Fs) -> Option> { + for ancestor in path.ancestors() { + if is_git_dir(ancestor, fs).await { + return Some(Arc::from(ancestor)); + } + } + None +} + async fn build_gitignore(abs_path: &Path, fs: &dyn Fs) -> Result { let contents = fs.load(abs_path).await?; let parent = abs_path.parent().unwrap_or_else(|| Path::new("/")); @@ -3967,7 +3988,7 @@ impl BackgroundScanner { } else if fsmonitor_parse_state == Some(FsMonitorParseState::Cookies) && file_name == Some(*FSMONITOR_DAEMON) { fsmonitor_parse_state = Some(FsMonitorParseState::FsMonitor); false - } else if fsmonitor_parse_state != Some(FsMonitorParseState::FsMonitor) && file_name == Some(*DOT_GIT) { + } else if fsmonitor_parse_state != Some(FsMonitorParseState::FsMonitor) && smol::block_on(is_git_dir(ancestor, self.fs.as_ref())) { true } else { fsmonitor_parse_state.take(); diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index fbedd896e3..121caf0b7b 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -12,7 +12,13 @@ use pretty_assertions::assert_eq; use rand::prelude::*; use serde_json::json; use settings::{Settings, SettingsStore}; -use std::{env, fmt::Write, mem, path::Path, sync::Arc}; +use std::{ + env, + fmt::Write, + mem, + path::{Path, PathBuf}, + sync::Arc, +}; use util::{test::temp_tree, ResultExt}; #[gpui::test] @@ -532,14 +538,20 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) { assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 1); }); + let path = PathBuf::from("/root/one/node_modules/c/lib"); + // No work happens when files and directories change within an unloaded directory. let prev_fs_call_count = fs.read_dir_call_count() + fs.metadata_call_count(); - fs.create_dir("/root/one/node_modules/c/lib".as_ref()) - .await - .unwrap(); + // When we open a directory, we check each ancestor whether it's a git + // repository. That means we have an fs.metadata call per ancestor that we + // need to subtract here. + let ancestors = path.ancestors().count(); + + fs.create_dir(path.as_ref()).await.unwrap(); cx.executor().run_until_parked(); + assert_eq!( - fs.read_dir_call_count() + fs.metadata_call_count() - prev_fs_call_count, + fs.read_dir_call_count() + fs.metadata_call_count() - prev_fs_call_count - ancestors, 0 ); } From 787c75cbda8b0ea3ad1bc036329f3826ae6b9e8f Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 5 Dec 2024 13:22:25 -0500 Subject: [PATCH 302/886] assistant2: Add thread history (#21599) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds support for thread history to the Assistant 2 panel. We also now generate summaries for the threads. Screenshot 2024-12-05 at 12 56 53 PM Screenshot 2024-12-05 at 12 56 58 PM Release Notes: - N/A --------- Co-authored-by: Piotr --- Cargo.lock | 3 + crates/assistant2/Cargo.toml | 3 + crates/assistant2/src/active_thread.rs | 9 +- crates/assistant2/src/assistant.rs | 1 + crates/assistant2/src/assistant_panel.rs | 224 ++++++++++++----------- crates/assistant2/src/thread.rs | 102 +++++++++-- crates/assistant2/src/thread_history.rs | 144 +++++++++++++++ crates/assistant2/src/thread_store.rs | 16 +- 8 files changed, 375 insertions(+), 127 deletions(-) create mode 100644 crates/assistant2/src/thread_history.rs diff --git a/Cargo.lock b/Cargo.lock index be4e11263d..bd3cd06dca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -456,6 +456,7 @@ version = "0.1.0" dependencies = [ "anyhow", "assistant_tool", + "chrono", "client", "collections", "command_palette_hooks", @@ -478,6 +479,8 @@ dependencies = [ "settings", "smol", "theme", + "time", + "time_format", "ui", "unindent", "util", diff --git a/crates/assistant2/Cargo.toml b/crates/assistant2/Cargo.toml index e5253adbce..b5f5fe8ecd 100644 --- a/crates/assistant2/Cargo.toml +++ b/crates/assistant2/Cargo.toml @@ -15,6 +15,7 @@ doctest = false [dependencies] anyhow.workspace = true assistant_tool.workspace = true +chrono.workspace = true client.workspace = true collections.workspace = true command_palette_hooks.workspace = true @@ -37,6 +38,8 @@ serde_json.workspace = true settings.workspace = true smol.workspace = true theme.workspace = true +time.workspace = true +time_format.workspace = true ui.workspace = true unindent.workspace = true util.workspace = true diff --git a/crates/assistant2/src/active_thread.rs b/crates/assistant2/src/active_thread.rs index 13b67dc437..d9cd8fcc46 100644 --- a/crates/assistant2/src/active_thread.rs +++ b/crates/assistant2/src/active_thread.rs @@ -3,8 +3,8 @@ use std::sync::Arc; use assistant_tool::ToolWorkingSet; use collections::HashMap; use gpui::{ - list, AnyElement, Empty, ListAlignment, ListState, Model, StyleRefinement, Subscription, - TextStyleRefinement, View, WeakView, + list, AnyElement, AppContext, Empty, ListAlignment, ListState, Model, StyleRefinement, + Subscription, TextStyleRefinement, View, WeakView, }; use language::LanguageRegistry; use language_model::Role; @@ -70,6 +70,10 @@ impl ActiveThread { self.messages.is_empty() } + pub fn summary(&self, cx: &AppContext) -> Option { + self.thread.read(cx).summary() + } + pub fn last_error(&self) -> Option { self.last_error.clone() } @@ -139,6 +143,7 @@ impl ActiveThread { self.last_error = Some(error.clone()); } ThreadEvent::StreamedCompletion => {} + ThreadEvent::SummaryChanged => {} ThreadEvent::StreamedAssistantText(message_id, text) => { if let Some(markdown) = self.rendered_messages_by_id.get_mut(&message_id) { markdown.update(cx, |markdown, cx| { diff --git a/crates/assistant2/src/assistant.rs b/crates/assistant2/src/assistant.rs index 13ac2d821b..3c8520680e 100644 --- a/crates/assistant2/src/assistant.rs +++ b/crates/assistant2/src/assistant.rs @@ -3,6 +3,7 @@ mod assistant_panel; mod context_picker; mod message_editor; mod thread; +mod thread_history; mod thread_store; use command_palette_hooks::CommandPaletteFilter; diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index d17480cd0e..2d9f563c2f 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -11,13 +11,15 @@ use gpui::{ use language::LanguageRegistry; use language_model::LanguageModelRegistry; use language_model_selector::LanguageModelSelector; -use ui::{prelude::*, ButtonLike, Divider, IconButtonShape, KeyBinding, ListItem, Tab, Tooltip}; +use time::UtcOffset; +use ui::{prelude::*, ButtonLike, Divider, IconButtonShape, KeyBinding, Tab, Tooltip}; use workspace::dock::{DockPosition, Panel, PanelEvent}; use workspace::Workspace; use crate::active_thread::ActiveThread; use crate::message_editor::MessageEditor; -use crate::thread::{Thread, ThreadError, ThreadId}; +use crate::thread::{ThreadError, ThreadId}; +use crate::thread_history::{PastThread, ThreadHistory}; use crate::thread_store::ThreadStore; use crate::{NewThread, OpenHistory, ToggleFocus, ToggleModelSelector}; @@ -32,13 +34,21 @@ pub fn init(cx: &mut AppContext) { .detach(); } +enum ActiveView { + Thread, + History, +} + pub struct AssistantPanel { workspace: WeakView, language_registry: Arc, thread_store: Model, - thread: Option>, + thread: View, message_editor: View, tools: Arc, + local_timezone: UtcOffset, + active_view: ActiveView, + history: View, } impl AssistantPanel { @@ -68,14 +78,31 @@ impl AssistantPanel { cx: &mut ViewContext, ) -> Self { let thread = thread_store.update(cx, |this, cx| this.create_thread(cx)); + let language_registry = workspace.project().read(cx).languages().clone(); + let workspace = workspace.weak_handle(); + let weak_self = cx.view().downgrade(); Self { - workspace: workspace.weak_handle(), - language_registry: workspace.project().read(cx).languages().clone(), - thread_store, - thread: None, - message_editor: cx.new_view(|cx| MessageEditor::new(thread, cx)), + active_view: ActiveView::Thread, + workspace: workspace.clone(), + language_registry: language_registry.clone(), + thread_store: thread_store.clone(), + thread: cx.new_view(|cx| { + ActiveThread::new( + thread.clone(), + workspace, + language_registry, + tools.clone(), + cx, + ) + }), + message_editor: cx.new_view(|cx| MessageEditor::new(thread.clone(), cx)), tools, + local_timezone: UtcOffset::from_whole_seconds( + chrono::Local::now().offset().local_minus_utc(), + ) + .unwrap(), + history: cx.new_view(|cx| ThreadHistory::new(weak_self, thread_store, cx)), } } @@ -84,7 +111,8 @@ impl AssistantPanel { .thread_store .update(cx, |this, cx| this.create_thread(cx)); - self.thread = Some(cx.new_view(|cx| { + self.active_view = ActiveView::Thread; + self.thread = cx.new_view(|cx| { ActiveThread::new( thread.clone(), self.workspace.clone(), @@ -92,12 +120,12 @@ impl AssistantPanel { self.tools.clone(), cx, ) - })); + }); self.message_editor = cx.new_view(|cx| MessageEditor::new(thread, cx)); self.message_editor.focus_handle(cx).focus(cx); } - fn open_thread(&mut self, thread_id: &ThreadId, cx: &mut ViewContext) { + pub(crate) fn open_thread(&mut self, thread_id: &ThreadId, cx: &mut ViewContext) { let Some(thread) = self .thread_store .update(cx, |this, cx| this.open_thread(thread_id, cx)) @@ -105,7 +133,8 @@ impl AssistantPanel { return; }; - self.thread = Some(cx.new_view(|cx| { + self.active_view = ActiveView::Thread; + self.thread = cx.new_view(|cx| { ActiveThread::new( thread.clone(), self.workspace.clone(), @@ -113,15 +142,22 @@ impl AssistantPanel { self.tools.clone(), cx, ) - })); + }); self.message_editor = cx.new_view(|cx| MessageEditor::new(thread, cx)); self.message_editor.focus_handle(cx).focus(cx); } + + pub(crate) fn local_timezone(&self) -> UtcOffset { + self.local_timezone + } } impl FocusableView for AssistantPanel { fn focus_handle(&self, cx: &AppContext) -> FocusHandle { - self.message_editor.focus_handle(cx) + match self.active_view { + ActiveView::Thread => self.message_editor.focus_handle(cx), + ActiveView::History => self.history.focus_handle(cx), + } } } @@ -180,7 +216,7 @@ impl AssistantPanel { .bg(cx.theme().colors().tab_bar_background) .border_b_1() .border_color(cx.theme().colors().border_variant) - .child(h_flex().child(Label::new("Thread Title Goes Here"))) + .child(h_flex().children(self.thread.read(cx).summary(cx).map(Label::new))) .child( h_flex() .gap(DynamicSpacing::Base08.rems(cx)) @@ -291,15 +327,11 @@ impl AssistantPanel { } fn render_active_thread_or_empty_state(&self, cx: &mut ViewContext) -> AnyElement { - let Some(thread) = self.thread.as_ref() else { - return self.render_thread_empty_state(cx).into_any_element(); - }; - - if thread.read(cx).is_empty() { + if self.thread.read(cx).is_empty() { return self.render_thread_empty_state(cx).into_any_element(); } - thread.clone().into_any() + self.thread.clone().into_any() } fn render_thread_empty_state(&self, cx: &mut ViewContext) -> impl IntoElement { @@ -361,63 +393,41 @@ impl AssistantPanel { .child(Label::new("/src/components").size(LabelSize::Small)), ), ) - .child( - h_flex() - .w_full() - .justify_center() - .child(Label::new("Recent Threads:").size(LabelSize::Small)), - ) - .child( - v_flex().gap_2().children( - recent_threads - .into_iter() - .map(|thread| self.render_past_thread(thread, cx)), - ), - ) - .child( - h_flex().w_full().justify_center().child( - Button::new("view-all-past-threads", "View All Past Threads") - .style(ButtonStyle::Subtle) - .label_size(LabelSize::Small) - .key_binding(KeyBinding::for_action_in( - &OpenHistory, - &self.focus_handle(cx), - cx, - )) - .on_click(move |_event, cx| { - cx.dispatch_action(OpenHistory.boxed_clone()); - }), - ), - ) - } - - fn render_past_thread( - &self, - thread: Model, - cx: &mut ViewContext, - ) -> impl IntoElement { - let id = thread.read(cx).id().clone(); - - ListItem::new(("past-thread", thread.entity_id())) - .start_slot(Icon::new(IconName::MessageBubbles)) - .child(Label::new(format!("Thread {id}"))) - .end_slot( - h_flex() - .gap_2() - .child(Label::new("1 hour ago").color(Color::Disabled)) + .when(!recent_threads.is_empty(), |parent| { + parent .child( - IconButton::new("delete", IconName::TrashAlt) - .shape(IconButtonShape::Square) - .icon_size(IconSize::Small), - ), - ) - .on_click(cx.listener(move |this, _event, cx| { - this.open_thread(&id, cx); - })) + h_flex() + .w_full() + .justify_center() + .child(Label::new("Recent Threads:").size(LabelSize::Small)), + ) + .child( + v_flex().gap_2().children( + recent_threads + .into_iter() + .map(|thread| PastThread::new(thread, cx.view().downgrade())), + ), + ) + .child( + h_flex().w_full().justify_center().child( + Button::new("view-all-past-threads", "View All Past Threads") + .style(ButtonStyle::Subtle) + .label_size(LabelSize::Small) + .key_binding(KeyBinding::for_action_in( + &OpenHistory, + &self.focus_handle(cx), + cx, + )) + .on_click(move |_event, cx| { + cx.dispatch_action(OpenHistory.boxed_clone()); + }), + ), + ) + }) } fn render_last_error(&self, cx: &mut ViewContext) -> Option { - let last_error = self.thread.as_ref()?.read(cx).last_error()?; + let last_error = self.thread.read(cx).last_error()?; Some( div() @@ -467,11 +477,9 @@ impl AssistantPanel { .mt_1() .child(Button::new("subscribe", "Subscribe").on_click(cx.listener( |this, _, cx| { - if let Some(thread) = this.thread.as_ref() { - thread.update(cx, |this, _cx| { - this.clear_last_error(); - }); - } + this.thread.update(cx, |this, _cx| { + this.clear_last_error(); + }); cx.open_url(&zed_urls::account_url(cx)); cx.notify(); @@ -479,11 +487,9 @@ impl AssistantPanel { ))) .child(Button::new("dismiss", "Dismiss").on_click(cx.listener( |this, _, cx| { - if let Some(thread) = this.thread.as_ref() { - thread.update(cx, |this, _cx| { - this.clear_last_error(); - }); - } + this.thread.update(cx, |this, _cx| { + this.clear_last_error(); + }); cx.notify(); }, @@ -518,11 +524,9 @@ impl AssistantPanel { .child( Button::new("subscribe", "Update Monthly Spend Limit").on_click( cx.listener(|this, _, cx| { - if let Some(thread) = this.thread.as_ref() { - thread.update(cx, |this, _cx| { - this.clear_last_error(); - }); - } + this.thread.update(cx, |this, _cx| { + this.clear_last_error(); + }); cx.open_url(&zed_urls::account_url(cx)); cx.notify(); @@ -531,11 +535,9 @@ impl AssistantPanel { ) .child(Button::new("dismiss", "Dismiss").on_click(cx.listener( |this, _, cx| { - if let Some(thread) = this.thread.as_ref() { - thread.update(cx, |this, _cx| { - this.clear_last_error(); - }); - } + this.thread.update(cx, |this, _cx| { + this.clear_last_error(); + }); cx.notify(); }, @@ -574,11 +576,9 @@ impl AssistantPanel { .mt_1() .child(Button::new("dismiss", "Dismiss").on_click(cx.listener( |this, _, cx| { - if let Some(thread) = this.thread.as_ref() { - thread.update(cx, |this, _cx| { - this.clear_last_error(); - }); - } + this.thread.update(cx, |this, _cx| { + this.clear_last_error(); + }); cx.notify(); }, @@ -597,17 +597,23 @@ impl Render for AssistantPanel { .on_action(cx.listener(|this, _: &NewThread, cx| { this.new_thread(cx); })) - .on_action(cx.listener(|_this, _: &OpenHistory, _cx| { - println!("Open History"); + .on_action(cx.listener(|this, _: &OpenHistory, cx| { + this.active_view = ActiveView::History; + this.history.focus_handle(cx).focus(cx); + cx.notify(); })) .child(self.render_toolbar(cx)) - .child(self.render_active_thread_or_empty_state(cx)) - .child( - h_flex() - .border_t_1() - .border_color(cx.theme().colors().border_variant) - .child(self.message_editor.clone()), - ) - .children(self.render_last_error(cx)) + .map(|parent| match self.active_view { + ActiveView::Thread => parent + .child(self.render_active_thread_or_empty_state(cx)) + .child( + h_flex() + .border_t_1() + .border_color(cx.theme().colors().border_variant) + .child(self.message_editor.clone()), + ) + .children(self.render_last_error(cx)), + ActiveView::History => parent.child(self.history.clone()), + }) } } diff --git a/crates/assistant2/src/thread.rs b/crates/assistant2/src/thread.rs index 185719fa98..833f8c9b03 100644 --- a/crates/assistant2/src/thread.rs +++ b/crates/assistant2/src/thread.rs @@ -2,18 +2,19 @@ use std::sync::Arc; use anyhow::Result; use assistant_tool::ToolWorkingSet; +use chrono::{DateTime, Utc}; use collections::HashMap; use futures::future::Shared; use futures::{FutureExt as _, StreamExt as _}; use gpui::{AppContext, EventEmitter, ModelContext, SharedString, Task}; use language_model::{ - LanguageModel, LanguageModelCompletionEvent, LanguageModelRequest, LanguageModelRequestMessage, - LanguageModelToolResult, LanguageModelToolUse, LanguageModelToolUseId, MessageContent, Role, - StopReason, + LanguageModel, LanguageModelCompletionEvent, LanguageModelRegistry, LanguageModelRequest, + LanguageModelRequestMessage, LanguageModelToolResult, LanguageModelToolUse, + LanguageModelToolUseId, MessageContent, Role, StopReason, }; use language_models::provider::cloud::{MaxMonthlySpendReachedError, PaymentRequiredError}; use serde::{Deserialize, Serialize}; -use util::post_inc; +use util::{post_inc, TryFutureExt as _}; use uuid::Uuid; #[derive(Debug, Clone, Copy)] @@ -56,6 +57,9 @@ pub struct Message { /// A thread of conversation with the LLM. pub struct Thread { id: ThreadId, + updated_at: DateTime, + summary: Option, + pending_summary: Task>, messages: Vec, next_message_id: MessageId, completion_count: usize, @@ -70,6 +74,9 @@ impl Thread { pub fn new(tools: Arc, _cx: &mut ModelContext) -> Self { Self { id: ThreadId::new(), + updated_at: Utc::now(), + summary: None, + pending_summary: Task::ready(None), messages: Vec::new(), next_message_id: MessageId(0), completion_count: 0, @@ -89,6 +96,23 @@ impl Thread { self.messages.is_empty() } + pub fn updated_at(&self) -> DateTime { + self.updated_at + } + + pub fn touch_updated_at(&mut self) { + self.updated_at = Utc::now(); + } + + pub fn summary(&self) -> Option { + self.summary.clone() + } + + pub fn set_summary(&mut self, summary: impl Into, cx: &mut ModelContext) { + self.summary = Some(summary.into()); + cx.emit(ThreadEvent::SummaryChanged); + } + pub fn message(&self, id: MessageId) -> Option<&Message> { self.messages.iter().find(|message| message.id == id) } @@ -121,6 +145,7 @@ impl Thread { role, text: text.into(), }); + self.touch_updated_at(); cx.emit(ThreadEvent::MessageAdded(id)); } @@ -191,13 +216,7 @@ impl Thread { thread.update(&mut cx, |thread, cx| { match event { LanguageModelCompletionEvent::StartMessage { .. } => { - let id = thread.next_message_id.post_inc(); - thread.messages.push(Message { - id, - role: Role::Assistant, - text: String::new(), - }); - cx.emit(ThreadEvent::MessageAdded(id)); + thread.insert_message(Role::Assistant, String::new(), cx); } LanguageModelCompletionEvent::Stop(reason) => { stop_reason = reason; @@ -239,6 +258,7 @@ impl Thread { } } + thread.touch_updated_at(); cx.emit(ThreadEvent::StreamedCompletion); cx.notify(); })?; @@ -246,10 +266,14 @@ impl Thread { smol::future::yield_now().await; } - thread.update(&mut cx, |thread, _cx| { + thread.update(&mut cx, |thread, cx| { thread .pending_completions .retain(|completion| completion.id != pending_completion_id); + + if thread.summary.is_none() && thread.messages.len() >= 2 { + thread.summarize(cx); + } })?; anyhow::Ok(stop_reason) @@ -292,6 +316,59 @@ impl Thread { }); } + pub fn summarize(&mut self, cx: &mut ModelContext) { + let Some(provider) = LanguageModelRegistry::read_global(cx).active_provider() else { + return; + }; + let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else { + return; + }; + + if !provider.is_authenticated(cx) { + return; + } + + let mut request = self.to_completion_request(RequestKind::Chat, cx); + request.messages.push(LanguageModelRequestMessage { + role: Role::User, + content: vec![ + "Generate a concise 3-7 word title for this conversation, omitting punctuation. Go straight to the title, without any preamble and prefix like `Here's a concise suggestion:...` or `Title:`" + .into(), + ], + cache: false, + }); + + self.pending_summary = cx.spawn(|this, mut cx| { + async move { + let stream = model.stream_completion_text(request, &cx); + let mut messages = stream.await?; + + let mut new_summary = String::new(); + while let Some(message) = messages.stream.next().await { + let text = message?; + let mut lines = text.lines(); + new_summary.extend(lines.next()); + + // Stop if the LLM generated multiple lines. + if lines.next().is_some() { + break; + } + } + + this.update(&mut cx, |this, cx| { + if !new_summary.is_empty() { + this.summary = Some(new_summary.into()); + } + + cx.emit(ThreadEvent::SummaryChanged); + })?; + + anyhow::Ok(()) + } + .log_err() + }); + } + pub fn insert_tool_output( &mut self, assistant_message_id: MessageId, @@ -365,6 +442,7 @@ pub enum ThreadEvent { StreamedCompletion, StreamedAssistantText(MessageId, String), MessageAdded(MessageId), + SummaryChanged, UsePendingTools, ToolFinished { #[allow(unused)] diff --git a/crates/assistant2/src/thread_history.rs b/crates/assistant2/src/thread_history.rs new file mode 100644 index 0000000000..7216ca695a --- /dev/null +++ b/crates/assistant2/src/thread_history.rs @@ -0,0 +1,144 @@ +use gpui::{ + uniform_list, AppContext, FocusHandle, FocusableView, Model, UniformListScrollHandle, WeakView, +}; +use time::{OffsetDateTime, UtcOffset}; +use ui::{prelude::*, IconButtonShape, ListItem}; + +use crate::thread::Thread; +use crate::thread_store::ThreadStore; +use crate::AssistantPanel; + +pub struct ThreadHistory { + focus_handle: FocusHandle, + assistant_panel: WeakView, + thread_store: Model, + scroll_handle: UniformListScrollHandle, +} + +impl ThreadHistory { + pub(crate) fn new( + assistant_panel: WeakView, + thread_store: Model, + cx: &mut ViewContext, + ) -> Self { + Self { + focus_handle: cx.focus_handle(), + assistant_panel, + thread_store, + scroll_handle: UniformListScrollHandle::default(), + } + } +} + +impl FocusableView for ThreadHistory { + fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl Render for ThreadHistory { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let threads = self.thread_store.update(cx, |this, cx| this.threads(cx)); + + v_flex() + .id("thread-history-container") + .track_focus(&self.focus_handle) + .overflow_y_scroll() + .size_full() + .p_1() + .map(|history| { + if threads.is_empty() { + history + .justify_center() + .child( + h_flex().w_full().justify_center().child( + Label::new("You don't have any past threads yet.") + .size(LabelSize::Small), + ), + ) + } else { + history.child( + uniform_list( + cx.view().clone(), + "thread-history", + threads.len(), + move |history, range, _cx| { + threads[range] + .iter() + .map(|thread| { + PastThread::new( + thread.clone(), + history.assistant_panel.clone(), + ) + }) + .collect() + }, + ) + .track_scroll(self.scroll_handle.clone()) + .flex_grow(), + ) + } + }) + } +} + +#[derive(IntoElement)] +pub struct PastThread { + thread: Model, + assistant_panel: WeakView, +} + +impl PastThread { + pub fn new(thread: Model, assistant_panel: WeakView) -> Self { + Self { + thread, + assistant_panel, + } + } +} + +impl RenderOnce for PastThread { + fn render(self, cx: &mut WindowContext) -> impl IntoElement { + let (id, summary) = { + const DEFAULT_SUMMARY: SharedString = SharedString::new_static("New Thread"); + let thread = self.thread.read(cx); + ( + thread.id().clone(), + thread.summary().unwrap_or(DEFAULT_SUMMARY), + ) + }; + + let thread_timestamp = time_format::format_localized_timestamp( + OffsetDateTime::from_unix_timestamp(self.thread.read(cx).updated_at().timestamp()) + .unwrap(), + OffsetDateTime::now_utc(), + self.assistant_panel + .update(cx, |this, _cx| this.local_timezone()) + .unwrap_or(UtcOffset::UTC), + time_format::TimestampFormat::EnhancedAbsolute, + ); + ListItem::new(("past-thread", self.thread.entity_id())) + .start_slot(Icon::new(IconName::MessageBubbles)) + .child(Label::new(summary)) + .end_slot( + h_flex() + .gap_2() + .child(Label::new(thread_timestamp).color(Color::Disabled)) + .child( + IconButton::new("delete", IconName::TrashAlt) + .shape(IconButtonShape::Square) + .icon_size(IconSize::Small), + ), + ) + .on_click({ + let assistant_panel = self.assistant_panel.clone(); + move |_event, cx| { + assistant_panel + .update(cx, |this, cx| { + this.open_thread(&id, cx); + }) + .ok(); + } + }) + } +} diff --git a/crates/assistant2/src/thread_store.rs b/crates/assistant2/src/thread_store.rs index 80e6d29265..7ceee9306b 100644 --- a/crates/assistant2/src/thread_store.rs +++ b/crates/assistant2/src/thread_store.rs @@ -52,13 +52,19 @@ impl ThreadStore { }) } - pub fn recent_threads(&self, limit: usize, cx: &ModelContext) -> Vec> { - self.threads + pub fn threads(&self, cx: &ModelContext) -> Vec> { + let mut threads = self + .threads .iter() .filter(|thread| !thread.read(cx).is_empty()) - .take(limit) .cloned() - .collect() + .collect::>(); + threads.sort_unstable_by_key(|thread| std::cmp::Reverse(thread.read(cx).updated_at())); + threads + } + + pub fn recent_threads(&self, limit: usize, cx: &ModelContext) -> Vec> { + self.threads(cx).into_iter().take(limit).collect() } pub fn create_thread(&mut self, cx: &mut ModelContext) -> Model { @@ -148,6 +154,7 @@ impl ThreadStore { self.threads.push(cx.new_model(|cx| { let mut thread = Thread::new(self.tools.clone(), cx); + thread.set_summary("Introduction to quantum computing", cx); thread.insert_user_message("Hello! Can you help me understand quantum computing?", cx); thread.insert_message(Role::Assistant, "Of course! I'd be happy to help you understand quantum computing. Quantum computing is a fascinating field that uses the principles of quantum mechanics to process information. Unlike classical computers that use bits (0s and 1s), quantum computers use quantum bits or 'qubits'. These qubits can exist in multiple states simultaneously, a property called superposition. This allows quantum computers to perform certain calculations much faster than classical computers. What specific aspect of quantum computing would you like to know more about?", cx); thread.insert_user_message("That's interesting! Can you explain how quantum entanglement is used in quantum computing?", cx); @@ -157,6 +164,7 @@ impl ThreadStore { self.threads.push(cx.new_model(|cx| { let mut thread = Thread::new(self.tools.clone(), cx); + thread.set_summary("Rust web development and async programming", cx); thread.insert_user_message("Can you show me an example of Rust code for a simple web server?", cx); thread.insert_message(Role::Assistant, "Certainly! Here's an example of a simple web server in Rust using the `actix-web` framework: From 1efd165ead62cc00957cfd3a8c3da0cc4572e93b Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 5 Dec 2024 21:48:33 +0200 Subject: [PATCH 303/886] Restore project diff test (#21606) Restores a basic project diff test Release Notes: - N/A --------- Co-authored-by: Cole Miller --- crates/editor/src/git/project_diff.rs | 246 ++++++++++++++------------ crates/project/src/buffer_store.rs | 7 + 2 files changed, 141 insertions(+), 112 deletions(-) diff --git a/crates/editor/src/git/project_diff.rs b/crates/editor/src/git/project_diff.rs index e3d9f6abd6..8ececa9bb8 100644 --- a/crates/editor/src/git/project_diff.rs +++ b/crates/editor/src/git/project_diff.rs @@ -55,6 +55,7 @@ struct ProjectDiffEditor { _subscriptions: Vec, } +#[derive(Debug)] struct Changes { _status: GitFileStatus, buffer: Model, @@ -235,15 +236,16 @@ impl ProjectDiffEditor { let mut change_sets = Vec::new(); for (status, entry_id, entry_path, open_task) in open_tasks { let (_, opened_model) = open_task.await.with_context(|| { - format!("loading buffer {} for git diff", entry_path.path.display()) + format!("loading buffer {:?} for git diff", entry_path.path) })?; let buffer = match opened_model.downcast::() { Ok(buffer) => buffer, Err(_model) => anyhow::bail!( - "Could not load {} as a buffer for git diff", - entry_path.path.display() + "Could not load {:?} as a buffer for git diff", + entry_path.path ), }; + let change_set = project .update(&mut cx, |project, cx| { project.open_unstaged_changes(buffer.clone(), cx) @@ -1089,13 +1091,16 @@ impl Render for ProjectDiffEditor { #[cfg(test)] mod tests { - // use std::{ops::Deref as _, path::Path, sync::Arc}; + use gpui::{SemanticVersion, TestAppContext, VisualTestContext}; + use project::buffer_store::BufferChangeSet; + use serde_json::json; + use settings::SettingsStore; + use std::{ + ops::Deref as _, + path::{Path, PathBuf}, + }; - // use fs::RealFs; - // use gpui::{SemanticVersion, TestAppContext, VisualTestContext}; - // use settings::SettingsStore; - - // use super::*; + use super::*; // TODO finish // #[gpui::test] @@ -1111,114 +1116,131 @@ mod tests { // // Apply randomized changes to the project: select a random file, random change and apply to buffers // } - // #[gpui::test] - // async fn simple_edit_test(cx: &mut TestAppContext) { - // cx.executor().allow_parking(); - // init_test(cx); + #[gpui::test(iterations = 30)] + async fn simple_edit_test(cx: &mut TestAppContext) { + cx.executor().allow_parking(); + init_test(cx); - // let dir = tempfile::tempdir().unwrap(); - // let dst = dir.path(); + let fs = fs::FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/root", + json!({ + ".git": {}, + "file_a": "This is file_a", + "file_b": "This is file_b", + }), + ) + .await; - // std::fs::write(dst.join("file_a"), "This is file_a").unwrap(); - // std::fs::write(dst.join("file_b"), "This is file_b").unwrap(); + let project = Project::test(fs.clone(), [Path::new("/root")], cx).await; + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx); - // run_git(dst, &["init"]); - // run_git(dst, &["add", "*"]); - // run_git(dst, &["commit", "-m", "Initial commit"]); + let file_a_editor = workspace + .update(cx, |workspace, cx| { + let file_a_editor = + workspace.open_abs_path(PathBuf::from("/root/file_a"), true, cx); + ProjectDiffEditor::deploy(workspace, &Deploy, cx); + file_a_editor + }) + .unwrap() + .await + .expect("did not open an item at all") + .downcast::() + .expect("did not open an editor for file_a"); + let project_diff_editor = workspace + .update(cx, |workspace, cx| { + workspace + .active_pane() + .read(cx) + .items() + .find_map(|item| item.downcast::()) + }) + .unwrap() + .expect("did not find a ProjectDiffEditor"); + project_diff_editor.update(cx, |project_diff_editor, cx| { + assert!( + project_diff_editor.editor.read(cx).text(cx).is_empty(), + "Should have no changes after opening the diff on no git changes" + ); + }); - // let project = Project::test(Arc::new(RealFs::default()), [dst], cx).await; - // let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); - // let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx); + let old_text = file_a_editor.update(cx, |editor, cx| editor.text(cx)); + let change = "an edit after git add"; + file_a_editor + .update(cx, |file_a_editor, cx| { + file_a_editor.insert(change, cx); + file_a_editor.save(false, project.clone(), cx) + }) + .await + .expect("failed to save a file"); + file_a_editor.update(cx, |file_a_editor, cx| { + let change_set = cx.new_model(|cx| { + BufferChangeSet::new_with_base_text( + old_text.clone(), + file_a_editor + .buffer() + .read(cx) + .as_singleton() + .unwrap() + .read(cx) + .text_snapshot(), + cx, + ) + }); + file_a_editor + .diff_map + .add_change_set(change_set.clone(), cx); + project.update(cx, |project, cx| { + project.buffer_store().update(cx, |buffer_store, cx| { + buffer_store.set_change_set( + file_a_editor + .buffer() + .read(cx) + .as_singleton() + .unwrap() + .read(cx) + .remote_id(), + change_set, + ); + }); + }); + }); + fs.set_status_for_repo_via_git_operation( + Path::new("/root/.git"), + &[(Path::new("file_a"), GitFileStatus::Modified)], + ); + cx.executor() + .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); + cx.run_until_parked(); - // let file_a_editor = workspace - // .update(cx, |workspace, cx| { - // let file_a_editor = workspace.open_abs_path(dst.join("file_a"), true, cx); - // ProjectDiffEditor::deploy(workspace, &Deploy, cx); - // file_a_editor - // }) - // .unwrap() - // .await - // .expect("did not open an item at all") - // .downcast::() - // .expect("did not open an editor for file_a"); + project_diff_editor.update(cx, |project_diff_editor, cx| { + assert_eq!( + // TODO assert it better: extract added text (based on the background changes) and deleted text (based on the deleted blocks added) + project_diff_editor.editor.read(cx).text(cx), + format!("{change}{old_text}"), + "Should have a new change shown in the beginning, and the old text shown as deleted text afterwards" + ); + }); + } - // let project_diff_editor = workspace - // .update(cx, |workspace, cx| { - // workspace - // .active_pane() - // .read(cx) - // .items() - // .find_map(|item| item.downcast::()) - // }) - // .unwrap() - // .expect("did not find a ProjectDiffEditor"); - // project_diff_editor.update(cx, |project_diff_editor, cx| { - // assert!( - // project_diff_editor.editor.read(cx).text(cx).is_empty(), - // "Should have no changes after opening the diff on no git changes" - // ); - // }); + fn init_test(cx: &mut gpui::TestAppContext) { + if std::env::var("RUST_LOG").is_ok() { + env_logger::try_init().ok(); + } - // let old_text = file_a_editor.update(cx, |editor, cx| editor.text(cx)); - // let change = "an edit after git add"; - // file_a_editor - // .update(cx, |file_a_editor, cx| { - // file_a_editor.insert(change, cx); - // file_a_editor.save(false, project.clone(), cx) - // }) - // .await - // .expect("failed to save a file"); - // cx.executor().advance_clock(Duration::from_secs(1)); - // cx.run_until_parked(); - - // // TODO does not work on Linux for some reason, returning a blank line - // // hence disable the last check for now, and do some fiddling to avoid the warnings. - // #[cfg(target_os = "linux")] - // { - // if true { - // return; - // } - // } - // project_diff_editor.update(cx, |project_diff_editor, cx| { - // // TODO assert it better: extract added text (based on the background changes) and deleted text (based on the deleted blocks added) - // assert_eq!( - // project_diff_editor.editor.read(cx).text(cx), - // format!("{change}{old_text}"), - // "Should have a new change shown in the beginning, and the old text shown as deleted text afterwards" - // ); - // }); - // } - - // fn run_git(path: &Path, args: &[&str]) -> String { - // let output = std::process::Command::new("git") - // .args(args) - // .current_dir(path) - // .output() - // .expect("git commit failed"); - - // format!( - // "Stdout: {}; stderr: {}", - // String::from_utf8(output.stdout).unwrap(), - // String::from_utf8(output.stderr).unwrap() - // ) - // } - - // fn init_test(cx: &mut gpui::TestAppContext) { - // if std::env::var("RUST_LOG").is_ok() { - // env_logger::try_init().ok(); - // } - - // cx.update(|cx| { - // assets::Assets.load_test_fonts(cx); - // let settings_store = SettingsStore::test(cx); - // cx.set_global(settings_store); - // theme::init(theme::LoadThemes::JustBase, cx); - // release_channel::init(SemanticVersion::default(), cx); - // client::init_settings(cx); - // language::init(cx); - // Project::init_settings(cx); - // workspace::init_settings(cx); - // crate::init(cx); - // }); - // } + cx.update(|cx| { + assets::Assets.load_test_fonts(cx); + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + theme::init(theme::LoadThemes::JustBase, cx); + release_channel::init(SemanticVersion::default(), cx); + client::init_settings(cx); + language::init(cx); + Project::init_settings(cx); + workspace::init_settings(cx); + crate::init(cx); + cx.set_staff(true); + }); + } } diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index a4c6231206..4509142189 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -49,6 +49,7 @@ struct SharedBuffer { unstaged_changes: Option>, } +#[derive(Debug)] pub struct BufferChangeSet { pub buffer_id: BufferId, pub base_text: Option>, @@ -1045,6 +1046,12 @@ impl BufferStore { .spawn(async move { task.await.map_err(|e| anyhow!("{e}")) }) } + #[cfg(any(test, feature = "test-support"))] + pub fn set_change_set(&mut self, buffer_id: BufferId, change_set: Model) { + self.loading_change_sets + .insert(buffer_id, Task::ready(Ok(change_set)).shared()); + } + pub async fn open_unstaged_changes_internal( this: WeakModel, text: Result>, From c8b3c4c6cd82f7c60776c13ea82aac906eee3a3f Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 5 Dec 2024 15:57:35 -0500 Subject: [PATCH 304/886] assistant2: Add ability to delete past threads (#21607) This PR adds the ability to delete past threads in Assistant2. Release Notes: - N/A --- crates/assistant2/src/assistant_panel.rs | 9 +++++++-- crates/assistant2/src/thread_history.rs | 14 +++++++++++++- crates/assistant2/src/thread_store.rs | 4 ++++ 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index 2d9f563c2f..fde3aa02ba 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -106,6 +106,10 @@ impl AssistantPanel { } } + pub(crate) fn local_timezone(&self) -> UtcOffset { + self.local_timezone + } + fn new_thread(&mut self, cx: &mut ViewContext) { let thread = self .thread_store @@ -147,8 +151,9 @@ impl AssistantPanel { self.message_editor.focus_handle(cx).focus(cx); } - pub(crate) fn local_timezone(&self) -> UtcOffset { - self.local_timezone + pub(crate) fn delete_thread(&mut self, thread_id: &ThreadId, cx: &mut ViewContext) { + self.thread_store + .update(cx, |this, cx| this.delete_thread(thread_id, cx)); } } diff --git a/crates/assistant2/src/thread_history.rs b/crates/assistant2/src/thread_history.rs index 7216ca695a..f183276f7b 100644 --- a/crates/assistant2/src/thread_history.rs +++ b/crates/assistant2/src/thread_history.rs @@ -127,11 +127,23 @@ impl RenderOnce for PastThread { .child( IconButton::new("delete", IconName::TrashAlt) .shape(IconButtonShape::Square) - .icon_size(IconSize::Small), + .icon_size(IconSize::Small) + .on_click({ + let assistant_panel = self.assistant_panel.clone(); + let id = id.clone(); + move |_event, cx| { + assistant_panel + .update(cx, |this, cx| { + this.delete_thread(&id, cx); + }) + .ok(); + } + }), ), ) .on_click({ let assistant_panel = self.assistant_panel.clone(); + let id = id.clone(); move |_event, cx| { assistant_panel .update(cx, |this, cx| { diff --git a/crates/assistant2/src/thread_store.rs b/crates/assistant2/src/thread_store.rs index 7ceee9306b..94cb72ce43 100644 --- a/crates/assistant2/src/thread_store.rs +++ b/crates/assistant2/src/thread_store.rs @@ -80,6 +80,10 @@ impl ThreadStore { .cloned() } + pub fn delete_thread(&mut self, id: &ThreadId, cx: &mut ModelContext) { + self.threads.retain(|thread| thread.read(cx).id() != id); + } + fn register_context_server_handlers(&self, cx: &mut ModelContext) { cx.subscribe( &self.context_server_manager.clone(), From 0511768b226094a25fddb99fc6e7ef53ad220197 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Thu, 5 Dec 2024 19:17:26 -0300 Subject: [PATCH 305/886] project panel: Use theme token for focused border color (#21593) Closes https://github.com/zed-industries/zed/issues/12723 This PR makes the border color of a focused project panel item use the `panel_focused_border` theme token. This allow theme makers to customize that independently of the `text_accent` color, which was the one being previously used. ### One Dark | Before | After | |--------|--------| | Screenshot 2024-12-05 at 18 37 00 | Screenshot 2024-12-05 at 18 39 42 | | Screenshot 2024-12-05 at 18 37 08 | Screenshot 2024-12-05 at 18 39 51 | ### Gruvbox Hard | Before | After | |--------|--------| | Screenshot 2024-12-05 at 18 38 05 | Screenshot 2024-12-05 at 18 40 15 | | Screenshot 2024-12-05 at 18 38 16 | Screenshot 2024-12-05 at 18 39 57 | Release Notes: - N/A --- crates/project_panel/src/project_panel.rs | 4 +++- crates/theme/src/default_colors.rs | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index d263c75ca7..ca6f89f69a 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -261,6 +261,7 @@ struct ItemColors { hover: Hsla, drag_over: Hsla, marked_active: Hsla, + focused: Hsla, } fn get_item_color(cx: &ViewContext) -> ItemColors { @@ -271,6 +272,7 @@ fn get_item_color(cx: &ViewContext) -> ItemColors { hover: colors.ghost_element_hover, drag_over: colors.drop_target_background, marked_active: colors.ghost_element_selected, + focused: colors.panel_focused_border, } } @@ -3504,7 +3506,7 @@ impl ProjectPanel { .rounded_none() .when( !self.mouse_down && is_active && self.focus_handle.contains_focused(cx), - |this| this.border_color(Color::Selected.color(cx)), + |this| this.border_color(item_colors.focused), ) } diff --git a/crates/theme/src/default_colors.rs b/crates/theme/src/default_colors.rs index 05dd6cd1e7..b9780a304a 100644 --- a/crates/theme/src/default_colors.rs +++ b/crates/theme/src/default_colors.rs @@ -58,7 +58,7 @@ impl ThemeColors { tab_active_background: neutral().light().step_1(), search_match_background: neutral().light().step_5(), panel_background: neutral().light().step_2(), - panel_focused_border: blue().light().step_5(), + panel_focused_border: blue().light().step_10(), panel_indent_guide: neutral().light_alpha().step_5(), panel_indent_guide_hover: neutral().light_alpha().step_6(), panel_indent_guide_active: neutral().light_alpha().step_6(), @@ -164,7 +164,7 @@ impl ThemeColors { tab_active_background: neutral().dark().step_1(), search_match_background: neutral().dark().step_5(), panel_background: neutral().dark().step_2(), - panel_focused_border: blue().dark().step_5(), + panel_focused_border: blue().dark().step_12(), panel_indent_guide: neutral().dark_alpha().step_4(), panel_indent_guide_hover: neutral().dark_alpha().step_6(), panel_indent_guide_active: neutral().dark_alpha().step_6(), From 6a4cd53fd8abf6caffbd7f25d4e1d51d54343db8 Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Thu, 5 Dec 2024 16:06:17 -0700 Subject: [PATCH 306/886] Use LiveKit's Rust SDK on Linux while continue using Swift SDK on Mac (#21550) Similar to #20826 but keeps the Swift implementation. There were quite a few changes in the `call` crate, and so that code now has two variants. Closes #13714 Release Notes: - Added preliminary Linux support for voice chat and viewing screenshares. --------- Co-authored-by: Kirill Bulatov Co-authored-by: Kirill Bulatov Co-authored-by: Mikayla --- .cargo/config.toml | 6 + .github/workflows/ci.yml | 1 + Cargo.lock | 502 ++++- Cargo.toml | 16 +- crates/call/Cargo.toml | 15 +- crates/call/src/call.rs | 575 +----- crates/call/src/cross_platform/mod.rs | 552 +++++ crates/call/src/cross_platform/participant.rs | 68 + crates/call/src/cross_platform/room.rs | 1771 +++++++++++++++++ crates/call/src/macos/mod.rs | 545 +++++ crates/call/src/{ => macos}/participant.rs | 14 +- crates/call/src/{ => macos}/room.rs | 16 +- crates/collab/.env.toml | 6 +- crates/collab/Cargo.toml | 15 +- crates/collab/k8s/collab.template.yml | 6 +- crates/collab/src/db/queries/channels.rs | 8 +- crates/collab/src/db/queries/rooms.rs | 6 +- crates/collab/src/lib.rs | 28 +- crates/collab/src/rpc.rs | 90 +- crates/collab/src/tests.rs | 3 + .../collab/src/tests/channel_guest_tests.rs | 24 +- crates/collab/src/tests/following_tests.rs | 208 +- crates/collab/src/tests/integration_tests.rs | 123 +- crates/collab/src/tests/test_server.rs | 36 +- crates/collab_ui/src/collab_panel.rs | 5 +- crates/gpui/build.rs | 1 + crates/gpui/src/app.rs | 11 +- crates/gpui/src/app/test_context.rs | 10 +- crates/gpui/src/geometry.rs | 5 + crates/gpui/src/platform.rs | 26 + crates/gpui/src/platform/linux.rs | 2 + crates/gpui/src/platform/linux/platform.rs | 12 +- crates/gpui/src/platform/mac.rs | 5 + crates/gpui/src/platform/mac/platform.rs | 14 +- .../gpui/src/platform/mac/screen_capture.rs | 239 +++ crates/gpui/src/platform/test.rs | 2 + crates/gpui/src/platform/test/platform.rs | 58 +- crates/gpui/src/platform/windows.rs | 2 + crates/gpui/src/platform/windows/platform.rs | 8 + crates/http_client/Cargo.toml | 2 +- .../.cargo/config.toml | 2 +- crates/livekit_client/Cargo.toml | 65 + .../LICENSE-GPL | 0 crates/livekit_client/examples/test_app.rs | 442 ++++ crates/livekit_client/src/livekit_client.rs | 661 ++++++ .../src/remote_video_track_view.rs | 99 + crates/livekit_client/src/test.rs | 825 ++++++++ crates/livekit_client/src/test/participant.rs | 111 ++ crates/livekit_client/src/test/publication.rs | 116 ++ crates/livekit_client/src/test/track.rs | 201 ++ crates/livekit_client/src/test/webrtc.rs | 136 ++ .../livekit_client_macos/.cargo/config.toml | 2 + .../Cargo.toml | 12 +- crates/livekit_client_macos/LICENSE-GPL | 1 + .../LiveKitBridge/Package.resolved | 0 .../LiveKitBridge/Package.swift | 0 .../LiveKitBridge/README.md | 0 .../Sources/LiveKitBridge/LiveKitBridge.swift | 0 .../build.rs | 0 .../examples/test_app.rs | 6 +- .../src/livekit_client.rs} | 0 .../src/prod.rs | 0 .../src/test.rs | 22 +- .../Cargo.toml | 4 +- .../LICENSE-AGPL | 0 .../build.rs | 0 .../src/api.rs | 0 .../src/livekit_server.rs} | 0 .../src/proto.rs | 0 .../src/token.rs | 0 .../vendored/protocol/README.md | 0 .../vendored/protocol/livekit_analytics.proto | 0 .../vendored/protocol/livekit_egress.proto | 0 .../vendored/protocol/livekit_ingress.proto | 0 .../vendored/protocol/livekit_internal.proto | 0 .../vendored/protocol/livekit_models.proto | 0 .../vendored/protocol/livekit_room.proto | 0 .../protocol/livekit_rpc_internal.proto | 0 .../vendored/protocol/livekit_rtc.proto | 0 .../vendored/protocol/livekit_webhook.proto | 0 crates/media/Cargo.toml | 1 + crates/media/src/media.rs | 11 +- crates/proto/proto/zed.proto | 2 +- crates/title_bar/src/collab.rs | 65 +- crates/workspace/Cargo.toml | 2 + crates/workspace/src/shared_screen.rs | 362 +++- crates/workspace/src/workspace.rs | 13 +- crates/zed/Cargo.toml | 6 + crates/zed/build.rs | 8 + script/bundle-linux | 2 +- typos.toml | 2 +- 91 files changed, 7187 insertions(+), 1028 deletions(-) create mode 100644 crates/call/src/cross_platform/mod.rs create mode 100644 crates/call/src/cross_platform/participant.rs create mode 100644 crates/call/src/cross_platform/room.rs create mode 100644 crates/call/src/macos/mod.rs rename crates/call/src/{ => macos}/participant.rs (80%) rename crates/call/src/{ => macos}/room.rs (99%) create mode 100644 crates/gpui/src/platform/mac/screen_capture.rs rename crates/{live_kit_client => livekit_client}/.cargo/config.toml (62%) create mode 100644 crates/livekit_client/Cargo.toml rename crates/{live_kit_client => livekit_client}/LICENSE-GPL (100%) create mode 100644 crates/livekit_client/examples/test_app.rs create mode 100644 crates/livekit_client/src/livekit_client.rs create mode 100644 crates/livekit_client/src/remote_video_track_view.rs create mode 100644 crates/livekit_client/src/test.rs create mode 100644 crates/livekit_client/src/test/participant.rs create mode 100644 crates/livekit_client/src/test/publication.rs create mode 100644 crates/livekit_client/src/test/track.rs create mode 100644 crates/livekit_client/src/test/webrtc.rs create mode 100644 crates/livekit_client_macos/.cargo/config.toml rename crates/{live_kit_client => livekit_client_macos}/Cargo.toml (87%) create mode 120000 crates/livekit_client_macos/LICENSE-GPL rename crates/{live_kit_client => livekit_client_macos}/LiveKitBridge/Package.resolved (100%) rename crates/{live_kit_client => livekit_client_macos}/LiveKitBridge/Package.swift (100%) rename crates/{live_kit_client => livekit_client_macos}/LiveKitBridge/README.md (100%) rename crates/{live_kit_client => livekit_client_macos}/LiveKitBridge/Sources/LiveKitBridge/LiveKitBridge.swift (100%) rename crates/{live_kit_client => livekit_client_macos}/build.rs (100%) rename crates/{live_kit_client => livekit_client_macos}/examples/test_app.rs (97%) rename crates/{live_kit_client/src/live_kit_client.rs => livekit_client_macos/src/livekit_client.rs} (100%) rename crates/{live_kit_client => livekit_client_macos}/src/prod.rs (100%) rename crates/{live_kit_client => livekit_client_macos}/src/test.rs (97%) rename crates/{live_kit_server => livekit_server}/Cargo.toml (90%) rename crates/{live_kit_server => livekit_server}/LICENSE-AGPL (100%) rename crates/{live_kit_server => livekit_server}/build.rs (100%) rename crates/{live_kit_server => livekit_server}/src/api.rs (100%) rename crates/{live_kit_server/src/live_kit_server.rs => livekit_server/src/livekit_server.rs} (100%) rename crates/{live_kit_server => livekit_server}/src/proto.rs (100%) rename crates/{live_kit_server => livekit_server}/src/token.rs (100%) rename crates/{live_kit_server => livekit_server}/vendored/protocol/README.md (100%) rename crates/{live_kit_server => livekit_server}/vendored/protocol/livekit_analytics.proto (100%) rename crates/{live_kit_server => livekit_server}/vendored/protocol/livekit_egress.proto (100%) rename crates/{live_kit_server => livekit_server}/vendored/protocol/livekit_ingress.proto (100%) rename crates/{live_kit_server => livekit_server}/vendored/protocol/livekit_internal.proto (100%) rename crates/{live_kit_server => livekit_server}/vendored/protocol/livekit_models.proto (100%) rename crates/{live_kit_server => livekit_server}/vendored/protocol/livekit_room.proto (100%) rename crates/{live_kit_server => livekit_server}/vendored/protocol/livekit_rpc_internal.proto (100%) rename crates/{live_kit_server => livekit_server}/vendored/protocol/livekit_rtc.proto (100%) rename crates/{live_kit_server => livekit_server}/vendored/protocol/livekit_webhook.proto (100%) diff --git a/.cargo/config.toml b/.cargo/config.toml index a657ae61b9..043adf6b30 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -13,6 +13,12 @@ rustflags = ["-C", "link-arg=-fuse-ld=mold"] linker = "clang" rustflags = ["-C", "link-arg=-fuse-ld=mold"] +[target.aarch64-apple-darwin] +rustflags = ["-C", "link-args=-Objc -all_load"] + +[target.x86_64-apple-darwin] +rustflags = ["-C", "link-args=-Objc -all_load"] + # This cfg will reduce the size of `windows::core::Error` from 16 bytes to 4 bytes [target.'cfg(target_os = "windows")'] rustflags = ["--cfg", "windows_slim_errors"] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 602808f1b5..46e7ab7d51 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -129,6 +129,7 @@ jobs: run: | cargo build --workspace --bins --all-features cargo check -p gpui --features "macos-blade" + cargo check -p workspace --features "livekit-cross-platform" cargo build -p remote_server linux_tests: diff --git a/Cargo.lock b/Cargo.lock index bd3cd06dca..4d040c581c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -962,6 +962,22 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "async-tungstenite" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cca750b12e02c389c1694d35c16539f88b8bbaa5945934fdc1b41a776688589" +dependencies = [ + "async-native-tls", + "async-std", + "async-tls", + "futures-io", + "futures-util", + "log", + "pin-project-lite", + "tungstenite 0.21.0", +] + [[package]] name = "async-tungstenite" version = "0.28.0" @@ -1830,7 +1846,7 @@ dependencies = [ "arrayvec", "cc", "cfg-if", - "constant_time_eq", + "constant_time_eq 0.3.1", ] [[package]] @@ -2015,6 +2031,27 @@ dependencies = [ "either", ] +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "call" version = "0.1.0" @@ -2023,12 +2060,14 @@ dependencies = [ "audio", "client", "collections", + "feature_flags", "fs", "futures 0.3.31", "gpui", "http_client", "language", - "live_kit_client", + "livekit_client", + "livekit_client_macos", "log", "postage", "project", @@ -2486,7 +2525,7 @@ dependencies = [ "anyhow", "async-native-tls", "async-recursion 0.3.2", - "async-tungstenite", + "async-tungstenite 0.28.0", "chrono", "clock", "cocoa 0.26.0", @@ -2618,7 +2657,7 @@ dependencies = [ "assistant_tool", "async-stripe", "async-trait", - "async-tungstenite", + "async-tungstenite 0.28.0", "audio", "aws-config", "aws-sdk-kinesis", @@ -2656,8 +2695,9 @@ dependencies = [ "jsonwebtoken", "language", "language_model", - "live_kit_client", - "live_kit_server", + "livekit_client", + "livekit_client_macos", + "livekit_server", "log", "lsp", "menu", @@ -2670,7 +2710,7 @@ dependencies = [ "pretty_assertions", "project", "prometheus", - "prost", + "prost 0.9.0", "rand 0.8.5", "recent_projects", "release_channel", @@ -2870,6 +2910,12 @@ dependencies = [ "tiny-keccak", ] +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + [[package]] name = "constant_time_eq" version = "0.3.1" @@ -3077,6 +3123,17 @@ dependencies = [ "coreaudio-sys", ] +[[package]] +name = "coreaudio-rs" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ca07354f6d0640333ef95f48d460a4bcf34812a7e7967f9b44c728a8f37c28" +dependencies = [ + "bitflags 1.3.2", + "core-foundation-sys", + "coreaudio-sys", +] + [[package]] name = "coreaudio-sys" version = "0.2.16" @@ -3111,12 +3168,11 @@ dependencies = [ [[package]] name = "cpal" version = "0.15.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" +source = "git+https://github.com/zed-industries/cpal?rev=fd8bc2fd39f1f5fdee5a0690656caff9a26d9d50#fd8bc2fd39f1f5fdee5a0690656caff9a26d9d50" dependencies = [ "alsa", "core-foundation-sys", - "coreaudio-rs", + "coreaudio-rs 0.11.3", "dasp_sample", "jni", "js-sys", @@ -3448,6 +3504,65 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991" +[[package]] +name = "cxx" +version = "1.0.133" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05e1ec88093d2abd9cf1b09ffd979136b8e922bf31cad966a8fe0d73233112ef" +dependencies = [ + "cc", + "cxxbridge-cmd", + "cxxbridge-flags", + "cxxbridge-macro", + "foldhash", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.133" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afa390d956ee7ccb41aeed7ed7856ab3ffb4fc587e7216be7e0f83e949b4e6c" +dependencies = [ + "cc", + "codespan-reporting", + "proc-macro2", + "quote", + "scratch", + "syn 2.0.87", +] + +[[package]] +name = "cxxbridge-cmd" +version = "1.0.133" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c23bfff654d6227cbc83de8e059d2f8678ede5fc3a6c5a35d5c379983cc61e6" +dependencies = [ + "clap", + "codespan-reporting", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.133" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c01b36e22051bc6928a78583f1621abaaf7621561c2ada1b00f7878fbe2caa" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.133" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e14013136fac689345d17b9a6df55977251f11d333c0a571e8d963b55e1f95" +dependencies = [ + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.87", +] + [[package]] name = "dashmap" version = "5.5.3" @@ -4706,6 +4821,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "fsevent" version = "0.1.0" @@ -6338,6 +6463,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.12.1" @@ -6463,7 +6597,7 @@ checksum = "58d9afa5bc6eeafb78f710a2efc585f69099f8b6a99dc7eb826581e3773a6e31" dependencies = [ "anyhow", "async-trait", - "async-tungstenite", + "async-tungstenite 0.28.0", "futures 0.3.31", "jupyter-protocol", "serde", @@ -6881,6 +7015,29 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "libwebrtc" +version = "0.3.7" +source = "git+https://github.com/zed-industries/rust-sdks?rev=799f10133d93ba2a88642cd480d01ec4da53408c#799f10133d93ba2a88642cd480d01ec4da53408c" +dependencies = [ + "cxx", + "jni", + "js-sys", + "lazy_static", + "livekit-protocol", + "livekit-runtime", + "log", + "parking_lot", + "serde", + "serde_json", + "thiserror 1.0.69", + "tokio", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webrtc-sys", +] + [[package]] name = "libz-sys" version = "1.1.20" @@ -6893,6 +7050,15 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "link-cplusplus" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d240c6f7e1ba3a28b0249f774e6a9dd0175054b52dfbb61b16eb8505c3785c9" +dependencies = [ + "cc", +] + [[package]] name = "linkify" version = "0.10.0" @@ -6941,7 +7107,112 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" [[package]] -name = "live_kit_client" +name = "livekit" +version = "0.7.0" +source = "git+https://github.com/zed-industries/rust-sdks?rev=799f10133d93ba2a88642cd480d01ec4da53408c#799f10133d93ba2a88642cd480d01ec4da53408c" +dependencies = [ + "chrono", + "futures-util", + "lazy_static", + "libwebrtc", + "livekit-api", + "livekit-protocol", + "livekit-runtime", + "log", + "parking_lot", + "prost 0.12.6", + "semver", + "serde", + "serde_json", + "thiserror 1.0.69", + "tokio", +] + +[[package]] +name = "livekit-api" +version = "0.4.1" +source = "git+https://github.com/zed-industries/rust-sdks?rev=799f10133d93ba2a88642cd480d01ec4da53408c#799f10133d93ba2a88642cd480d01ec4da53408c" +dependencies = [ + "async-tungstenite 0.25.1", + "futures-util", + "http 0.2.12", + "jsonwebtoken", + "livekit-protocol", + "livekit-runtime", + "log", + "parking_lot", + "prost 0.12.6", + "reqwest 0.11.27", + "scopeguard", + "serde", + "serde_json", + "sha2", + "thiserror 1.0.69", + "tokio", + "tokio-tungstenite 0.20.1", + "url", +] + +[[package]] +name = "livekit-protocol" +version = "0.3.6" +source = "git+https://github.com/zed-industries/rust-sdks?rev=799f10133d93ba2a88642cd480d01ec4da53408c#799f10133d93ba2a88642cd480d01ec4da53408c" +dependencies = [ + "futures-util", + "livekit-runtime", + "parking_lot", + "pbjson", + "pbjson-types", + "prost 0.12.6", + "prost-types 0.12.6", + "serde", + "thiserror 1.0.69", + "tokio", +] + +[[package]] +name = "livekit-runtime" +version = "0.3.1" +source = "git+https://github.com/zed-industries/rust-sdks?rev=799f10133d93ba2a88642cd480d01ec4da53408c#799f10133d93ba2a88642cd480d01ec4da53408c" +dependencies = [ + "async-io 2.4.0", + "async-std", + "async-task", + "futures 0.3.31", +] + +[[package]] +name = "livekit_client" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "collections", + "core-foundation 0.9.4", + "coreaudio-rs 0.12.1", + "cpal", + "futures 0.3.31", + "gpui", + "http 0.2.12", + "http_client", + "image", + "livekit", + "livekit_server", + "log", + "media", + "nanoid", + "parking_lot", + "postage", + "serde", + "serde_json", + "sha2", + "simplelog", + "smallvec", + "util", +] + +[[package]] +name = "livekit_client_macos" version = "0.1.0" dependencies = [ "anyhow", @@ -6951,7 +7222,7 @@ dependencies = [ "core-foundation 0.9.4", "futures 0.3.31", "gpui", - "live_kit_server", + "livekit_server", "log", "media", "nanoid", @@ -6964,16 +7235,16 @@ dependencies = [ ] [[package]] -name = "live_kit_server" +name = "livekit_server" version = "0.1.0" dependencies = [ "anyhow", "async-trait", "jsonwebtoken", "log", - "prost", - "prost-build", - "prost-types", + "prost 0.9.0", + "prost-build 0.9.0", + "prost-types 0.9.0", "reqwest 0.12.8", "serde", ] @@ -7262,6 +7533,7 @@ dependencies = [ "anyhow", "bindgen", "core-foundation 0.9.4", + "ctor", "foreign-types 0.5.0", "metal", "objc", @@ -7954,7 +8226,7 @@ dependencies = [ "md-5", "num", "num-bigint-dig", - "pbkdf2", + "pbkdf2 0.12.2", "rand 0.8.5", "serde", "sha2", @@ -8274,6 +8546,17 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "password-hash" version = "0.5.0" @@ -8324,6 +8607,55 @@ dependencies = [ "util", ] +[[package]] +name = "pbjson" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1030c719b0ec2a2d25a5df729d6cff1acf3cc230bf766f4f97833591f7577b90" +dependencies = [ + "base64 0.21.7", + "serde", +] + +[[package]] +name = "pbjson-build" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2580e33f2292d34be285c5bc3dba5259542b083cfad6037b6d70345f24dcb735" +dependencies = [ + "heck 0.4.1", + "itertools 0.11.0", + "prost 0.12.6", + "prost-types 0.12.6", +] + +[[package]] +name = "pbjson-types" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18f596653ba4ac51bdecbb4ef6773bc7f56042dc13927910de1684ad3d32aa12" +dependencies = [ + "bytes 1.8.0", + "chrono", + "pbjson", + "pbjson-build", + "prost 0.12.6", + "prost-build 0.12.6", + "serde", +] + +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest", + "hmac", + "password-hash 0.4.2", + "sha2", +] + [[package]] name = "pbkdf2" version = "0.12.2" @@ -9353,7 +9685,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "444879275cb4fd84958b1a1d5420d15e6fcf7c235fe47f053c9c2a80aceb6001" dependencies = [ "bytes 1.8.0", - "prost-derive", + "prost-derive 0.9.0", +] + +[[package]] +name = "prost" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" +dependencies = [ + "bytes 1.8.0", + "prost-derive 0.12.6", ] [[package]] @@ -9369,13 +9711,34 @@ dependencies = [ "log", "multimap", "petgraph", - "prost", - "prost-types", + "prost 0.9.0", + "prost-types 0.9.0", "regex", "tempfile", "which 4.4.2", ] +[[package]] +name = "prost-build" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" +dependencies = [ + "bytes 1.8.0", + "heck 0.5.0", + "itertools 0.12.1", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost 0.12.6", + "prost-types 0.12.6", + "regex", + "syn 2.0.87", + "tempfile", +] + [[package]] name = "prost-derive" version = "0.9.0" @@ -9389,6 +9752,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "prost-derive" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" +dependencies = [ + "anyhow", + "itertools 0.12.1", + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "prost-types" version = "0.9.0" @@ -9396,7 +9772,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "534b7a0e836e3c482d2693070f982e39e7611da9695d4d1f5a4b186b51faef0a" dependencies = [ "bytes 1.8.0", - "prost", + "prost 0.9.0", +] + +[[package]] +name = "prost-types" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" +dependencies = [ + "prost 0.12.6", ] [[package]] @@ -9405,8 +9790,8 @@ version = "0.1.0" dependencies = [ "anyhow", "collections", - "prost", - "prost-build", + "prost 0.9.0", + "prost-build 0.9.0", "serde", ] @@ -9906,7 +10291,7 @@ dependencies = [ "log", "parking_lot", "paths", - "prost", + "prost 0.9.0", "release_channel", "rpc", "serde", @@ -10041,6 +10426,7 @@ dependencies = [ "http 0.2.12", "http-body 0.4.6", "hyper 0.14.31", + "hyper-rustls 0.24.2", "hyper-tls", "ipnet", "js-sys", @@ -10050,6 +10436,8 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", + "rustls 0.21.12", + "rustls-native-certs 0.6.3", "rustls-pemfile 1.0.4", "serde", "serde_json", @@ -10058,6 +10446,7 @@ dependencies = [ "system-configuration 0.5.1", "tokio", "tokio-native-tls", + "tokio-rustls 0.24.1", "tower-service", "url", "wasm-bindgen", @@ -10281,7 +10670,7 @@ name = "rpc" version = "0.1.0" dependencies = [ "anyhow", - "async-tungstenite", + "async-tungstenite 0.28.0", "base64 0.22.1", "chrono", "collections", @@ -10657,14 +11046,20 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scratch" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152" + [[package]] name = "scrypt" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" dependencies = [ - "password-hash", - "pbkdf2", + "password-hash 0.5.0", + "pbkdf2 0.12.2", "salsa20", "sha2", ] @@ -12823,7 +13218,10 @@ checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" dependencies = [ "futures-util", "log", + "rustls 0.21.12", + "rustls-native-certs 0.6.3", "tokio", + "tokio-rustls 0.24.1", "tungstenite 0.20.1", ] @@ -13331,6 +13729,7 @@ dependencies = [ "httparse", "log", "rand 0.8.5", + "rustls 0.21.12", "sha1", "thiserror 1.0.69", "url", @@ -13349,6 +13748,7 @@ dependencies = [ "http 1.1.0", "httparse", "log", + "native-tls", "rand 0.8.5", "sha1", "thiserror 1.0.69", @@ -14461,6 +14861,32 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "webrtc-sys" +version = "0.3.5" +source = "git+https://github.com/zed-industries/rust-sdks?rev=799f10133d93ba2a88642cd480d01ec4da53408c#799f10133d93ba2a88642cd480d01ec4da53408c" +dependencies = [ + "cc", + "cxx", + "cxx-build", + "glob", + "log", + "webrtc-sys-build", +] + +[[package]] +name = "webrtc-sys-build" +version = "0.3.5" +source = "git+https://github.com/zed-industries/rust-sdks?rev=799f10133d93ba2a88642cd480d01ec4da53408c#799f10133d93ba2a88642cd480d01ec4da53408c" +dependencies = [ + "fs2", + "regex", + "reqwest 0.11.27", + "scratch", + "semver", + "zip", +] + [[package]] name = "weezl" version = "0.1.8" @@ -16026,6 +16452,26 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "aes", + "byteorder", + "bzip2", + "constant_time_eq 0.1.5", + "crc32fast", + "crossbeam-utils", + "flate2", + "hmac", + "pbkdf2 0.11.0", + "sha1", + "time", + "zstd", +] + [[package]] name = "zstd" version = "0.11.2+zstd.1.5.2" diff --git a/Cargo.toml b/Cargo.toml index 5bf65b3e14..a21a65c8fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,8 +65,9 @@ members = [ "crates/language_selector", "crates/language_tools", "crates/languages", - "crates/live_kit_client", - "crates/live_kit_server", + "crates/livekit_client", + "crates/livekit_client_macos", + "crates/livekit_server", "crates/lsp", "crates/markdown", "crates/markdown_preview", @@ -248,8 +249,9 @@ language_models = { path = "crates/language_models" } language_selector = { path = "crates/language_selector" } language_tools = { path = "crates/language_tools" } languages = { path = "crates/languages" } -live_kit_client = { path = "crates/live_kit_client" } -live_kit_server = { path = "crates/live_kit_server" } +livekit_client = { path = "crates/livekit_client" } +livekit_client_macos = { path = "crates/livekit_client_macos" } +livekit_server = { path = "crates/livekit_server" } lsp = { path = "crates/lsp" } markdown = { path = "crates/markdown" } markdown_preview = { path = "crates/markdown_preview" } @@ -382,6 +384,7 @@ heed = { version = "0.20.1", features = ["read-txn-no-tls"] } hex = "0.4.3" html5ever = "0.27.0" hyper = "0.14" +http = "1.1" ignore = "0.4.22" image = "0.25.1" indexmap = { version = "1.6.2", features = ["serde"] } @@ -393,6 +396,7 @@ jupyter-websocket-client = { version = "0.8.0" } libc = "0.2" libsqlite3-sys = { version = "0.30.1", features = ["bundled"] } linkify = "0.10.0" +livekit = { git = "https://github.com/zed-industries/rust-sdks", rev="799f10133d93ba2a88642cd480d01ec4da53408c", features = ["dispatcher", "services-dispatcher", "rustls-tls-native-roots"], default-features = false } log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] } markup5ever_rcdom = "0.3.0" nanoid = "0.4" @@ -571,6 +575,10 @@ features = [ "Win32_UI_WindowsAndMessaging", ] +# TODO livekit https://github.com/RustAudio/cpal/pull/891 +[patch.crates-io] +cpal = { git = "https://github.com/zed-industries/cpal", rev = "fd8bc2fd39f1f5fdee5a0690656caff9a26d9d50" } + [profile.dev] split-debuginfo = "unpacked" debug = "limited" diff --git a/crates/call/Cargo.toml b/crates/call/Cargo.toml index 974c860c08..e7bc8b44a3 100644 --- a/crates/call/Cargo.toml +++ b/crates/call/Cargo.toml @@ -17,21 +17,23 @@ test-support = [ "client/test-support", "collections/test-support", "gpui/test-support", - "live_kit_client/test-support", + "livekit_client/test-support", "project/test-support", "util/test-support" ] +livekit-macos = ["livekit_client_macos"] +livekit-cross-platform = ["livekit_client"] [dependencies] anyhow.workspace = true audio.workspace = true client.workspace = true collections.workspace = true +feature_flags.workspace = true fs.workspace = true futures.workspace = true gpui.workspace = true language.workspace = true -live_kit_client.workspace = true log.workspace = true postage.workspace = true project.workspace = true @@ -40,6 +42,8 @@ serde.workspace = true serde_derive.workspace = true settings.workspace = true util.workspace = true +livekit_client_macos = { workspace = true, optional = true } +livekit_client = { workspace = true, optional = true } [dev-dependencies] client = { workspace = true, features = ["test-support"] } @@ -47,7 +51,12 @@ collections = { workspace = true, features = ["test-support"] } fs = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] } language = { workspace = true, features = ["test-support"] } -live_kit_client = { workspace = true, features = ["test-support"] } project = { workspace = true, features = ["test-support"] } util = { workspace = true, features = ["test-support"] } http_client = { workspace = true, features = ["test-support"] } + +[target.'cfg(target_os = "macos")'.dev-dependencies] +livekit_client_macos = { workspace = true, features = ["test-support"] } + +[target.'cfg(not(target_os = "macos"))'.dev-dependencies] +livekit_client = { workspace = true, features = ["test-support"] } diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index c7993f3658..9fdce4b8ba 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -1,546 +1,41 @@ pub mod call_settings; -pub mod participant; -pub mod room; -use anyhow::{anyhow, Result}; -use audio::Audio; -use call_settings::CallSettings; -use client::{proto, ChannelId, Client, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE}; -use collections::HashSet; -use futures::{channel::oneshot, future::Shared, Future, FutureExt}; -use gpui::{ - AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, ModelContext, Subscription, - Task, WeakModel, -}; -use postage::watch; -use project::Project; -use room::Event; -use settings::Settings; -use std::sync::Arc; +#[cfg(any( + all(target_os = "macos", feature = "livekit-macos"), + all( + not(target_os = "macos"), + feature = "livekit-macos", + not(feature = "livekit-cross-platform") + ) +))] +mod macos; -pub use participant::ParticipantLocation; -pub use room::Room; +#[cfg(any( + all(target_os = "macos", feature = "livekit-macos"), + all( + not(target_os = "macos"), + feature = "livekit-macos", + not(feature = "livekit-cross-platform") + ) +))] +pub use macos::*; -struct GlobalActiveCall(Model); - -impl Global for GlobalActiveCall {} - -pub fn init(client: Arc, user_store: Model, cx: &mut AppContext) { - CallSettings::register(cx); - - let active_call = cx.new_model(|cx| ActiveCall::new(client, user_store, cx)); - cx.set_global(GlobalActiveCall(active_call)); -} - -pub struct OneAtATime { - cancel: Option>, -} - -impl OneAtATime { - /// spawn a task in the given context. - /// if another task is spawned before that resolves, or if the OneAtATime itself is dropped, the first task will be cancelled and return Ok(None) - /// otherwise you'll see the result of the task. - fn spawn(&mut self, cx: &mut AppContext, f: F) -> Task>> - where - F: 'static + FnOnce(AsyncAppContext) -> Fut, - Fut: Future>, - R: 'static, - { - let (tx, rx) = oneshot::channel(); - self.cancel.replace(tx); - cx.spawn(|cx| async move { - futures::select_biased! { - _ = rx.fuse() => Ok(None), - result = f(cx).fuse() => result.map(Some), - } - }) - } - - fn running(&self) -> bool { - self.cancel - .as_ref() - .is_some_and(|cancel| !cancel.is_canceled()) - } -} - -#[derive(Clone)] -pub struct IncomingCall { - pub room_id: u64, - pub calling_user: Arc, - pub participants: Vec>, - pub initial_project: Option, -} - -/// Singleton global maintaining the user's participation in a room across workspaces. -pub struct ActiveCall { - room: Option<(Model, Vec)>, - pending_room_creation: Option, Arc>>>>, - location: Option>, - _join_debouncer: OneAtATime, - pending_invites: HashSet, - incoming_call: ( - watch::Sender>, - watch::Receiver>, +#[cfg(any( + all( + target_os = "macos", + feature = "livekit-cross-platform", + not(feature = "livekit-macos"), ), - client: Arc, - user_store: Model, - _subscriptions: Vec, -} + all(not(target_os = "macos"), feature = "livekit-cross-platform"), +))] +mod cross_platform; -impl EventEmitter for ActiveCall {} - -impl ActiveCall { - fn new(client: Arc, user_store: Model, cx: &mut ModelContext) -> Self { - Self { - room: None, - pending_room_creation: None, - location: None, - pending_invites: Default::default(), - incoming_call: watch::channel(), - _join_debouncer: OneAtATime { cancel: None }, - _subscriptions: vec![ - client.add_request_handler(cx.weak_model(), Self::handle_incoming_call), - client.add_message_handler(cx.weak_model(), Self::handle_call_canceled), - ], - client, - user_store, - } - } - - pub fn channel_id(&self, cx: &AppContext) -> Option { - self.room()?.read(cx).channel_id() - } - - async fn handle_incoming_call( - this: Model, - envelope: TypedEnvelope, - mut cx: AsyncAppContext, - ) -> Result { - let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?; - let call = IncomingCall { - room_id: envelope.payload.room_id, - participants: user_store - .update(&mut cx, |user_store, cx| { - user_store.get_users(envelope.payload.participant_user_ids, cx) - })? - .await?, - calling_user: user_store - .update(&mut cx, |user_store, cx| { - user_store.get_user(envelope.payload.calling_user_id, cx) - })? - .await?, - initial_project: envelope.payload.initial_project, - }; - this.update(&mut cx, |this, _| { - *this.incoming_call.0.borrow_mut() = Some(call); - })?; - - Ok(proto::Ack {}) - } - - async fn handle_call_canceled( - this: Model, - envelope: TypedEnvelope, - mut cx: AsyncAppContext, - ) -> Result<()> { - this.update(&mut cx, |this, _| { - let mut incoming_call = this.incoming_call.0.borrow_mut(); - if incoming_call - .as_ref() - .map_or(false, |call| call.room_id == envelope.payload.room_id) - { - incoming_call.take(); - } - })?; - Ok(()) - } - - pub fn global(cx: &AppContext) -> Model { - cx.global::().0.clone() - } - - pub fn try_global(cx: &AppContext) -> Option> { - cx.try_global::() - .map(|call| call.0.clone()) - } - - pub fn invite( - &mut self, - called_user_id: u64, - initial_project: Option>, - cx: &mut ModelContext, - ) -> Task> { - if !self.pending_invites.insert(called_user_id) { - return Task::ready(Err(anyhow!("user was already invited"))); - } - cx.notify(); - - if self._join_debouncer.running() { - return Task::ready(Ok(())); - } - - let room = if let Some(room) = self.room().cloned() { - Some(Task::ready(Ok(room)).shared()) - } else { - self.pending_room_creation.clone() - }; - - let invite = if let Some(room) = room { - cx.spawn(move |_, mut cx| async move { - let room = room.await.map_err(|err| anyhow!("{:?}", err))?; - - let initial_project_id = if let Some(initial_project) = initial_project { - Some( - room.update(&mut cx, |room, cx| room.share_project(initial_project, cx))? - .await?, - ) - } else { - None - }; - - room.update(&mut cx, move |room, cx| { - room.call(called_user_id, initial_project_id, cx) - })? - .await?; - - anyhow::Ok(()) - }) - } else { - let client = self.client.clone(); - let user_store = self.user_store.clone(); - let room = cx - .spawn(move |this, mut cx| async move { - let create_room = async { - let room = cx - .update(|cx| { - Room::create( - called_user_id, - initial_project, - client, - user_store, - cx, - ) - })? - .await?; - - this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx))? - .await?; - - anyhow::Ok(room) - }; - - let room = create_room.await; - this.update(&mut cx, |this, _| this.pending_room_creation = None)?; - room.map_err(Arc::new) - }) - .shared(); - self.pending_room_creation = Some(room.clone()); - cx.background_executor().spawn(async move { - room.await.map_err(|err| anyhow!("{:?}", err))?; - anyhow::Ok(()) - }) - }; - - cx.spawn(move |this, mut cx| async move { - let result = invite.await; - if result.is_ok() { - this.update(&mut cx, |this, cx| this.report_call_event("invite", cx))?; - } else { - //TODO: report collaboration error - log::error!("invite failed: {:?}", result); - } - - this.update(&mut cx, |this, cx| { - this.pending_invites.remove(&called_user_id); - cx.notify(); - })?; - result - }) - } - - pub fn cancel_invite( - &mut self, - called_user_id: u64, - cx: &mut ModelContext, - ) -> Task> { - let room_id = if let Some(room) = self.room() { - room.read(cx).id() - } else { - return Task::ready(Err(anyhow!("no active call"))); - }; - - let client = self.client.clone(); - cx.background_executor().spawn(async move { - client - .request(proto::CancelCall { - room_id, - called_user_id, - }) - .await?; - anyhow::Ok(()) - }) - } - - pub fn incoming(&self) -> watch::Receiver> { - self.incoming_call.1.clone() - } - - pub fn accept_incoming(&mut self, cx: &mut ModelContext) -> Task> { - if self.room.is_some() { - return Task::ready(Err(anyhow!("cannot join while on another call"))); - } - - let call = if let Some(call) = self.incoming_call.0.borrow_mut().take() { - call - } else { - return Task::ready(Err(anyhow!("no incoming call"))); - }; - - if self.pending_room_creation.is_some() { - return Task::ready(Ok(())); - } - - let room_id = call.room_id; - let client = self.client.clone(); - let user_store = self.user_store.clone(); - let join = self - ._join_debouncer - .spawn(cx, move |cx| Room::join(room_id, client, user_store, cx)); - - cx.spawn(|this, mut cx| async move { - let room = join.await?; - this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))? - .await?; - this.update(&mut cx, |this, cx| { - this.report_call_event("accept incoming", cx) - })?; - Ok(()) - }) - } - - pub fn decline_incoming(&mut self, _: &mut ModelContext) -> Result<()> { - let call = self - .incoming_call - .0 - .borrow_mut() - .take() - .ok_or_else(|| anyhow!("no incoming call"))?; - report_call_event_for_room("decline incoming", call.room_id, None, &self.client); - self.client.send(proto::DeclineCall { - room_id: call.room_id, - })?; - Ok(()) - } - - pub fn join_channel( - &mut self, - channel_id: ChannelId, - cx: &mut ModelContext, - ) -> Task>>> { - if let Some(room) = self.room().cloned() { - if room.read(cx).channel_id() == Some(channel_id) { - return Task::ready(Ok(Some(room))); - } else { - room.update(cx, |room, cx| room.clear_state(cx)); - } - } - - if self.pending_room_creation.is_some() { - return Task::ready(Ok(None)); - } - - let client = self.client.clone(); - let user_store = self.user_store.clone(); - let join = self._join_debouncer.spawn(cx, move |cx| async move { - Room::join_channel(channel_id, client, user_store, cx).await - }); - - cx.spawn(|this, mut cx| async move { - let room = join.await?; - this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))? - .await?; - this.update(&mut cx, |this, cx| { - this.report_call_event("join channel", cx) - })?; - Ok(room) - }) - } - - pub fn hang_up(&mut self, cx: &mut ModelContext) -> Task> { - cx.notify(); - self.report_call_event("hang up", cx); - - Audio::end_call(cx); - - let channel_id = self.channel_id(cx); - if let Some((room, _)) = self.room.take() { - cx.emit(Event::RoomLeft { channel_id }); - room.update(cx, |room, cx| room.leave(cx)) - } else { - Task::ready(Ok(())) - } - } - - pub fn share_project( - &mut self, - project: Model, - cx: &mut ModelContext, - ) -> Task> { - if let Some((room, _)) = self.room.as_ref() { - self.report_call_event("share project", cx); - room.update(cx, |room, cx| room.share_project(project, cx)) - } else { - Task::ready(Err(anyhow!("no active call"))) - } - } - - pub fn unshare_project( - &mut self, - project: Model, - cx: &mut ModelContext, - ) -> Result<()> { - if let Some((room, _)) = self.room.as_ref() { - self.report_call_event("unshare project", cx); - room.update(cx, |room, cx| room.unshare_project(project, cx)) - } else { - Err(anyhow!("no active call")) - } - } - - pub fn location(&self) -> Option<&WeakModel> { - self.location.as_ref() - } - - pub fn set_location( - &mut self, - project: Option<&Model>, - cx: &mut ModelContext, - ) -> Task> { - if project.is_some() || !*ZED_ALWAYS_ACTIVE { - self.location = project.map(|project| project.downgrade()); - if let Some((room, _)) = self.room.as_ref() { - return room.update(cx, |room, cx| room.set_location(project, cx)); - } - } - Task::ready(Ok(())) - } - - fn set_room( - &mut self, - room: Option>, - cx: &mut ModelContext, - ) -> Task> { - if room.as_ref() == self.room.as_ref().map(|room| &room.0) { - Task::ready(Ok(())) - } else { - cx.notify(); - if let Some(room) = room { - if room.read(cx).status().is_offline() { - self.room = None; - Task::ready(Ok(())) - } else { - let subscriptions = vec![ - cx.observe(&room, |this, room, cx| { - if room.read(cx).status().is_offline() { - this.set_room(None, cx).detach_and_log_err(cx); - } - - cx.notify(); - }), - cx.subscribe(&room, |_, _, event, cx| cx.emit(event.clone())), - ]; - self.room = Some((room.clone(), subscriptions)); - let location = self - .location - .as_ref() - .and_then(|location| location.upgrade()); - let channel_id = room.read(cx).channel_id(); - cx.emit(Event::RoomJoined { channel_id }); - room.update(cx, |room, cx| room.set_location(location.as_ref(), cx)) - } - } else { - self.room = None; - Task::ready(Ok(())) - } - } - } - - pub fn room(&self) -> Option<&Model> { - self.room.as_ref().map(|(room, _)| room) - } - - pub fn client(&self) -> Arc { - self.client.clone() - } - - pub fn pending_invites(&self) -> &HashSet { - &self.pending_invites - } - - pub fn report_call_event(&self, operation: &'static str, cx: &mut AppContext) { - if let Some(room) = self.room() { - let room = room.read(cx); - report_call_event_for_room(operation, room.id(), room.channel_id(), &self.client); - } - } -} - -pub fn report_call_event_for_room( - operation: &'static str, - room_id: u64, - channel_id: Option, - client: &Arc, -) { - let telemetry = client.telemetry(); - - telemetry.report_call_event(operation, Some(room_id), channel_id) -} - -pub fn report_call_event_for_channel( - operation: &'static str, - channel_id: ChannelId, - client: &Arc, - cx: &AppContext, -) { - let room = ActiveCall::global(cx).read(cx).room(); - - let telemetry = client.telemetry(); - - telemetry.report_call_event(operation, room.map(|r| r.read(cx).id()), Some(channel_id)) -} - -#[cfg(test)] -mod test { - use gpui::TestAppContext; - - use crate::OneAtATime; - - #[gpui::test] - async fn test_one_at_a_time(cx: &mut TestAppContext) { - let mut one_at_a_time = OneAtATime { cancel: None }; - - assert_eq!( - cx.update(|cx| one_at_a_time.spawn(cx, |_| async { Ok(1) })) - .await - .unwrap(), - Some(1) - ); - - let (a, b) = cx.update(|cx| { - ( - one_at_a_time.spawn(cx, |_| async { - panic!(""); - }), - one_at_a_time.spawn(cx, |_| async { Ok(3) }), - ) - }); - - assert_eq!(a.await.unwrap(), None::); - assert_eq!(b.await.unwrap(), Some(3)); - - let promise = cx.update(|cx| one_at_a_time.spawn(cx, |_| async { Ok(4) })); - drop(one_at_a_time); - - assert_eq!(promise.await.unwrap(), None); - } -} +#[cfg(any( + all( + target_os = "macos", + feature = "livekit-cross-platform", + not(feature = "livekit-macos"), + ), + all(not(target_os = "macos"), feature = "livekit-cross-platform"), +))] +pub use cross_platform::*; diff --git a/crates/call/src/cross_platform/mod.rs b/crates/call/src/cross_platform/mod.rs new file mode 100644 index 0000000000..4a95af1525 --- /dev/null +++ b/crates/call/src/cross_platform/mod.rs @@ -0,0 +1,552 @@ +pub mod participant; +pub mod room; + +use crate::call_settings::CallSettings; +use anyhow::{anyhow, Result}; +use audio::Audio; +use client::{proto, ChannelId, Client, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE}; +use collections::HashSet; +use futures::{channel::oneshot, future::Shared, Future, FutureExt}; +use gpui::{ + AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, ModelContext, Subscription, + Task, WeakModel, +}; +use postage::watch; +use project::Project; +use room::Event; +use settings::Settings; +use std::sync::Arc; + +pub use livekit_client::{ + track::RemoteVideoTrack, RemoteVideoTrackView, RemoteVideoTrackViewEvent, +}; +pub use participant::ParticipantLocation; +pub use room::Room; + +struct GlobalActiveCall(Model); + +impl Global for GlobalActiveCall {} + +pub fn init(client: Arc, user_store: Model, cx: &mut AppContext) { + livekit_client::init( + cx.background_executor().dispatcher.clone(), + cx.http_client(), + ); + CallSettings::register(cx); + + let active_call = cx.new_model(|cx| ActiveCall::new(client, user_store, cx)); + cx.set_global(GlobalActiveCall(active_call)); +} + +pub struct OneAtATime { + cancel: Option>, +} + +impl OneAtATime { + /// spawn a task in the given context. + /// if another task is spawned before that resolves, or if the OneAtATime itself is dropped, the first task will be cancelled and return Ok(None) + /// otherwise you'll see the result of the task. + fn spawn(&mut self, cx: &mut AppContext, f: F) -> Task>> + where + F: 'static + FnOnce(AsyncAppContext) -> Fut, + Fut: Future>, + R: 'static, + { + let (tx, rx) = oneshot::channel(); + self.cancel.replace(tx); + cx.spawn(|cx| async move { + futures::select_biased! { + _ = rx.fuse() => Ok(None), + result = f(cx).fuse() => result.map(Some), + } + }) + } + + fn running(&self) -> bool { + self.cancel + .as_ref() + .is_some_and(|cancel| !cancel.is_canceled()) + } +} + +#[derive(Clone)] +pub struct IncomingCall { + pub room_id: u64, + pub calling_user: Arc, + pub participants: Vec>, + pub initial_project: Option, +} + +/// Singleton global maintaining the user's participation in a room across workspaces. +pub struct ActiveCall { + room: Option<(Model, Vec)>, + pending_room_creation: Option, Arc>>>>, + location: Option>, + _join_debouncer: OneAtATime, + pending_invites: HashSet, + incoming_call: ( + watch::Sender>, + watch::Receiver>, + ), + client: Arc, + user_store: Model, + _subscriptions: Vec, +} + +impl EventEmitter for ActiveCall {} + +impl ActiveCall { + fn new(client: Arc, user_store: Model, cx: &mut ModelContext) -> Self { + Self { + room: None, + pending_room_creation: None, + location: None, + pending_invites: Default::default(), + incoming_call: watch::channel(), + _join_debouncer: OneAtATime { cancel: None }, + _subscriptions: vec![ + client.add_request_handler(cx.weak_model(), Self::handle_incoming_call), + client.add_message_handler(cx.weak_model(), Self::handle_call_canceled), + ], + client, + user_store, + } + } + + pub fn channel_id(&self, cx: &AppContext) -> Option { + self.room()?.read(cx).channel_id() + } + + async fn handle_incoming_call( + this: Model, + envelope: TypedEnvelope, + mut cx: AsyncAppContext, + ) -> Result { + let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?; + let call = IncomingCall { + room_id: envelope.payload.room_id, + participants: user_store + .update(&mut cx, |user_store, cx| { + user_store.get_users(envelope.payload.participant_user_ids, cx) + })? + .await?, + calling_user: user_store + .update(&mut cx, |user_store, cx| { + user_store.get_user(envelope.payload.calling_user_id, cx) + })? + .await?, + initial_project: envelope.payload.initial_project, + }; + this.update(&mut cx, |this, _| { + *this.incoming_call.0.borrow_mut() = Some(call); + })?; + + Ok(proto::Ack {}) + } + + async fn handle_call_canceled( + this: Model, + envelope: TypedEnvelope, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |this, _| { + let mut incoming_call = this.incoming_call.0.borrow_mut(); + if incoming_call + .as_ref() + .map_or(false, |call| call.room_id == envelope.payload.room_id) + { + incoming_call.take(); + } + })?; + Ok(()) + } + + pub fn global(cx: &AppContext) -> Model { + cx.global::().0.clone() + } + + pub fn try_global(cx: &AppContext) -> Option> { + cx.try_global::() + .map(|call| call.0.clone()) + } + + pub fn invite( + &mut self, + called_user_id: u64, + initial_project: Option>, + cx: &mut ModelContext, + ) -> Task> { + if !self.pending_invites.insert(called_user_id) { + return Task::ready(Err(anyhow!("user was already invited"))); + } + cx.notify(); + + if self._join_debouncer.running() { + return Task::ready(Ok(())); + } + + let room = if let Some(room) = self.room().cloned() { + Some(Task::ready(Ok(room)).shared()) + } else { + self.pending_room_creation.clone() + }; + + let invite = if let Some(room) = room { + cx.spawn(move |_, mut cx| async move { + let room = room.await.map_err(|err| anyhow!("{:?}", err))?; + + let initial_project_id = if let Some(initial_project) = initial_project { + Some( + room.update(&mut cx, |room, cx| room.share_project(initial_project, cx))? + .await?, + ) + } else { + None + }; + + room.update(&mut cx, move |room, cx| { + room.call(called_user_id, initial_project_id, cx) + })? + .await?; + + anyhow::Ok(()) + }) + } else { + let client = self.client.clone(); + let user_store = self.user_store.clone(); + let room = cx + .spawn(move |this, mut cx| async move { + let create_room = async { + let room = cx + .update(|cx| { + Room::create( + called_user_id, + initial_project, + client, + user_store, + cx, + ) + })? + .await?; + + this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx))? + .await?; + + anyhow::Ok(room) + }; + + let room = create_room.await; + this.update(&mut cx, |this, _| this.pending_room_creation = None)?; + room.map_err(Arc::new) + }) + .shared(); + self.pending_room_creation = Some(room.clone()); + cx.background_executor().spawn(async move { + room.await.map_err(|err| anyhow!("{:?}", err))?; + anyhow::Ok(()) + }) + }; + + cx.spawn(move |this, mut cx| async move { + let result = invite.await; + if result.is_ok() { + this.update(&mut cx, |this, cx| this.report_call_event("invite", cx))?; + } else { + //TODO: report collaboration error + log::error!("invite failed: {:?}", result); + } + + this.update(&mut cx, |this, cx| { + this.pending_invites.remove(&called_user_id); + cx.notify(); + })?; + result + }) + } + + pub fn cancel_invite( + &mut self, + called_user_id: u64, + cx: &mut ModelContext, + ) -> Task> { + let room_id = if let Some(room) = self.room() { + room.read(cx).id() + } else { + return Task::ready(Err(anyhow!("no active call"))); + }; + + let client = self.client.clone(); + cx.background_executor().spawn(async move { + client + .request(proto::CancelCall { + room_id, + called_user_id, + }) + .await?; + anyhow::Ok(()) + }) + } + + pub fn incoming(&self) -> watch::Receiver> { + self.incoming_call.1.clone() + } + + pub fn accept_incoming(&mut self, cx: &mut ModelContext) -> Task> { + if self.room.is_some() { + return Task::ready(Err(anyhow!("cannot join while on another call"))); + } + + let call = if let Some(call) = self.incoming_call.0.borrow_mut().take() { + call + } else { + return Task::ready(Err(anyhow!("no incoming call"))); + }; + + if self.pending_room_creation.is_some() { + return Task::ready(Ok(())); + } + + let room_id = call.room_id; + let client = self.client.clone(); + let user_store = self.user_store.clone(); + let join = self + ._join_debouncer + .spawn(cx, move |cx| Room::join(room_id, client, user_store, cx)); + + cx.spawn(|this, mut cx| async move { + let room = join.await?; + this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))? + .await?; + this.update(&mut cx, |this, cx| { + this.report_call_event("accept incoming", cx) + })?; + Ok(()) + }) + } + + pub fn decline_incoming(&mut self, _: &mut ModelContext) -> Result<()> { + let call = self + .incoming_call + .0 + .borrow_mut() + .take() + .ok_or_else(|| anyhow!("no incoming call"))?; + report_call_event_for_room("decline incoming", call.room_id, None, &self.client); + self.client.send(proto::DeclineCall { + room_id: call.room_id, + })?; + Ok(()) + } + + pub fn join_channel( + &mut self, + channel_id: ChannelId, + cx: &mut ModelContext, + ) -> Task>>> { + if let Some(room) = self.room().cloned() { + if room.read(cx).channel_id() == Some(channel_id) { + return Task::ready(Ok(Some(room))); + } else { + room.update(cx, |room, cx| room.clear_state(cx)); + } + } + + if self.pending_room_creation.is_some() { + return Task::ready(Ok(None)); + } + + let client = self.client.clone(); + let user_store = self.user_store.clone(); + let join = self._join_debouncer.spawn(cx, move |cx| async move { + Room::join_channel(channel_id, client, user_store, cx).await + }); + + cx.spawn(|this, mut cx| async move { + let room = join.await?; + this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))? + .await?; + this.update(&mut cx, |this, cx| { + this.report_call_event("join channel", cx) + })?; + Ok(room) + }) + } + + pub fn hang_up(&mut self, cx: &mut ModelContext) -> Task> { + cx.notify(); + self.report_call_event("hang up", cx); + + Audio::end_call(cx); + + let channel_id = self.channel_id(cx); + if let Some((room, _)) = self.room.take() { + cx.emit(Event::RoomLeft { channel_id }); + room.update(cx, |room, cx| room.leave(cx)) + } else { + Task::ready(Ok(())) + } + } + + pub fn share_project( + &mut self, + project: Model, + cx: &mut ModelContext, + ) -> Task> { + if let Some((room, _)) = self.room.as_ref() { + self.report_call_event("share project", cx); + room.update(cx, |room, cx| room.share_project(project, cx)) + } else { + Task::ready(Err(anyhow!("no active call"))) + } + } + + pub fn unshare_project( + &mut self, + project: Model, + cx: &mut ModelContext, + ) -> Result<()> { + if let Some((room, _)) = self.room.as_ref() { + self.report_call_event("unshare project", cx); + room.update(cx, |room, cx| room.unshare_project(project, cx)) + } else { + Err(anyhow!("no active call")) + } + } + + pub fn location(&self) -> Option<&WeakModel> { + self.location.as_ref() + } + + pub fn set_location( + &mut self, + project: Option<&Model>, + cx: &mut ModelContext, + ) -> Task> { + if project.is_some() || !*ZED_ALWAYS_ACTIVE { + self.location = project.map(|project| project.downgrade()); + if let Some((room, _)) = self.room.as_ref() { + return room.update(cx, |room, cx| room.set_location(project, cx)); + } + } + Task::ready(Ok(())) + } + + fn set_room( + &mut self, + room: Option>, + cx: &mut ModelContext, + ) -> Task> { + if room.as_ref() == self.room.as_ref().map(|room| &room.0) { + Task::ready(Ok(())) + } else { + cx.notify(); + if let Some(room) = room { + if room.read(cx).status().is_offline() { + self.room = None; + Task::ready(Ok(())) + } else { + let subscriptions = vec![ + cx.observe(&room, |this, room, cx| { + if room.read(cx).status().is_offline() { + this.set_room(None, cx).detach_and_log_err(cx); + } + + cx.notify(); + }), + cx.subscribe(&room, |_, _, event, cx| cx.emit(event.clone())), + ]; + self.room = Some((room.clone(), subscriptions)); + let location = self + .location + .as_ref() + .and_then(|location| location.upgrade()); + let channel_id = room.read(cx).channel_id(); + cx.emit(Event::RoomJoined { channel_id }); + room.update(cx, |room, cx| room.set_location(location.as_ref(), cx)) + } + } else { + self.room = None; + Task::ready(Ok(())) + } + } + } + + pub fn room(&self) -> Option<&Model> { + self.room.as_ref().map(|(room, _)| room) + } + + pub fn client(&self) -> Arc { + self.client.clone() + } + + pub fn pending_invites(&self) -> &HashSet { + &self.pending_invites + } + + pub fn report_call_event(&self, operation: &'static str, cx: &mut AppContext) { + if let Some(room) = self.room() { + let room = room.read(cx); + report_call_event_for_room(operation, room.id(), room.channel_id(), &self.client); + } + } +} + +pub fn report_call_event_for_room( + operation: &'static str, + room_id: u64, + channel_id: Option, + client: &Arc, +) { + let telemetry = client.telemetry(); + + telemetry.report_call_event(operation, Some(room_id), channel_id) +} + +pub fn report_call_event_for_channel( + operation: &'static str, + channel_id: ChannelId, + client: &Arc, + cx: &AppContext, +) { + let room = ActiveCall::global(cx).read(cx).room(); + + let telemetry = client.telemetry(); + + telemetry.report_call_event(operation, room.map(|r| r.read(cx).id()), Some(channel_id)) +} + +#[cfg(test)] +mod test { + use gpui::TestAppContext; + + use crate::OneAtATime; + + #[gpui::test] + async fn test_one_at_a_time(cx: &mut TestAppContext) { + let mut one_at_a_time = OneAtATime { cancel: None }; + + assert_eq!( + cx.update(|cx| one_at_a_time.spawn(cx, |_| async { Ok(1) })) + .await + .unwrap(), + Some(1) + ); + + let (a, b) = cx.update(|cx| { + ( + one_at_a_time.spawn(cx, |_| async { + panic!(""); + }), + one_at_a_time.spawn(cx, |_| async { Ok(3) }), + ) + }); + + assert_eq!(a.await.unwrap(), None::); + assert_eq!(b.await.unwrap(), Some(3)); + + let promise = cx.update(|cx| one_at_a_time.spawn(cx, |_| async { Ok(4) })); + drop(one_at_a_time); + + assert_eq!(promise.await.unwrap(), None); + } +} diff --git a/crates/call/src/cross_platform/participant.rs b/crates/call/src/cross_platform/participant.rs new file mode 100644 index 0000000000..2ca33be728 --- /dev/null +++ b/crates/call/src/cross_platform/participant.rs @@ -0,0 +1,68 @@ +#![cfg_attr(target_os = "windows", allow(unused))] + +use anyhow::{anyhow, Result}; +use client::{proto, ParticipantIndex, User}; +use collections::HashMap; +use gpui::WeakModel; +use livekit_client::AudioStream; +use project::Project; +use std::sync::Arc; + +#[cfg(not(target_os = "windows"))] +pub use livekit_client::id::TrackSid; +pub use livekit_client::track::{RemoteAudioTrack, RemoteVideoTrack}; + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum ParticipantLocation { + SharedProject { project_id: u64 }, + UnsharedProject, + External, +} + +impl ParticipantLocation { + pub fn from_proto(location: Option) -> Result { + match location.and_then(|l| l.variant) { + Some(proto::participant_location::Variant::SharedProject(project)) => { + Ok(Self::SharedProject { + project_id: project.id, + }) + } + Some(proto::participant_location::Variant::UnsharedProject(_)) => { + Ok(Self::UnsharedProject) + } + Some(proto::participant_location::Variant::External(_)) => Ok(Self::External), + None => Err(anyhow!("participant location was not provided")), + } + } +} + +#[derive(Clone, Default)] +pub struct LocalParticipant { + pub projects: Vec, + pub active_project: Option>, + pub role: proto::ChannelRole, +} + +pub struct RemoteParticipant { + pub user: Arc, + pub peer_id: proto::PeerId, + pub role: proto::ChannelRole, + pub projects: Vec, + pub location: ParticipantLocation, + pub participant_index: ParticipantIndex, + pub muted: bool, + pub speaking: bool, + #[cfg(not(target_os = "windows"))] + pub video_tracks: HashMap, + #[cfg(not(target_os = "windows"))] + pub audio_tracks: HashMap, +} + +impl RemoteParticipant { + pub fn has_video_tracks(&self) -> bool { + #[cfg(not(target_os = "windows"))] + return !self.video_tracks.is_empty(); + #[cfg(target_os = "windows")] + return false; + } +} diff --git a/crates/call/src/cross_platform/room.rs b/crates/call/src/cross_platform/room.rs new file mode 100644 index 0000000000..11033098f7 --- /dev/null +++ b/crates/call/src/cross_platform/room.rs @@ -0,0 +1,1771 @@ +#![cfg_attr(target_os = "windows", allow(unused))] + +use crate::{ + call_settings::CallSettings, + participant::{LocalParticipant, ParticipantLocation, RemoteParticipant}, +}; +use anyhow::{anyhow, Result}; +use audio::{Audio, Sound}; +use client::{ + proto::{self, PeerId}, + ChannelId, Client, ParticipantIndex, TypedEnvelope, User, UserStore, +}; +use collections::{BTreeMap, HashMap, HashSet}; +use fs::Fs; +use futures::{FutureExt, StreamExt}; +use gpui::{ + AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task, WeakModel, +}; +use language::LanguageRegistry; +#[cfg(not(target_os = "windows"))] +use livekit::{ + capture_local_audio_track, capture_local_video_track, + id::ParticipantIdentity, + options::{TrackPublishOptions, VideoCodec}, + play_remote_audio_track, + publication::LocalTrackPublication, + track::{TrackKind, TrackSource}, + RoomEvent, RoomOptions, +}; +#[cfg(target_os = "windows")] +use livekit::{publication::LocalTrackPublication, RoomEvent}; +use livekit_client as livekit; +use postage::{sink::Sink, stream::Stream, watch}; +use project::Project; +use settings::Settings as _; +use std::{any::Any, future::Future, mem, sync::Arc, time::Duration}; +use util::{post_inc, ResultExt, TryFutureExt}; + +pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30); + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Event { + RoomJoined { + channel_id: Option, + }, + ParticipantLocationChanged { + participant_id: proto::PeerId, + }, + RemoteVideoTracksChanged { + participant_id: proto::PeerId, + }, + RemoteAudioTracksChanged { + participant_id: proto::PeerId, + }, + RemoteProjectShared { + owner: Arc, + project_id: u64, + worktree_root_names: Vec, + }, + RemoteProjectUnshared { + project_id: u64, + }, + RemoteProjectJoined { + project_id: u64, + }, + RemoteProjectInvitationDiscarded { + project_id: u64, + }, + RoomLeft { + channel_id: Option, + }, +} + +pub struct Room { + id: u64, + channel_id: Option, + live_kit: Option, + status: RoomStatus, + shared_projects: HashSet>, + joined_projects: HashSet>, + local_participant: LocalParticipant, + remote_participants: BTreeMap, + pending_participants: Vec>, + participant_user_ids: HashSet, + pending_call_count: usize, + leave_when_empty: bool, + client: Arc, + user_store: Model, + follows_by_leader_id_project_id: HashMap<(PeerId, u64), Vec>, + client_subscriptions: Vec, + _subscriptions: Vec, + room_update_completed_tx: watch::Sender>, + room_update_completed_rx: watch::Receiver>, + pending_room_update: Option>, + maintain_connection: Option>>, +} + +impl EventEmitter for Room {} + +impl Room { + pub fn channel_id(&self) -> Option { + self.channel_id + } + + pub fn is_sharing_project(&self) -> bool { + !self.shared_projects.is_empty() + } + + #[cfg(all(any(test, feature = "test-support"), not(target_os = "windows")))] + pub fn is_connected(&self) -> bool { + if let Some(live_kit) = self.live_kit.as_ref() { + live_kit.room.connection_state() == livekit::ConnectionState::Connected + } else { + false + } + } + + fn new( + id: u64, + channel_id: Option, + livekit_connection_info: Option, + client: Arc, + user_store: Model, + cx: &mut ModelContext, + ) -> Self { + spawn_room_connection(livekit_connection_info, cx); + + let maintain_connection = cx.spawn({ + let client = client.clone(); + move |this, cx| Self::maintain_connection(this, client.clone(), cx).log_err() + }); + + Audio::play_sound(Sound::Joined, cx); + + let (room_update_completed_tx, room_update_completed_rx) = watch::channel(); + + Self { + id, + channel_id, + live_kit: None, + status: RoomStatus::Online, + shared_projects: Default::default(), + joined_projects: Default::default(), + participant_user_ids: Default::default(), + local_participant: Default::default(), + remote_participants: Default::default(), + pending_participants: Default::default(), + pending_call_count: 0, + client_subscriptions: vec![ + client.add_message_handler(cx.weak_model(), Self::handle_room_updated) + ], + _subscriptions: vec![ + cx.on_release(Self::released), + cx.on_app_quit(Self::app_will_quit), + ], + leave_when_empty: false, + pending_room_update: None, + client, + user_store, + follows_by_leader_id_project_id: Default::default(), + maintain_connection: Some(maintain_connection), + room_update_completed_tx, + room_update_completed_rx, + } + } + + pub(crate) fn create( + called_user_id: u64, + initial_project: Option>, + client: Arc, + user_store: Model, + cx: &mut AppContext, + ) -> Task>> { + cx.spawn(move |mut cx| async move { + let response = client.request(proto::CreateRoom {}).await?; + let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?; + let room = cx.new_model(|cx| { + let mut room = Self::new( + room_proto.id, + None, + response.live_kit_connection_info, + client, + user_store, + cx, + ); + if let Some(participant) = room_proto.participants.first() { + room.local_participant.role = participant.role() + } + room + })?; + + let initial_project_id = if let Some(initial_project) = initial_project { + let initial_project_id = room + .update(&mut cx, |room, cx| { + room.share_project(initial_project.clone(), cx) + })? + .await?; + Some(initial_project_id) + } else { + None + }; + + let did_join = room + .update(&mut cx, |room, cx| { + room.leave_when_empty = true; + room.call(called_user_id, initial_project_id, cx) + })? + .await; + match did_join { + Ok(()) => Ok(room), + Err(error) => Err(error.context("room creation failed")), + } + }) + } + + pub(crate) async fn join_channel( + channel_id: ChannelId, + client: Arc, + user_store: Model, + cx: AsyncAppContext, + ) -> Result> { + Self::from_join_response( + client + .request(proto::JoinChannel { + channel_id: channel_id.0, + }) + .await?, + client, + user_store, + cx, + ) + } + + pub(crate) async fn join( + room_id: u64, + client: Arc, + user_store: Model, + cx: AsyncAppContext, + ) -> Result> { + Self::from_join_response( + client.request(proto::JoinRoom { id: room_id }).await?, + client, + user_store, + cx, + ) + } + + fn released(&mut self, cx: &mut AppContext) { + if self.status.is_online() { + self.leave_internal(cx).detach_and_log_err(cx); + } + } + + fn app_will_quit(&mut self, cx: &mut ModelContext) -> impl Future { + let task = if self.status.is_online() { + let leave = self.leave_internal(cx); + Some(cx.background_executor().spawn(async move { + leave.await.log_err(); + })) + } else { + None + }; + + async move { + if let Some(task) = task { + task.await; + } + } + } + + pub fn mute_on_join(cx: &AppContext) -> bool { + CallSettings::get_global(cx).mute_on_join || client::IMPERSONATE_LOGIN.is_some() + } + + fn from_join_response( + response: proto::JoinRoomResponse, + client: Arc, + user_store: Model, + mut cx: AsyncAppContext, + ) -> Result> { + let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?; + let room = cx.new_model(|cx| { + Self::new( + room_proto.id, + response.channel_id.map(ChannelId), + response.live_kit_connection_info, + client, + user_store, + cx, + ) + })?; + room.update(&mut cx, |room, cx| { + room.leave_when_empty = room.channel_id.is_none(); + room.apply_room_update(room_proto, cx)?; + anyhow::Ok(()) + })??; + Ok(room) + } + + fn should_leave(&self) -> bool { + self.leave_when_empty + && self.pending_room_update.is_none() + && self.pending_participants.is_empty() + && self.remote_participants.is_empty() + && self.pending_call_count == 0 + } + + pub(crate) fn leave(&mut self, cx: &mut ModelContext) -> Task> { + cx.notify(); + self.leave_internal(cx) + } + + fn leave_internal(&mut self, cx: &mut AppContext) -> Task> { + if self.status.is_offline() { + return Task::ready(Err(anyhow!("room is offline"))); + } + + log::info!("leaving room"); + Audio::play_sound(Sound::Leave, cx); + + self.clear_state(cx); + + let leave_room = self.client.request(proto::LeaveRoom {}); + cx.background_executor().spawn(async move { + leave_room.await?; + anyhow::Ok(()) + }) + } + + pub(crate) fn clear_state(&mut self, cx: &mut AppContext) { + for project in self.shared_projects.drain() { + if let Some(project) = project.upgrade() { + project.update(cx, |project, cx| { + project.unshare(cx).log_err(); + }); + } + } + for project in self.joined_projects.drain() { + if let Some(project) = project.upgrade() { + project.update(cx, |project, cx| { + project.disconnected_from_host(cx); + project.close(cx); + }); + } + } + + self.status = RoomStatus::Offline; + self.remote_participants.clear(); + self.pending_participants.clear(); + self.participant_user_ids.clear(); + self.client_subscriptions.clear(); + self.live_kit.take(); + self.pending_room_update.take(); + self.maintain_connection.take(); + } + + async fn maintain_connection( + this: WeakModel, + client: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + let mut client_status = client.status(); + loop { + let _ = client_status.try_recv(); + let is_connected = client_status.borrow().is_connected(); + // Even if we're initially connected, any future change of the status means we momentarily disconnected. + if !is_connected || client_status.next().await.is_some() { + log::info!("detected client disconnection"); + + this.upgrade() + .ok_or_else(|| anyhow!("room was dropped"))? + .update(&mut cx, |this, cx| { + this.status = RoomStatus::Rejoining; + cx.notify(); + })?; + + // Wait for client to re-establish a connection to the server. + { + let mut reconnection_timeout = + cx.background_executor().timer(RECONNECT_TIMEOUT).fuse(); + let client_reconnection = async { + let mut remaining_attempts = 3; + while remaining_attempts > 0 { + if client_status.borrow().is_connected() { + log::info!("client reconnected, attempting to rejoin room"); + + let Some(this) = this.upgrade() else { break }; + match this.update(&mut cx, |this, cx| this.rejoin(cx)) { + Ok(task) => { + if task.await.log_err().is_some() { + return true; + } else { + remaining_attempts -= 1; + } + } + Err(_app_dropped) => return false, + } + } else if client_status.borrow().is_signed_out() { + return false; + } + + log::info!( + "waiting for client status change, remaining attempts {}", + remaining_attempts + ); + client_status.next().await; + } + false + } + .fuse(); + futures::pin_mut!(client_reconnection); + + futures::select_biased! { + reconnected = client_reconnection => { + if reconnected { + log::info!("successfully reconnected to room"); + // If we successfully joined the room, go back around the loop + // waiting for future connection status changes. + continue; + } + } + _ = reconnection_timeout => { + log::info!("room reconnection timeout expired"); + } + } + } + + break; + } + } + + // The client failed to re-establish a connection to the server + // or an error occurred while trying to re-join the room. Either way + // we leave the room and return an error. + if let Some(this) = this.upgrade() { + log::info!("reconnection failed, leaving room"); + this.update(&mut cx, |this, cx| this.leave(cx))?.await?; + } + Err(anyhow!( + "can't reconnect to room: client failed to re-establish connection" + )) + } + + fn rejoin(&mut self, cx: &mut ModelContext) -> Task> { + let mut projects = HashMap::default(); + let mut reshared_projects = Vec::new(); + let mut rejoined_projects = Vec::new(); + self.shared_projects.retain(|project| { + if let Some(handle) = project.upgrade() { + let project = handle.read(cx); + if let Some(project_id) = project.remote_id() { + projects.insert(project_id, handle.clone()); + reshared_projects.push(proto::UpdateProject { + project_id, + worktrees: project.worktree_metadata_protos(cx), + }); + return true; + } + } + false + }); + self.joined_projects.retain(|project| { + if let Some(handle) = project.upgrade() { + let project = handle.read(cx); + if let Some(project_id) = project.remote_id() { + projects.insert(project_id, handle.clone()); + rejoined_projects.push(proto::RejoinProject { + id: project_id, + worktrees: project + .worktrees(cx) + .map(|worktree| { + let worktree = worktree.read(cx); + proto::RejoinWorktree { + id: worktree.id().to_proto(), + scan_id: worktree.completed_scan_id() as u64, + } + }) + .collect(), + }); + } + return true; + } + false + }); + + let response = self.client.request_envelope(proto::RejoinRoom { + id: self.id, + reshared_projects, + rejoined_projects, + }); + + cx.spawn(|this, mut cx| async move { + let response = response.await?; + let message_id = response.message_id; + let response = response.payload; + let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?; + this.update(&mut cx, |this, cx| { + this.status = RoomStatus::Online; + this.apply_room_update(room_proto, cx)?; + + for reshared_project in response.reshared_projects { + if let Some(project) = projects.get(&reshared_project.id) { + project.update(cx, |project, cx| { + project.reshared(reshared_project, cx).log_err(); + }); + } + } + + for rejoined_project in response.rejoined_projects { + if let Some(project) = projects.get(&rejoined_project.id) { + project.update(cx, |project, cx| { + project.rejoined(rejoined_project, message_id, cx).log_err(); + }); + } + } + + anyhow::Ok(()) + })? + }) + } + + pub fn id(&self) -> u64 { + self.id + } + + pub fn status(&self) -> RoomStatus { + self.status + } + + pub fn local_participant(&self) -> &LocalParticipant { + &self.local_participant + } + + pub fn remote_participants(&self) -> &BTreeMap { + &self.remote_participants + } + + pub fn remote_participant_for_peer_id(&self, peer_id: PeerId) -> Option<&RemoteParticipant> { + self.remote_participants + .values() + .find(|p| p.peer_id == peer_id) + } + + pub fn role_for_user(&self, user_id: u64) -> Option { + self.remote_participants + .get(&user_id) + .map(|participant| participant.role) + } + + pub fn contains_guests(&self) -> bool { + self.local_participant.role == proto::ChannelRole::Guest + || self + .remote_participants + .values() + .any(|p| p.role == proto::ChannelRole::Guest) + } + + pub fn local_participant_is_admin(&self) -> bool { + self.local_participant.role == proto::ChannelRole::Admin + } + + pub fn local_participant_is_guest(&self) -> bool { + self.local_participant.role == proto::ChannelRole::Guest + } + + pub fn set_participant_role( + &mut self, + user_id: u64, + role: proto::ChannelRole, + cx: &ModelContext, + ) -> Task> { + let client = self.client.clone(); + let room_id = self.id; + let role = role.into(); + cx.spawn(|_, _| async move { + client + .request(proto::SetRoomParticipantRole { + room_id, + user_id, + role, + }) + .await + .map(|_| ()) + }) + } + + pub fn pending_participants(&self) -> &[Arc] { + &self.pending_participants + } + + pub fn contains_participant(&self, user_id: u64) -> bool { + self.participant_user_ids.contains(&user_id) + } + + pub fn followers_for(&self, leader_id: PeerId, project_id: u64) -> &[PeerId] { + self.follows_by_leader_id_project_id + .get(&(leader_id, project_id)) + .map_or(&[], |v| v.as_slice()) + } + + /// Returns the most 'active' projects, defined as most people in the project + pub fn most_active_project(&self, cx: &AppContext) -> Option<(u64, u64)> { + let mut project_hosts_and_guest_counts = HashMap::, u32)>::default(); + for participant in self.remote_participants.values() { + match participant.location { + ParticipantLocation::SharedProject { project_id } => { + project_hosts_and_guest_counts + .entry(project_id) + .or_default() + .1 += 1; + } + ParticipantLocation::External | ParticipantLocation::UnsharedProject => {} + } + for project in &participant.projects { + project_hosts_and_guest_counts + .entry(project.id) + .or_default() + .0 = Some(participant.user.id); + } + } + + if let Some(user) = self.user_store.read(cx).current_user() { + for project in &self.local_participant.projects { + project_hosts_and_guest_counts + .entry(project.id) + .or_default() + .0 = Some(user.id); + } + } + + project_hosts_and_guest_counts + .into_iter() + .filter_map(|(id, (host, guest_count))| Some((id, host?, guest_count))) + .max_by_key(|(_, _, guest_count)| *guest_count) + .map(|(id, host, _)| (id, host)) + } + + async fn handle_room_updated( + this: Model, + envelope: TypedEnvelope, + mut cx: AsyncAppContext, + ) -> Result<()> { + let room = envelope + .payload + .room + .ok_or_else(|| anyhow!("invalid room"))?; + this.update(&mut cx, |this, cx| this.apply_room_update(room, cx))? + } + + fn apply_room_update(&mut self, room: proto::Room, cx: &mut ModelContext) -> Result<()> { + log::trace!( + "client {:?}. room update: {:?}", + self.client.user_id(), + &room + ); + + self.pending_room_update = Some(self.start_room_connection(room, cx)); + + cx.notify(); + Ok(()) + } + + pub fn room_update_completed(&mut self) -> impl Future { + let mut done_rx = self.room_update_completed_rx.clone(); + async move { + while let Some(result) = done_rx.next().await { + if result.is_some() { + break; + } + } + } + } + + #[cfg(target_os = "windows")] + fn start_room_connection( + &self, + mut room: proto::Room, + cx: &mut ModelContext, + ) -> Task<()> { + Task::ready(()) + } + + #[cfg(not(target_os = "windows"))] + fn start_room_connection( + &self, + mut room: proto::Room, + cx: &mut ModelContext, + ) -> Task<()> { + // Filter ourselves out from the room's participants. + let local_participant_ix = room + .participants + .iter() + .position(|participant| Some(participant.user_id) == self.client.user_id()); + let local_participant = local_participant_ix.map(|ix| room.participants.swap_remove(ix)); + + let pending_participant_user_ids = room + .pending_participants + .iter() + .map(|p| p.user_id) + .collect::>(); + + let remote_participant_user_ids = room + .participants + .iter() + .map(|p| p.user_id) + .collect::>(); + + let (remote_participants, pending_participants) = + self.user_store.update(cx, move |user_store, cx| { + ( + user_store.get_users(remote_participant_user_ids, cx), + user_store.get_users(pending_participant_user_ids, cx), + ) + }); + cx.spawn(|this, mut cx| async move { + let (remote_participants, pending_participants) = + futures::join!(remote_participants, pending_participants); + + this.update(&mut cx, |this, cx| { + this.participant_user_ids.clear(); + + if let Some(participant) = local_participant { + let role = participant.role(); + this.local_participant.projects = participant.projects; + if this.local_participant.role != role { + this.local_participant.role = role; + + if role == proto::ChannelRole::Guest { + for project in mem::take(&mut this.shared_projects) { + if let Some(project) = project.upgrade() { + this.unshare_project(project, cx).log_err(); + } + } + this.local_participant.projects.clear(); + if let Some(livekit_room) = &mut this.live_kit { + livekit_room.stop_publishing(cx); + } + } + + this.joined_projects.retain(|project| { + if let Some(project) = project.upgrade() { + project.update(cx, |project, cx| project.set_role(role, cx)); + true + } else { + false + } + }); + } + } else { + this.local_participant.projects.clear(); + } + + let livekit_participants = this + .live_kit + .as_ref() + .map(|live_kit| live_kit.room.remote_participants()); + + if let Some(participants) = remote_participants.log_err() { + for (participant, user) in room.participants.into_iter().zip(participants) { + let Some(peer_id) = participant.peer_id else { + continue; + }; + let participant_index = ParticipantIndex(participant.participant_index); + this.participant_user_ids.insert(participant.user_id); + + let old_projects = this + .remote_participants + .get(&participant.user_id) + .into_iter() + .flat_map(|existing| &existing.projects) + .map(|project| project.id) + .collect::>(); + let new_projects = participant + .projects + .iter() + .map(|project| project.id) + .collect::>(); + + for project in &participant.projects { + if !old_projects.contains(&project.id) { + cx.emit(Event::RemoteProjectShared { + owner: user.clone(), + project_id: project.id, + worktree_root_names: project.worktree_root_names.clone(), + }); + } + } + + for unshared_project_id in old_projects.difference(&new_projects) { + this.joined_projects.retain(|project| { + if let Some(project) = project.upgrade() { + project.update(cx, |project, cx| { + if project.remote_id() == Some(*unshared_project_id) { + project.disconnected_from_host(cx); + false + } else { + true + } + }) + } else { + false + } + }); + cx.emit(Event::RemoteProjectUnshared { + project_id: *unshared_project_id, + }); + } + + let role = participant.role(); + let location = ParticipantLocation::from_proto(participant.location) + .unwrap_or(ParticipantLocation::External); + if let Some(remote_participant) = + this.remote_participants.get_mut(&participant.user_id) + { + remote_participant.peer_id = peer_id; + remote_participant.projects = participant.projects; + remote_participant.participant_index = participant_index; + if location != remote_participant.location + || role != remote_participant.role + { + remote_participant.location = location; + remote_participant.role = role; + cx.emit(Event::ParticipantLocationChanged { + participant_id: peer_id, + }); + } + } else { + this.remote_participants.insert( + participant.user_id, + RemoteParticipant { + user: user.clone(), + participant_index, + peer_id, + projects: participant.projects, + location, + role, + muted: true, + speaking: false, + video_tracks: Default::default(), + #[cfg(not(target_os = "windows"))] + audio_tracks: Default::default(), + }, + ); + + Audio::play_sound(Sound::Joined, cx); + if let Some(livekit_participants) = &livekit_participants { + if let Some(livekit_participant) = livekit_participants + .get(&ParticipantIdentity(user.id.to_string())) + { + for publication in + livekit_participant.track_publications().into_values() + { + if let Some(track) = publication.track() { + this.livekit_room_updated( + RoomEvent::TrackSubscribed { + track, + publication, + participant: livekit_participant.clone(), + }, + cx, + ) + .warn_on_err(); + } + } + } + } + } + } + + this.remote_participants.retain(|user_id, participant| { + if this.participant_user_ids.contains(user_id) { + true + } else { + for project in &participant.projects { + cx.emit(Event::RemoteProjectUnshared { + project_id: project.id, + }); + } + false + } + }); + } + + if let Some(pending_participants) = pending_participants.log_err() { + this.pending_participants = pending_participants; + for participant in &this.pending_participants { + this.participant_user_ids.insert(participant.id); + } + } + + this.follows_by_leader_id_project_id.clear(); + for follower in room.followers { + let project_id = follower.project_id; + let (leader, follower) = match (follower.leader_id, follower.follower_id) { + (Some(leader), Some(follower)) => (leader, follower), + + _ => { + log::error!("Follower message {follower:?} missing some state"); + continue; + } + }; + + let list = this + .follows_by_leader_id_project_id + .entry((leader, project_id)) + .or_default(); + if !list.contains(&follower) { + list.push(follower); + } + } + + this.pending_room_update.take(); + if this.should_leave() { + log::info!("room is empty, leaving"); + this.leave(cx).detach(); + } + + this.user_store.update(cx, |user_store, cx| { + let participant_indices_by_user_id = this + .remote_participants + .iter() + .map(|(user_id, participant)| (*user_id, participant.participant_index)) + .collect(); + user_store.set_participant_indices(participant_indices_by_user_id, cx); + }); + + this.check_invariants(); + this.room_update_completed_tx.try_send(Some(())).ok(); + cx.notify(); + }) + .ok(); + }) + } + + fn livekit_room_updated( + &mut self, + event: RoomEvent, + cx: &mut ModelContext, + ) -> Result<()> { + log::trace!( + "client {:?}. livekit event: {:?}", + self.client.user_id(), + &event + ); + + match event { + #[cfg(not(target_os = "windows"))] + RoomEvent::TrackSubscribed { + track, + participant, + publication, + } => { + let user_id = participant.identity().0.parse()?; + let track_id = track.sid(); + let participant = self.remote_participants.get_mut(&user_id).ok_or_else(|| { + anyhow!( + "{:?} subscribed to track by unknown participant {user_id}", + self.client.user_id() + ) + })?; + if self.live_kit.as_ref().map_or(true, |kit| kit.deafened) { + track.rtc_track().set_enabled(false); + } + match track { + livekit::track::RemoteTrack::Audio(track) => { + cx.emit(Event::RemoteAudioTracksChanged { + participant_id: participant.peer_id, + }); + let stream = play_remote_audio_track(&track, cx.background_executor())?; + participant.audio_tracks.insert(track_id, (track, stream)); + participant.muted = publication.is_muted(); + } + livekit::track::RemoteTrack::Video(track) => { + cx.emit(Event::RemoteVideoTracksChanged { + participant_id: participant.peer_id, + }); + participant.video_tracks.insert(track_id, track); + } + } + } + + #[cfg(not(target_os = "windows"))] + RoomEvent::TrackUnsubscribed { + track, participant, .. + } => { + let user_id = participant.identity().0.parse()?; + let participant = self.remote_participants.get_mut(&user_id).ok_or_else(|| { + anyhow!( + "{:?}, unsubscribed from track by unknown participant {user_id}", + self.client.user_id() + ) + })?; + match track { + livekit::track::RemoteTrack::Audio(track) => { + participant.audio_tracks.remove(&track.sid()); + participant.muted = true; + cx.emit(Event::RemoteAudioTracksChanged { + participant_id: participant.peer_id, + }); + } + livekit::track::RemoteTrack::Video(track) => { + participant.video_tracks.remove(&track.sid()); + cx.emit(Event::RemoteVideoTracksChanged { + participant_id: participant.peer_id, + }); + } + } + } + + #[cfg(not(target_os = "windows"))] + RoomEvent::ActiveSpeakersChanged { speakers } => { + let mut speaker_ids = speakers + .into_iter() + .filter_map(|speaker| speaker.identity().0.parse().ok()) + .collect::>(); + speaker_ids.sort_unstable(); + for (sid, participant) in &mut self.remote_participants { + participant.speaking = speaker_ids.binary_search(sid).is_ok(); + } + if let Some(id) = self.client.user_id() { + if let Some(room) = &mut self.live_kit { + room.speaking = speaker_ids.binary_search(&id).is_ok(); + } + } + } + + #[cfg(not(target_os = "windows"))] + RoomEvent::TrackMuted { + participant, + publication, + } + | RoomEvent::TrackUnmuted { + participant, + publication, + } => { + let mut found = false; + let user_id = participant.identity().0.parse()?; + let track_id = publication.sid(); + if let Some(participant) = self.remote_participants.get_mut(&user_id) { + for (track, _) in participant.audio_tracks.values() { + if track.sid() == track_id { + found = true; + break; + } + } + if found { + participant.muted = publication.is_muted(); + } + } + } + + #[cfg(not(target_os = "windows"))] + RoomEvent::LocalTrackUnpublished { publication, .. } => { + log::info!("unpublished track {}", publication.sid()); + if let Some(room) = &mut self.live_kit { + if let LocalTrack::Published { + track_publication, .. + } = &room.microphone_track + { + if track_publication.sid() == publication.sid() { + room.microphone_track = LocalTrack::None; + } + } + if let LocalTrack::Published { + track_publication, .. + } = &room.screen_track + { + if track_publication.sid() == publication.sid() { + room.screen_track = LocalTrack::None; + } + } + } + } + + #[cfg(not(target_os = "windows"))] + RoomEvent::LocalTrackPublished { publication, .. } => { + log::info!("published track {:?}", publication.sid()); + } + + #[cfg(not(target_os = "windows"))] + RoomEvent::Disconnected { reason } => { + log::info!("disconnected from room: {reason:?}"); + self.leave(cx).detach_and_log_err(cx); + } + _ => {} + } + + cx.notify(); + Ok(()) + } + + fn check_invariants(&self) { + #[cfg(any(test, feature = "test-support"))] + { + for participant in self.remote_participants.values() { + assert!(self.participant_user_ids.contains(&participant.user.id)); + assert_ne!(participant.user.id, self.client.user_id().unwrap()); + } + + for participant in &self.pending_participants { + assert!(self.participant_user_ids.contains(&participant.id)); + assert_ne!(participant.id, self.client.user_id().unwrap()); + } + + assert_eq!( + self.participant_user_ids.len(), + self.remote_participants.len() + self.pending_participants.len() + ); + } + } + + pub(crate) fn call( + &mut self, + called_user_id: u64, + initial_project_id: Option, + cx: &mut ModelContext, + ) -> Task> { + if self.status.is_offline() { + return Task::ready(Err(anyhow!("room is offline"))); + } + + cx.notify(); + let client = self.client.clone(); + let room_id = self.id; + self.pending_call_count += 1; + cx.spawn(move |this, mut cx| async move { + let result = client + .request(proto::Call { + room_id, + called_user_id, + initial_project_id, + }) + .await; + this.update(&mut cx, |this, cx| { + this.pending_call_count -= 1; + if this.should_leave() { + this.leave(cx).detach_and_log_err(cx); + } + })?; + result?; + Ok(()) + }) + } + + pub fn join_project( + &mut self, + id: u64, + language_registry: Arc, + fs: Arc, + cx: &mut ModelContext, + ) -> Task>> { + let client = self.client.clone(); + let user_store = self.user_store.clone(); + cx.emit(Event::RemoteProjectJoined { project_id: id }); + cx.spawn(move |this, mut cx| async move { + let project = + Project::in_room(id, client, user_store, language_registry, fs, cx.clone()).await?; + + this.update(&mut cx, |this, cx| { + this.joined_projects.retain(|project| { + if let Some(project) = project.upgrade() { + !project.read(cx).is_disconnected(cx) + } else { + false + } + }); + this.joined_projects.insert(project.downgrade()); + })?; + Ok(project) + }) + } + + pub fn share_project( + &mut self, + project: Model, + cx: &mut ModelContext, + ) -> Task> { + if let Some(project_id) = project.read(cx).remote_id() { + return Task::ready(Ok(project_id)); + } + + let request = self.client.request(proto::ShareProject { + room_id: self.id(), + worktrees: project.read(cx).worktree_metadata_protos(cx), + is_ssh_project: project.read(cx).is_via_ssh(), + }); + + cx.spawn(|this, mut cx| async move { + let response = request.await?; + + project.update(&mut cx, |project, cx| { + project.shared(response.project_id, cx) + })??; + + // If the user's location is in this project, it changes from UnsharedProject to SharedProject. + this.update(&mut cx, |this, cx| { + this.shared_projects.insert(project.downgrade()); + let active_project = this.local_participant.active_project.as_ref(); + if active_project.map_or(false, |location| *location == project) { + this.set_location(Some(&project), cx) + } else { + Task::ready(Ok(())) + } + })? + .await?; + + Ok(response.project_id) + }) + } + + pub(crate) fn unshare_project( + &mut self, + project: Model, + cx: &mut ModelContext, + ) -> Result<()> { + let project_id = match project.read(cx).remote_id() { + Some(project_id) => project_id, + None => return Ok(()), + }; + + self.client.send(proto::UnshareProject { project_id })?; + project.update(cx, |this, cx| this.unshare(cx))?; + + if self.local_participant.active_project == Some(project.downgrade()) { + self.set_location(Some(&project), cx).detach_and_log_err(cx); + } + Ok(()) + } + + pub(crate) fn set_location( + &mut self, + project: Option<&Model>, + cx: &mut ModelContext, + ) -> Task> { + if self.status.is_offline() { + return Task::ready(Err(anyhow!("room is offline"))); + } + + let client = self.client.clone(); + let room_id = self.id; + let location = if let Some(project) = project { + self.local_participant.active_project = Some(project.downgrade()); + if let Some(project_id) = project.read(cx).remote_id() { + proto::participant_location::Variant::SharedProject( + proto::participant_location::SharedProject { id: project_id }, + ) + } else { + proto::participant_location::Variant::UnsharedProject( + proto::participant_location::UnsharedProject {}, + ) + } + } else { + self.local_participant.active_project = None; + proto::participant_location::Variant::External(proto::participant_location::External {}) + }; + + cx.notify(); + cx.background_executor().spawn(async move { + client + .request(proto::UpdateParticipantLocation { + room_id, + location: Some(proto::ParticipantLocation { + variant: Some(location), + }), + }) + .await?; + Ok(()) + }) + } + + pub fn is_screen_sharing(&self) -> bool { + self.live_kit.as_ref().map_or(false, |live_kit| { + !matches!(live_kit.screen_track, LocalTrack::None) + }) + } + + pub fn is_sharing_mic(&self) -> bool { + self.live_kit.as_ref().map_or(false, |live_kit| { + !matches!(live_kit.microphone_track, LocalTrack::None) + }) + } + + pub fn is_muted(&self) -> bool { + self.live_kit.as_ref().map_or(false, |live_kit| { + matches!(live_kit.microphone_track, LocalTrack::None) + || live_kit.muted_by_user + || live_kit.deafened + }) + } + + pub fn is_speaking(&self) -> bool { + self.live_kit + .as_ref() + .map_or(false, |live_kit| live_kit.speaking) + } + + pub fn is_deafened(&self) -> Option { + self.live_kit.as_ref().map(|live_kit| live_kit.deafened) + } + + pub fn can_use_microphone(&self, _cx: &AppContext) -> bool { + use proto::ChannelRole::*; + + #[cfg(not(any(test, feature = "test-support")))] + { + use feature_flags::FeatureFlagAppExt as _; + if cfg!(target_os = "windows") || (cfg!(target_os = "linux") && !_cx.is_staff()) { + return false; + } + } + + match self.local_participant.role { + Admin | Member | Talker => true, + Guest | Banned => false, + } + } + + pub fn can_share_projects(&self) -> bool { + use proto::ChannelRole::*; + match self.local_participant.role { + Admin | Member => true, + Guest | Banned | Talker => false, + } + } + + #[cfg(target_os = "windows")] + pub fn share_microphone(&mut self, cx: &mut ModelContext) -> Task> { + Task::ready(Err(anyhow!("Windows is not supported yet"))) + } + + #[cfg(not(target_os = "windows"))] + #[track_caller] + pub fn share_microphone(&mut self, cx: &mut ModelContext) -> Task> { + if self.status.is_offline() { + return Task::ready(Err(anyhow!("room is offline"))); + } + + let (participant, publish_id) = if let Some(live_kit) = self.live_kit.as_mut() { + let publish_id = post_inc(&mut live_kit.next_publish_id); + live_kit.microphone_track = LocalTrack::Pending { publish_id }; + cx.notify(); + (live_kit.room.local_participant(), publish_id) + } else { + return Task::ready(Err(anyhow!("live-kit was not initialized"))); + }; + + cx.spawn(move |this, mut cx| async move { + let (track, stream) = capture_local_audio_track(cx.background_executor())?.await; + + let publication = participant + .publish_track( + livekit::track::LocalTrack::Audio(track), + TrackPublishOptions { + source: TrackSource::Microphone, + ..Default::default() + }, + ) + .await + .map_err(|error| anyhow!("failed to publish track: {error}")); + this.update(&mut cx, |this, cx| { + let live_kit = this + .live_kit + .as_mut() + .ok_or_else(|| anyhow!("live-kit was not initialized"))?; + + let canceled = if let LocalTrack::Pending { + publish_id: cur_publish_id, + } = &live_kit.microphone_track + { + *cur_publish_id != publish_id + } else { + true + }; + + match publication { + Ok(publication) => { + if canceled { + cx.background_executor() + .spawn(async move { + participant.unpublish_track(&publication.sid()).await + }) + .detach_and_log_err(cx) + } else { + if live_kit.muted_by_user || live_kit.deafened { + publication.mute(); + } + live_kit.microphone_track = LocalTrack::Published { + track_publication: publication, + _stream: Box::new(stream), + }; + cx.notify(); + } + Ok(()) + } + Err(error) => { + if canceled { + Ok(()) + } else { + live_kit.microphone_track = LocalTrack::None; + cx.notify(); + Err(error) + } + } + } + })? + }) + } + + #[cfg(target_os = "windows")] + pub fn share_screen(&mut self, cx: &mut ModelContext) -> Task> { + Task::ready(Err(anyhow!("Windows is not supported yet"))) + } + + #[cfg(not(target_os = "windows"))] + pub fn share_screen(&mut self, cx: &mut ModelContext) -> Task> { + if self.status.is_offline() { + return Task::ready(Err(anyhow!("room is offline"))); + } + if self.is_screen_sharing() { + return Task::ready(Err(anyhow!("screen was already shared"))); + } + + let (participant, publish_id) = if let Some(live_kit) = self.live_kit.as_mut() { + let publish_id = post_inc(&mut live_kit.next_publish_id); + live_kit.screen_track = LocalTrack::Pending { publish_id }; + cx.notify(); + (live_kit.room.local_participant(), publish_id) + } else { + return Task::ready(Err(anyhow!("live-kit was not initialized"))); + }; + + let sources = cx.screen_capture_sources(); + + cx.spawn(move |this, mut cx| async move { + let sources = sources.await??; + let source = sources.first().ok_or_else(|| anyhow!("no display found"))?; + + let (track, stream) = capture_local_video_track(&**source).await?; + + let publication = participant + .publish_track( + livekit::track::LocalTrack::Video(track), + TrackPublishOptions { + source: TrackSource::Screenshare, + video_codec: VideoCodec::H264, + ..Default::default() + }, + ) + .await + .map_err(|error| anyhow!("error publishing screen track {error:?}")); + + this.update(&mut cx, |this, cx| { + let live_kit = this + .live_kit + .as_mut() + .ok_or_else(|| anyhow!("live-kit was not initialized"))?; + + let canceled = if let LocalTrack::Pending { + publish_id: cur_publish_id, + } = &live_kit.screen_track + { + *cur_publish_id != publish_id + } else { + true + }; + + match publication { + Ok(publication) => { + if canceled { + cx.background_executor() + .spawn(async move { + participant.unpublish_track(&publication.sid()).await + }) + .detach() + } else { + live_kit.screen_track = LocalTrack::Published { + track_publication: publication, + _stream: Box::new(stream), + }; + cx.notify(); + } + + Audio::play_sound(Sound::StartScreenshare, cx); + Ok(()) + } + Err(error) => { + if canceled { + Ok(()) + } else { + live_kit.screen_track = LocalTrack::None; + cx.notify(); + Err(error) + } + } + } + })? + }) + } + + pub fn toggle_mute(&mut self, cx: &mut ModelContext) { + if let Some(live_kit) = self.live_kit.as_mut() { + // When unmuting, undeafen if the user was deafened before. + let was_deafened = live_kit.deafened; + if live_kit.muted_by_user + || live_kit.deafened + || matches!(live_kit.microphone_track, LocalTrack::None) + { + live_kit.muted_by_user = false; + live_kit.deafened = false; + } else { + live_kit.muted_by_user = true; + } + let muted = live_kit.muted_by_user; + let should_undeafen = was_deafened && !live_kit.deafened; + + if let Some(task) = self.set_mute(muted, cx) { + task.detach_and_log_err(cx); + } + + if should_undeafen { + self.set_deafened(false, cx); + } + } + } + + pub fn toggle_deafen(&mut self, cx: &mut ModelContext) { + if let Some(live_kit) = self.live_kit.as_mut() { + // When deafening, mute the microphone if it was not already muted. + // When un-deafening, unmute the microphone, unless it was explicitly muted. + let deafened = !live_kit.deafened; + live_kit.deafened = deafened; + let should_change_mute = !live_kit.muted_by_user; + + self.set_deafened(deafened, cx); + + if should_change_mute { + if let Some(task) = self.set_mute(deafened, cx) { + task.detach_and_log_err(cx); + } + } + } + } + + pub fn unshare_screen(&mut self, cx: &mut ModelContext) -> Result<()> { + if self.status.is_offline() { + return Err(anyhow!("room is offline")); + } + + let live_kit = self + .live_kit + .as_mut() + .ok_or_else(|| anyhow!("live-kit was not initialized"))?; + match mem::take(&mut live_kit.screen_track) { + LocalTrack::None => Err(anyhow!("screen was not shared")), + LocalTrack::Pending { .. } => { + cx.notify(); + Ok(()) + } + LocalTrack::Published { + track_publication, .. + } => { + #[cfg(not(target_os = "windows"))] + { + let local_participant = live_kit.room.local_participant(); + let sid = track_publication.sid(); + cx.background_executor() + .spawn(async move { local_participant.unpublish_track(&sid).await }) + .detach_and_log_err(cx); + cx.notify(); + } + Audio::play_sound(Sound::StopScreenshare, cx); + Ok(()) + } + } + } + + fn set_deafened(&mut self, deafened: bool, cx: &mut ModelContext) -> Option<()> { + #[cfg(not(target_os = "windows"))] + { + let live_kit = self.live_kit.as_mut()?; + cx.notify(); + for (_, participant) in live_kit.room.remote_participants() { + for (_, publication) in participant.track_publications() { + if publication.kind() == TrackKind::Audio { + publication.set_enabled(!deafened); + } + } + } + } + + None + } + + fn set_mute( + &mut self, + should_mute: bool, + cx: &mut ModelContext, + ) -> Option>> { + let live_kit = self.live_kit.as_mut()?; + cx.notify(); + + if should_mute { + Audio::play_sound(Sound::Mute, cx); + } else { + Audio::play_sound(Sound::Unmute, cx); + } + + match &mut live_kit.microphone_track { + LocalTrack::None => { + if should_mute { + None + } else { + Some(self.share_microphone(cx)) + } + } + LocalTrack::Pending { .. } => None, + LocalTrack::Published { + track_publication, .. + } => { + #[cfg(not(target_os = "windows"))] + { + if should_mute { + track_publication.mute() + } else { + track_publication.unmute() + } + } + None + } + } + } +} + +#[cfg(target_os = "windows")] +fn spawn_room_connection( + livekit_connection_info: Option, + cx: &mut ModelContext<'_, Room>, +) { +} + +#[cfg(not(target_os = "windows"))] +fn spawn_room_connection( + livekit_connection_info: Option, + cx: &mut ModelContext<'_, Room>, +) { + if let Some(connection_info) = livekit_connection_info { + cx.spawn(|this, mut cx| async move { + let (room, mut events) = livekit::Room::connect( + &connection_info.server_url, + &connection_info.token, + RoomOptions::default(), + ) + .await?; + + this.update(&mut cx, |this, cx| { + let _handle_updates = cx.spawn(|this, mut cx| async move { + while let Some(event) = events.recv().await { + if this + .update(&mut cx, |this, cx| { + this.livekit_room_updated(event, cx).warn_on_err(); + }) + .is_err() + { + break; + } + } + }); + + let muted_by_user = Room::mute_on_join(cx); + this.live_kit = Some(LiveKitRoom { + room: Arc::new(room), + screen_track: LocalTrack::None, + microphone_track: LocalTrack::None, + next_publish_id: 0, + muted_by_user, + deafened: false, + speaking: false, + _handle_updates, + }); + + if !muted_by_user && this.can_use_microphone(cx) { + this.share_microphone(cx) + } else { + Task::ready(Ok(())) + } + })? + .await + }) + .detach_and_log_err(cx); + } +} + +struct LiveKitRoom { + room: Arc, + screen_track: LocalTrack, + microphone_track: LocalTrack, + /// Tracks whether we're currently in a muted state due to auto-mute from deafening or manual mute performed by user. + muted_by_user: bool, + deafened: bool, + speaking: bool, + next_publish_id: usize, + _handle_updates: Task<()>, +} + +impl LiveKitRoom { + #[cfg(target_os = "windows")] + fn stop_publishing(&mut self, _cx: &mut ModelContext) {} + + #[cfg(not(target_os = "windows"))] + fn stop_publishing(&mut self, cx: &mut ModelContext) { + let mut tracks_to_unpublish = Vec::new(); + if let LocalTrack::Published { + track_publication, .. + } = mem::replace(&mut self.microphone_track, LocalTrack::None) + { + tracks_to_unpublish.push(track_publication.sid()); + cx.notify(); + } + + if let LocalTrack::Published { + track_publication, .. + } = mem::replace(&mut self.screen_track, LocalTrack::None) + { + tracks_to_unpublish.push(track_publication.sid()); + cx.notify(); + } + + let participant = self.room.local_participant(); + cx.background_executor() + .spawn(async move { + for sid in tracks_to_unpublish { + participant.unpublish_track(&sid).await.log_err(); + } + }) + .detach(); + } +} + +enum LocalTrack { + None, + Pending { + publish_id: usize, + }, + Published { + track_publication: LocalTrackPublication, + _stream: Box, + }, +} + +impl Default for LocalTrack { + fn default() -> Self { + Self::None + } +} + +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum RoomStatus { + Online, + Rejoining, + Offline, +} + +impl RoomStatus { + pub fn is_offline(&self) -> bool { + matches!(self, RoomStatus::Offline) + } + + pub fn is_online(&self) -> bool { + matches!(self, RoomStatus::Online) + } +} diff --git a/crates/call/src/macos/mod.rs b/crates/call/src/macos/mod.rs new file mode 100644 index 0000000000..24472bd1fb --- /dev/null +++ b/crates/call/src/macos/mod.rs @@ -0,0 +1,545 @@ +pub mod participant; +pub mod room; + +use crate::call_settings::CallSettings; +use anyhow::{anyhow, Result}; +use audio::Audio; +use client::{proto, ChannelId, Client, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE}; +use collections::HashSet; +use futures::{channel::oneshot, future::Shared, Future, FutureExt}; +use gpui::{ + AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, ModelContext, Subscription, + Task, WeakModel, +}; +use postage::watch; +use project::Project; +use room::Event; +use settings::Settings; +use std::sync::Arc; + +pub use participant::ParticipantLocation; +pub use room::Room; + +struct GlobalActiveCall(Model); + +impl Global for GlobalActiveCall {} + +pub fn init(client: Arc, user_store: Model, cx: &mut AppContext) { + CallSettings::register(cx); + + let active_call = cx.new_model(|cx| ActiveCall::new(client, user_store, cx)); + cx.set_global(GlobalActiveCall(active_call)); +} + +pub struct OneAtATime { + cancel: Option>, +} + +impl OneAtATime { + /// spawn a task in the given context. + /// if another task is spawned before that resolves, or if the OneAtATime itself is dropped, the first task will be cancelled and return Ok(None) + /// otherwise you'll see the result of the task. + fn spawn(&mut self, cx: &mut AppContext, f: F) -> Task>> + where + F: 'static + FnOnce(AsyncAppContext) -> Fut, + Fut: Future>, + R: 'static, + { + let (tx, rx) = oneshot::channel(); + self.cancel.replace(tx); + cx.spawn(|cx| async move { + futures::select_biased! { + _ = rx.fuse() => Ok(None), + result = f(cx).fuse() => result.map(Some), + } + }) + } + + fn running(&self) -> bool { + self.cancel + .as_ref() + .is_some_and(|cancel| !cancel.is_canceled()) + } +} + +#[derive(Clone)] +pub struct IncomingCall { + pub room_id: u64, + pub calling_user: Arc, + pub participants: Vec>, + pub initial_project: Option, +} + +/// Singleton global maintaining the user's participation in a room across workspaces. +pub struct ActiveCall { + room: Option<(Model, Vec)>, + pending_room_creation: Option, Arc>>>>, + location: Option>, + _join_debouncer: OneAtATime, + pending_invites: HashSet, + incoming_call: ( + watch::Sender>, + watch::Receiver>, + ), + client: Arc, + user_store: Model, + _subscriptions: Vec, +} + +impl EventEmitter for ActiveCall {} + +impl ActiveCall { + fn new(client: Arc, user_store: Model, cx: &mut ModelContext) -> Self { + Self { + room: None, + pending_room_creation: None, + location: None, + pending_invites: Default::default(), + incoming_call: watch::channel(), + _join_debouncer: OneAtATime { cancel: None }, + _subscriptions: vec![ + client.add_request_handler(cx.weak_model(), Self::handle_incoming_call), + client.add_message_handler(cx.weak_model(), Self::handle_call_canceled), + ], + client, + user_store, + } + } + + pub fn channel_id(&self, cx: &AppContext) -> Option { + self.room()?.read(cx).channel_id() + } + + async fn handle_incoming_call( + this: Model, + envelope: TypedEnvelope, + mut cx: AsyncAppContext, + ) -> Result { + let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?; + let call = IncomingCall { + room_id: envelope.payload.room_id, + participants: user_store + .update(&mut cx, |user_store, cx| { + user_store.get_users(envelope.payload.participant_user_ids, cx) + })? + .await?, + calling_user: user_store + .update(&mut cx, |user_store, cx| { + user_store.get_user(envelope.payload.calling_user_id, cx) + })? + .await?, + initial_project: envelope.payload.initial_project, + }; + this.update(&mut cx, |this, _| { + *this.incoming_call.0.borrow_mut() = Some(call); + })?; + + Ok(proto::Ack {}) + } + + async fn handle_call_canceled( + this: Model, + envelope: TypedEnvelope, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |this, _| { + let mut incoming_call = this.incoming_call.0.borrow_mut(); + if incoming_call + .as_ref() + .map_or(false, |call| call.room_id == envelope.payload.room_id) + { + incoming_call.take(); + } + })?; + Ok(()) + } + + pub fn global(cx: &AppContext) -> Model { + cx.global::().0.clone() + } + + pub fn try_global(cx: &AppContext) -> Option> { + cx.try_global::() + .map(|call| call.0.clone()) + } + + pub fn invite( + &mut self, + called_user_id: u64, + initial_project: Option>, + cx: &mut ModelContext, + ) -> Task> { + if !self.pending_invites.insert(called_user_id) { + return Task::ready(Err(anyhow!("user was already invited"))); + } + cx.notify(); + + if self._join_debouncer.running() { + return Task::ready(Ok(())); + } + + let room = if let Some(room) = self.room().cloned() { + Some(Task::ready(Ok(room)).shared()) + } else { + self.pending_room_creation.clone() + }; + + let invite = if let Some(room) = room { + cx.spawn(move |_, mut cx| async move { + let room = room.await.map_err(|err| anyhow!("{:?}", err))?; + + let initial_project_id = if let Some(initial_project) = initial_project { + Some( + room.update(&mut cx, |room, cx| room.share_project(initial_project, cx))? + .await?, + ) + } else { + None + }; + + room.update(&mut cx, move |room, cx| { + room.call(called_user_id, initial_project_id, cx) + })? + .await?; + + anyhow::Ok(()) + }) + } else { + let client = self.client.clone(); + let user_store = self.user_store.clone(); + let room = cx + .spawn(move |this, mut cx| async move { + let create_room = async { + let room = cx + .update(|cx| { + Room::create( + called_user_id, + initial_project, + client, + user_store, + cx, + ) + })? + .await?; + + this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx))? + .await?; + + anyhow::Ok(room) + }; + + let room = create_room.await; + this.update(&mut cx, |this, _| this.pending_room_creation = None)?; + room.map_err(Arc::new) + }) + .shared(); + self.pending_room_creation = Some(room.clone()); + cx.background_executor().spawn(async move { + room.await.map_err(|err| anyhow!("{:?}", err))?; + anyhow::Ok(()) + }) + }; + + cx.spawn(move |this, mut cx| async move { + let result = invite.await; + if result.is_ok() { + this.update(&mut cx, |this, cx| this.report_call_event("invite", cx))?; + } else { + //TODO: report collaboration error + log::error!("invite failed: {:?}", result); + } + + this.update(&mut cx, |this, cx| { + this.pending_invites.remove(&called_user_id); + cx.notify(); + })?; + result + }) + } + + pub fn cancel_invite( + &mut self, + called_user_id: u64, + cx: &mut ModelContext, + ) -> Task> { + let room_id = if let Some(room) = self.room() { + room.read(cx).id() + } else { + return Task::ready(Err(anyhow!("no active call"))); + }; + + let client = self.client.clone(); + cx.background_executor().spawn(async move { + client + .request(proto::CancelCall { + room_id, + called_user_id, + }) + .await?; + anyhow::Ok(()) + }) + } + + pub fn incoming(&self) -> watch::Receiver> { + self.incoming_call.1.clone() + } + + pub fn accept_incoming(&mut self, cx: &mut ModelContext) -> Task> { + if self.room.is_some() { + return Task::ready(Err(anyhow!("cannot join while on another call"))); + } + + let call = if let Some(call) = self.incoming_call.0.borrow_mut().take() { + call + } else { + return Task::ready(Err(anyhow!("no incoming call"))); + }; + + if self.pending_room_creation.is_some() { + return Task::ready(Ok(())); + } + + let room_id = call.room_id; + let client = self.client.clone(); + let user_store = self.user_store.clone(); + let join = self + ._join_debouncer + .spawn(cx, move |cx| Room::join(room_id, client, user_store, cx)); + + cx.spawn(|this, mut cx| async move { + let room = join.await?; + this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))? + .await?; + this.update(&mut cx, |this, cx| { + this.report_call_event("accept incoming", cx) + })?; + Ok(()) + }) + } + + pub fn decline_incoming(&mut self, _: &mut ModelContext) -> Result<()> { + let call = self + .incoming_call + .0 + .borrow_mut() + .take() + .ok_or_else(|| anyhow!("no incoming call"))?; + report_call_event_for_room("decline incoming", call.room_id, None, &self.client); + self.client.send(proto::DeclineCall { + room_id: call.room_id, + })?; + Ok(()) + } + + pub fn join_channel( + &mut self, + channel_id: ChannelId, + cx: &mut ModelContext, + ) -> Task>>> { + if let Some(room) = self.room().cloned() { + if room.read(cx).channel_id() == Some(channel_id) { + return Task::ready(Ok(Some(room))); + } else { + room.update(cx, |room, cx| room.clear_state(cx)); + } + } + + if self.pending_room_creation.is_some() { + return Task::ready(Ok(None)); + } + + let client = self.client.clone(); + let user_store = self.user_store.clone(); + let join = self._join_debouncer.spawn(cx, move |cx| async move { + Room::join_channel(channel_id, client, user_store, cx).await + }); + + cx.spawn(|this, mut cx| async move { + let room = join.await?; + this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))? + .await?; + this.update(&mut cx, |this, cx| { + this.report_call_event("join channel", cx) + })?; + Ok(room) + }) + } + + pub fn hang_up(&mut self, cx: &mut ModelContext) -> Task> { + cx.notify(); + self.report_call_event("hang up", cx); + + Audio::end_call(cx); + + let channel_id = self.channel_id(cx); + if let Some((room, _)) = self.room.take() { + cx.emit(Event::RoomLeft { channel_id }); + room.update(cx, |room, cx| room.leave(cx)) + } else { + Task::ready(Ok(())) + } + } + + pub fn share_project( + &mut self, + project: Model, + cx: &mut ModelContext, + ) -> Task> { + if let Some((room, _)) = self.room.as_ref() { + self.report_call_event("share project", cx); + room.update(cx, |room, cx| room.share_project(project, cx)) + } else { + Task::ready(Err(anyhow!("no active call"))) + } + } + + pub fn unshare_project( + &mut self, + project: Model, + cx: &mut ModelContext, + ) -> Result<()> { + if let Some((room, _)) = self.room.as_ref() { + self.report_call_event("unshare project", cx); + room.update(cx, |room, cx| room.unshare_project(project, cx)) + } else { + Err(anyhow!("no active call")) + } + } + + pub fn location(&self) -> Option<&WeakModel> { + self.location.as_ref() + } + + pub fn set_location( + &mut self, + project: Option<&Model>, + cx: &mut ModelContext, + ) -> Task> { + if project.is_some() || !*ZED_ALWAYS_ACTIVE { + self.location = project.map(|project| project.downgrade()); + if let Some((room, _)) = self.room.as_ref() { + return room.update(cx, |room, cx| room.set_location(project, cx)); + } + } + Task::ready(Ok(())) + } + + fn set_room( + &mut self, + room: Option>, + cx: &mut ModelContext, + ) -> Task> { + if room.as_ref() == self.room.as_ref().map(|room| &room.0) { + Task::ready(Ok(())) + } else { + cx.notify(); + if let Some(room) = room { + if room.read(cx).status().is_offline() { + self.room = None; + Task::ready(Ok(())) + } else { + let subscriptions = vec![ + cx.observe(&room, |this, room, cx| { + if room.read(cx).status().is_offline() { + this.set_room(None, cx).detach_and_log_err(cx); + } + + cx.notify(); + }), + cx.subscribe(&room, |_, _, event, cx| cx.emit(event.clone())), + ]; + self.room = Some((room.clone(), subscriptions)); + let location = self + .location + .as_ref() + .and_then(|location| location.upgrade()); + let channel_id = room.read(cx).channel_id(); + cx.emit(Event::RoomJoined { channel_id }); + room.update(cx, |room, cx| room.set_location(location.as_ref(), cx)) + } + } else { + self.room = None; + Task::ready(Ok(())) + } + } + } + + pub fn room(&self) -> Option<&Model> { + self.room.as_ref().map(|(room, _)| room) + } + + pub fn client(&self) -> Arc { + self.client.clone() + } + + pub fn pending_invites(&self) -> &HashSet { + &self.pending_invites + } + + pub fn report_call_event(&self, operation: &'static str, cx: &mut AppContext) { + if let Some(room) = self.room() { + let room = room.read(cx); + report_call_event_for_room(operation, room.id(), room.channel_id(), &self.client); + } + } +} + +pub fn report_call_event_for_room( + operation: &'static str, + room_id: u64, + channel_id: Option, + client: &Arc, +) { + let telemetry = client.telemetry(); + + telemetry.report_call_event(operation, Some(room_id), channel_id) +} + +pub fn report_call_event_for_channel( + operation: &'static str, + channel_id: ChannelId, + client: &Arc, + cx: &AppContext, +) { + let room = ActiveCall::global(cx).read(cx).room(); + + let telemetry = client.telemetry(); + + telemetry.report_call_event(operation, room.map(|r| r.read(cx).id()), Some(channel_id)) +} + +#[cfg(test)] +mod test { + use gpui::TestAppContext; + + use crate::OneAtATime; + + #[gpui::test] + async fn test_one_at_a_time(cx: &mut TestAppContext) { + let mut one_at_a_time = OneAtATime { cancel: None }; + + assert_eq!( + cx.update(|cx| one_at_a_time.spawn(cx, |_| async { Ok(1) })) + .await + .unwrap(), + Some(1) + ); + + let (a, b) = cx.update(|cx| { + ( + one_at_a_time.spawn(cx, |_| async { + panic!(""); + }), + one_at_a_time.spawn(cx, |_| async { Ok(3) }), + ) + }); + + assert_eq!(a.await.unwrap(), None::); + assert_eq!(b.await.unwrap(), Some(3)); + + let promise = cx.update(|cx| one_at_a_time.spawn(cx, |_| async { Ok(4) })); + drop(one_at_a_time); + + assert_eq!(promise.await.unwrap(), None); + } +} diff --git a/crates/call/src/participant.rs b/crates/call/src/macos/participant.rs similarity index 80% rename from crates/call/src/participant.rs rename to crates/call/src/macos/participant.rs index 9faefc63c3..82d946a928 100644 --- a/crates/call/src/participant.rs +++ b/crates/call/src/macos/participant.rs @@ -3,8 +3,8 @@ use client::ParticipantIndex; use client::{proto, User}; use collections::HashMap; use gpui::WeakModel; -pub use live_kit_client::Frame; -pub use live_kit_client::{RemoteAudioTrack, RemoteVideoTrack}; +pub use livekit_client_macos::Frame; +pub use livekit_client_macos::{RemoteAudioTrack, RemoteVideoTrack}; use project::Project; use std::sync::Arc; @@ -49,6 +49,12 @@ pub struct RemoteParticipant { pub participant_index: ParticipantIndex, pub muted: bool, pub speaking: bool, - pub video_tracks: HashMap>, - pub audio_tracks: HashMap>, + pub video_tracks: HashMap>, + pub audio_tracks: HashMap>, +} + +impl RemoteParticipant { + pub fn has_video_tracks(&self) -> bool { + !self.video_tracks.is_empty() + } } diff --git a/crates/call/src/room.rs b/crates/call/src/macos/room.rs similarity index 99% rename from crates/call/src/room.rs rename to crates/call/src/macos/room.rs index 3eb98f3109..6fd78570d8 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/macos/room.rs @@ -15,7 +15,7 @@ use gpui::{ AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task, WeakModel, }; use language::LanguageRegistry; -use live_kit_client::{LocalAudioTrack, LocalTrackPublication, LocalVideoTrack, RoomUpdate}; +use livekit_client_macos::{LocalAudioTrack, LocalTrackPublication, LocalVideoTrack, RoomUpdate}; use postage::{sink::Sink, stream::Stream, watch}; use project::Project; use settings::Settings as _; @@ -97,7 +97,7 @@ impl Room { if let Some(live_kit) = self.live_kit.as_ref() { matches!( *live_kit.room.status().borrow(), - live_kit_client::ConnectionState::Connected { .. } + livekit_client_macos::ConnectionState::Connected { .. } ) } else { false @@ -113,7 +113,7 @@ impl Room { cx: &mut ModelContext, ) -> Self { let live_kit_room = if let Some(connection_info) = live_kit_connection_info { - let room = live_kit_client::Room::new(); + let room = livekit_client_macos::Room::new(); let mut status = room.status(); // Consume the initial status of the room. let _ = status.try_recv(); @@ -125,7 +125,7 @@ impl Room { break; }; - if status == live_kit_client::ConnectionState::Disconnected { + if status == livekit_client_macos::ConnectionState::Disconnected { this.update(&mut cx, |this, cx| this.leave(cx).log_err()) .ok(); break; @@ -156,7 +156,7 @@ impl Room { cx.spawn(|this, mut cx| async move { connect.await?; this.update(&mut cx, |this, cx| { - if this.can_use_microphone() { + if this.can_use_microphone(cx) { if let Some(live_kit) = &this.live_kit { if !live_kit.muted_by_user && !live_kit.deafened { return this.share_microphone(cx); @@ -1317,7 +1317,7 @@ impl Room { self.live_kit.as_ref().map(|live_kit| live_kit.deafened) } - pub fn can_use_microphone(&self) -> bool { + pub fn can_use_microphone(&self, _cx: &AppContext) -> bool { use proto::ChannelRole::*; match self.local_participant.role { Admin | Member | Talker => true, @@ -1631,7 +1631,7 @@ impl Room { } #[cfg(any(test, feature = "test-support"))] - pub fn set_display_sources(&self, sources: Vec) { + pub fn set_display_sources(&self, sources: Vec) { self.live_kit .as_ref() .unwrap() @@ -1641,7 +1641,7 @@ impl Room { } struct LiveKitRoom { - room: Arc, + room: Arc, screen_track: LocalTrack, microphone_track: LocalTrack, /// Tracks whether we're currently in a muted state due to auto-mute from deafening or manual mute performed by user. diff --git a/crates/collab/.env.toml b/crates/collab/.env.toml index f542422e95..5d292387cb 100644 --- a/crates/collab/.env.toml +++ b/crates/collab/.env.toml @@ -5,9 +5,9 @@ HTTP_PORT = 8080 API_TOKEN = "secret" INVITE_LINK_PREFIX = "http://localhost:3000/invites/" ZED_ENVIRONMENT = "development" -LIVE_KIT_SERVER = "http://localhost:7880" -LIVE_KIT_KEY = "devkey" -LIVE_KIT_SECRET = "secret" +LIVEKIT_SERVER = "http://localhost:7880" +LIVEKIT_KEY = "devkey" +LIVEKIT_SECRET = "secret" BLOB_STORE_ACCESS_KEY = "the-blob-store-access-key" BLOB_STORE_SECRET_KEY = "the-blob-store-secret-key" BLOB_STORE_BUCKET = "the-extensions-bucket" diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index e56507c007..9c7f09bcf5 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -40,7 +40,7 @@ google_ai.workspace = true hex.workspace = true http_client.workspace = true jsonwebtoken.workspace = true -live_kit_server.workspace = true +livekit_server.workspace = true log.workspace = true nanoid.workspace = true open_ai.workspace = true @@ -77,6 +77,12 @@ tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json", "re util.workspace = true uuid.workspace = true +[target.'cfg(target_os = "macos")'.dependencies] +livekit_client_macos = { workspace = true, features = ["test-support"] } + +[target.'cfg(not(target_os = "macos"))'.dependencies] +livekit_client = { workspace = true, features = ["test-support"] } + [dev-dependencies] assistant = { workspace = true, features = ["test-support"] } assistant_tool.workspace = true @@ -101,7 +107,6 @@ hyper.workspace = true indoc.workspace = true language = { workspace = true, features = ["test-support"] } language_model = { workspace = true, features = ["test-support"] } -live_kit_client = { workspace = true, features = ["test-support"] } lsp = { workspace = true, features = ["test-support"] } menu.workspace = true multi_buffer = { workspace = true, features = ["test-support"] } @@ -125,5 +130,11 @@ util.workspace = true workspace = { workspace = true, features = ["test-support"] } worktree = { workspace = true, features = ["test-support"] } +[target.'cfg(target_os = "macos")'.dev-dependencies] +livekit_client_macos = { workspace = true, features = ["test-support"] } + +[target.'cfg(not(target_os = "macos"))'.dev-dependencies] +livekit_client = {workspace = true, features = ["test-support"] } + [package.metadata.cargo-machete] ignored = ["async-stripe"] diff --git a/crates/collab/k8s/collab.template.yml b/crates/collab/k8s/collab.template.yml index fb5d4ed6ec..a2f89e5646 100644 --- a/crates/collab/k8s/collab.template.yml +++ b/crates/collab/k8s/collab.template.yml @@ -109,17 +109,17 @@ spec: secretKeyRef: name: zed-client key: checksum-seed - - name: LIVE_KIT_SERVER + - name: LIVEKIT_SERVER valueFrom: secretKeyRef: name: livekit key: server - - name: LIVE_KIT_KEY + - name: LIVEKIT_KEY valueFrom: secretKeyRef: name: livekit key: key - - name: LIVE_KIT_SECRET + - name: LIVEKIT_SECRET valueFrom: secretKeyRef: name: livekit diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 10120ea814..b107358eff 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -154,9 +154,9 @@ impl Database { } let role = role.unwrap(); - let live_kit_room = format!("channel-{}", nanoid::nanoid!(30)); + let livekit_room = format!("channel-{}", nanoid::nanoid!(30)); let room_id = self - .get_or_create_channel_room(channel_id, &live_kit_room, &tx) + .get_or_create_channel_room(channel_id, &livekit_room, &tx) .await?; self.join_channel_room_internal(room_id, user_id, connection, role, &tx) @@ -896,7 +896,7 @@ impl Database { pub(crate) async fn get_or_create_channel_room( &self, channel_id: ChannelId, - live_kit_room: &str, + livekit_room: &str, tx: &DatabaseTransaction, ) -> Result { let room = room::Entity::find() @@ -909,7 +909,7 @@ impl Database { } else { let result = room::Entity::insert(room::ActiveModel { channel_id: ActiveValue::Set(Some(channel_id)), - live_kit_room: ActiveValue::Set(live_kit_room.to_string()), + live_kit_room: ActiveValue::Set(livekit_room.to_string()), ..Default::default() }) .exec(tx) diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index 682c4ed389..a3a99bee71 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -103,11 +103,11 @@ impl Database { &self, user_id: UserId, connection: ConnectionId, - live_kit_room: &str, + livekit_room: &str, ) -> Result { self.transaction(|tx| async move { let room = room::ActiveModel { - live_kit_room: ActiveValue::set(live_kit_room.into()), + live_kit_room: ActiveValue::set(livekit_room.into()), ..Default::default() } .insert(&*tx) @@ -1316,7 +1316,7 @@ impl Database { channel, proto::Room { id: db_room.id.to_proto(), - live_kit_room: db_room.live_kit_room, + livekit_room: db_room.live_kit_room, participants: participants.into_values().collect(), pending_participants, followers, diff --git a/crates/collab/src/lib.rs b/crates/collab/src/lib.rs index f595cff890..cfa0e1631e 100644 --- a/crates/collab/src/lib.rs +++ b/crates/collab/src/lib.rs @@ -156,9 +156,9 @@ pub struct Config { pub clickhouse_password: Option, pub clickhouse_database: Option, pub invite_link_prefix: String, - pub live_kit_server: Option, - pub live_kit_key: Option, - pub live_kit_secret: Option, + pub livekit_server: Option, + pub livekit_key: Option, + pub livekit_secret: Option, pub llm_database_url: Option, pub llm_database_max_connections: Option, pub llm_database_migrations_path: Option, @@ -210,9 +210,9 @@ impl Config { database_max_connections: 0, api_token: "".into(), invite_link_prefix: "".into(), - live_kit_server: None, - live_kit_key: None, - live_kit_secret: None, + livekit_server: None, + livekit_key: None, + livekit_secret: None, llm_database_url: None, llm_database_max_connections: None, llm_database_migrations_path: None, @@ -277,7 +277,7 @@ impl ServiceMode { pub struct AppState { pub db: Arc, pub llm_db: Option>, - pub live_kit_client: Option>, + pub livekit_client: Option>, pub blob_store_client: Option, pub stripe_client: Option>, pub stripe_billing: Option>, @@ -309,17 +309,17 @@ impl AppState { None }; - let live_kit_client = if let Some(((server, key), secret)) = config - .live_kit_server + let livekit_client = if let Some(((server, key), secret)) = config + .livekit_server .as_ref() - .zip(config.live_kit_key.as_ref()) - .zip(config.live_kit_secret.as_ref()) + .zip(config.livekit_key.as_ref()) + .zip(config.livekit_secret.as_ref()) { - Some(Arc::new(live_kit_server::api::LiveKitClient::new( + Some(Arc::new(livekit_server::api::LiveKitClient::new( server.clone(), key.clone(), secret.clone(), - )) as Arc) + )) as Arc) } else { None }; @@ -329,7 +329,7 @@ impl AppState { let this = Self { db: db.clone(), llm_db, - live_kit_client, + livekit_client, blob_store_client: build_blob_store_client(&config).await.log_err(), stripe_billing: stripe_client .clone() diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 0d9cb2f6c2..8fa627d9e1 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -419,7 +419,7 @@ impl Server { let peer = self.peer.clone(); let timeout = self.app_state.executor.sleep(CLEANUP_TIMEOUT); let pool = self.connection_pool.clone(); - let live_kit_client = self.app_state.live_kit_client.clone(); + let livekit_client = self.app_state.livekit_client.clone(); let span = info_span!("start server"); self.app_state.executor.spawn_detached( @@ -464,8 +464,8 @@ impl Server { for room_id in room_ids { let mut contacts_to_update = HashSet::default(); let mut canceled_calls_to_user_ids = Vec::new(); - let mut live_kit_room = String::new(); - let mut delete_live_kit_room = false; + let mut livekit_room = String::new(); + let mut delete_livekit_room = false; if let Some(mut refreshed_room) = app_state .db @@ -488,8 +488,8 @@ impl Server { .extend(refreshed_room.canceled_calls_to_user_ids.iter().copied()); canceled_calls_to_user_ids = mem::take(&mut refreshed_room.canceled_calls_to_user_ids); - live_kit_room = mem::take(&mut refreshed_room.room.live_kit_room); - delete_live_kit_room = refreshed_room.room.participants.is_empty(); + livekit_room = mem::take(&mut refreshed_room.room.livekit_room); + delete_livekit_room = refreshed_room.room.participants.is_empty(); } { @@ -540,9 +540,9 @@ impl Server { } } - if let Some(live_kit) = live_kit_client.as_ref() { - if delete_live_kit_room { - live_kit.delete_room(live_kit_room).await.trace_err(); + if let Some(live_kit) = livekit_client.as_ref() { + if delete_livekit_room { + live_kit.delete_room(livekit_room).await.trace_err(); } } } @@ -1211,15 +1211,15 @@ async fn create_room( response: Response, session: Session, ) -> Result<()> { - let live_kit_room = nanoid::nanoid!(30); + let livekit_room = nanoid::nanoid!(30); let live_kit_connection_info = util::maybe!(async { - let live_kit = session.app_state.live_kit_client.as_ref(); + let live_kit = session.app_state.livekit_client.as_ref(); let live_kit = live_kit?; let user_id = session.user_id().to_string(); let token = live_kit - .room_token(&live_kit_room, &user_id.to_string()) + .room_token(&livekit_room, &user_id.to_string()) .trace_err()?; Some(proto::LiveKitConnectionInfo { @@ -1233,7 +1233,7 @@ async fn create_room( let room = session .db() .await - .create_room(session.user_id(), session.connection_id, &live_kit_room) + .create_room(session.user_id(), session.connection_id, &livekit_room) .await?; response.send(proto::CreateRoomResponse { @@ -1285,22 +1285,22 @@ async fn join_room( .trace_err(); } - let live_kit_connection_info = - if let Some(live_kit) = session.app_state.live_kit_client.as_ref() { - live_kit - .room_token( - &joined_room.room.live_kit_room, - &session.user_id().to_string(), - ) - .trace_err() - .map(|token| proto::LiveKitConnectionInfo { - server_url: live_kit.url().into(), - token, - can_publish: true, - }) - } else { - None - }; + let live_kit_connection_info = if let Some(live_kit) = session.app_state.livekit_client.as_ref() + { + live_kit + .room_token( + &joined_room.room.livekit_room, + &session.user_id().to_string(), + ) + .trace_err() + .map(|token| proto::LiveKitConnectionInfo { + server_url: live_kit.url().into(), + token, + can_publish: true, + }) + } else { + None + }; response.send(proto::JoinRoomResponse { room: Some(joined_room.room), @@ -1507,7 +1507,7 @@ async fn set_room_participant_role( let user_id = UserId::from_proto(request.user_id); let role = ChannelRole::from(request.role()); - let (live_kit_room, can_publish) = { + let (livekit_room, can_publish) = { let room = session .db() .await @@ -1519,18 +1519,18 @@ async fn set_room_participant_role( ) .await?; - let live_kit_room = room.live_kit_room.clone(); + let livekit_room = room.livekit_room.clone(); let can_publish = ChannelRole::from(request.role()).can_use_microphone(); room_updated(&room, &session.peer); - (live_kit_room, can_publish) + (livekit_room, can_publish) }; - if let Some(live_kit) = session.app_state.live_kit_client.as_ref() { + if let Some(live_kit) = session.app_state.livekit_client.as_ref() { live_kit .update_participant( - live_kit_room.clone(), + livekit_room.clone(), request.user_id.to_string(), - live_kit_server::proto::ParticipantPermission { + livekit_server::proto::ParticipantPermission { can_subscribe: true, can_publish, can_publish_data: can_publish, @@ -3092,7 +3092,7 @@ async fn join_channel_internal( let live_kit_connection_info = session .app_state - .live_kit_client + .livekit_client .as_ref() .and_then(|live_kit| { let (can_publish, token) = if role == ChannelRole::Guest { @@ -3100,7 +3100,7 @@ async fn join_channel_internal( false, live_kit .guest_token( - &joined_room.room.live_kit_room, + &joined_room.room.livekit_room, &session.user_id().to_string(), ) .trace_err()?, @@ -3110,7 +3110,7 @@ async fn join_channel_internal( true, live_kit .room_token( - &joined_room.room.live_kit_room, + &joined_room.room.livekit_room, &session.user_id().to_string(), ) .trace_err()?, @@ -4314,8 +4314,8 @@ async fn leave_room_for_session(session: &Session, connection_id: ConnectionId) let room_id; let canceled_calls_to_user_ids; - let live_kit_room; - let delete_live_kit_room; + let livekit_room; + let delete_livekit_room; let room; let channel; @@ -4328,8 +4328,8 @@ async fn leave_room_for_session(session: &Session, connection_id: ConnectionId) room_id = RoomId::from_proto(left_room.room.id); canceled_calls_to_user_ids = mem::take(&mut left_room.canceled_calls_to_user_ids); - live_kit_room = mem::take(&mut left_room.room.live_kit_room); - delete_live_kit_room = left_room.deleted; + livekit_room = mem::take(&mut left_room.room.livekit_room); + delete_livekit_room = left_room.deleted; room = mem::take(&mut left_room.room); channel = mem::take(&mut left_room.channel); @@ -4369,14 +4369,14 @@ async fn leave_room_for_session(session: &Session, connection_id: ConnectionId) update_user_contacts(contact_user_id, session).await?; } - if let Some(live_kit) = session.app_state.live_kit_client.as_ref() { + if let Some(live_kit) = session.app_state.livekit_client.as_ref() { live_kit - .remove_participant(live_kit_room.clone(), session.user_id().to_string()) + .remove_participant(livekit_room.clone(), session.user_id().to_string()) .await .trace_err(); - if delete_live_kit_room { - live_kit.delete_room(live_kit_room).await.trace_err(); + if delete_livekit_room { + live_kit.delete_room(livekit_room).await.trace_err(); } } diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index 29373bc6ea..2ce69efc9b 100644 --- a/crates/collab/src/tests.rs +++ b/crates/collab/src/tests.rs @@ -1,3 +1,6 @@ +// todo(windows): Actually run the tests +#![cfg(not(target_os = "windows"))] + use std::sync::Arc; use call::Room; diff --git a/crates/collab/src/tests/channel_guest_tests.rs b/crates/collab/src/tests/channel_guest_tests.rs index 5a091fe308..006a3e5d1c 100644 --- a/crates/collab/src/tests/channel_guest_tests.rs +++ b/crates/collab/src/tests/channel_guest_tests.rs @@ -107,7 +107,9 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test }); assert!(project_b.read_with(cx_b, |project, cx| project.is_read_only(cx))); assert!(editor_b.update(cx_b, |e, cx| e.read_only(cx))); - assert!(room_b.read_with(cx_b, |room, _| !room.can_use_microphone())); + cx_b.update(|cx_b| { + assert!(room_b.read_with(cx_b, |room, cx| !room.can_use_microphone(cx))); + }); assert!(room_b .update(cx_b, |room, cx| room.share_microphone(cx)) .await @@ -133,7 +135,9 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test assert!(editor_b.update(cx_b, |editor, cx| !editor.read_only(cx))); // B sees themselves as muted, and can unmute. - assert!(room_b.read_with(cx_b, |room, _| room.can_use_microphone())); + cx_b.update(|cx_b| { + assert!(room_b.read_with(cx_b, |room, cx| room.can_use_microphone(cx))); + }); room_b.read_with(cx_b, |room, _| assert!(room.is_muted())); room_b.update(cx_b, |room, cx| room.toggle_mute(cx)); cx_a.run_until_parked(); @@ -226,7 +230,9 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes let room_b = cx_b .read(ActiveCall::global) .update(cx_b, |call, _| call.room().unwrap().clone()); - assert!(room_b.read_with(cx_b, |room, _| !room.can_use_microphone())); + cx_b.update(|cx_b| { + assert!(room_b.read_with(cx_b, |room, cx| !room.can_use_microphone(cx))); + }); // A tries to grant write access to B, but cannot because B has not // yet signed the zed CLA. @@ -244,7 +250,9 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes .unwrap_err(); cx_a.run_until_parked(); assert!(room_b.read_with(cx_b, |room, _| !room.can_share_projects())); - assert!(room_b.read_with(cx_b, |room, _| !room.can_use_microphone())); + cx_b.update(|cx_b| { + assert!(room_b.read_with(cx_b, |room, cx| !room.can_use_microphone(cx))); + }); // A tries to grant write access to B, but cannot because B has not // yet signed the zed CLA. @@ -262,7 +270,9 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes .unwrap(); cx_a.run_until_parked(); assert!(room_b.read_with(cx_b, |room, _| !room.can_share_projects())); - assert!(room_b.read_with(cx_b, |room, _| room.can_use_microphone())); + cx_b.update(|cx_b| { + assert!(room_b.read_with(cx_b, |room, cx| room.can_use_microphone(cx))); + }); // User B signs the zed CLA. server @@ -287,5 +297,7 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes .unwrap(); cx_a.run_until_parked(); assert!(room_b.read_with(cx_b, |room, _| room.can_share_projects())); - assert!(room_b.read_with(cx_b, |room, _| room.can_use_microphone())); + cx_b.update(|cx_b| { + assert!(room_b.read_with(cx_b, |room, cx| room.can_use_microphone(cx))); + }); } diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index d708194f58..4de368d2ea 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -1,5 +1,5 @@ #![allow(clippy::reversed_empty_ranges)] -use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer}; +use crate::tests::TestServer; use call::{ActiveCall, ParticipantLocation}; use client::ChannelId; use collab_ui::{ @@ -12,17 +12,11 @@ use gpui::{ View, VisualContext, VisualTestContext, }; use language::Capability; -use live_kit_client::MacOSDisplay; use project::WorktreeSettings; use rpc::proto::PeerId; use serde_json::json; use settings::SettingsStore; -use workspace::{ - dock::{test::TestPanel, DockPosition}, - item::{test::TestItem, ItemHandle as _}, - shared_screen::SharedScreen, - SplitDirection, Workspace, -}; +use workspace::{item::ItemHandle as _, SplitDirection, Workspace}; use super::TestClient; @@ -428,106 +422,118 @@ async fn test_basic_following( editor_a1.item_id() ); - // Client B activates an external window, which causes a new screen-sharing item to be added to the pane. - let display = MacOSDisplay::new(); - active_call_b - .update(cx_b, |call, cx| call.set_location(None, cx)) - .await - .unwrap(); - active_call_b - .update(cx_b, |call, cx| { - call.room().unwrap().update(cx, |room, cx| { - room.set_display_sources(vec![display.clone()]); - room.share_screen(cx) + // TODO: Re-enable this test once we can replace our swift Livekit SDK with the rust SDK + #[cfg(not(target_os = "macos"))] + { + use crate::rpc::RECONNECT_TIMEOUT; + use gpui::TestScreenCaptureSource; + use workspace::{ + dock::{test::TestPanel, DockPosition}, + item::test::TestItem, + shared_screen::SharedScreen, + }; + + // Client B activates an external window, which causes a new screen-sharing item to be added to the pane. + let display = TestScreenCaptureSource::new(); + active_call_b + .update(cx_b, |call, cx| call.set_location(None, cx)) + .await + .unwrap(); + cx_b.set_screen_capture_sources(vec![display]); + active_call_b + .update(cx_b, |call, cx| { + call.room() + .unwrap() + .update(cx, |room, cx| room.share_screen(cx)) }) - }) - .await - .unwrap(); - executor.run_until_parked(); - let shared_screen = workspace_a.update(cx_a, |workspace, cx| { - workspace - .active_item(cx) - .expect("no active item") - .downcast::() - .expect("active item isn't a shared screen") - }); + .await + .unwrap(); // This is what breaks + executor.run_until_parked(); + let shared_screen = workspace_a.update(cx_a, |workspace, cx| { + workspace + .active_item(cx) + .expect("no active item") + .downcast::() + .expect("active item isn't a shared screen") + }); - // Client B activates Zed again, which causes the previous editor to become focused again. - active_call_b - .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) - .await - .unwrap(); - executor.run_until_parked(); - workspace_a.update(cx_a, |workspace, cx| { + // Client B activates Zed again, which causes the previous editor to become focused again. + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); + executor.run_until_parked(); + workspace_a.update(cx_a, |workspace, cx| { + assert_eq!( + workspace.active_item(cx).unwrap().item_id(), + editor_a1.item_id() + ) + }); + + // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer. + workspace_b.update(cx_b, |workspace, cx| { + workspace.activate_item(&multibuffer_editor_b, true, true, cx) + }); + executor.run_until_parked(); + workspace_a.update(cx_a, |workspace, cx| { + assert_eq!( + workspace.active_item(cx).unwrap().item_id(), + multibuffer_editor_a.item_id() + ) + }); + + // Client B activates a panel, and the previously-opened screen-sharing item gets activated. + let panel = cx_b.new_view(|cx| TestPanel::new(DockPosition::Left, cx)); + workspace_b.update(cx_b, |workspace, cx| { + workspace.add_panel(panel, cx); + workspace.toggle_panel_focus::(cx); + }); + executor.run_until_parked(); assert_eq!( - workspace.active_item(cx).unwrap().item_id(), - editor_a1.item_id() - ) - }); + workspace_a.update(cx_a, |workspace, cx| workspace + .active_item(cx) + .unwrap() + .item_id()), + shared_screen.item_id() + ); - // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer. - workspace_b.update(cx_b, |workspace, cx| { - workspace.activate_item(&multibuffer_editor_b, true, true, cx) - }); - executor.run_until_parked(); - workspace_a.update(cx_a, |workspace, cx| { + // Toggling the focus back to the pane causes client A to return to the multibuffer. + workspace_b.update(cx_b, |workspace, cx| { + workspace.toggle_panel_focus::(cx); + }); + executor.run_until_parked(); + workspace_a.update(cx_a, |workspace, cx| { + assert_eq!( + workspace.active_item(cx).unwrap().item_id(), + multibuffer_editor_a.item_id() + ) + }); + + // Client B activates an item that doesn't implement following, + // so the previously-opened screen-sharing item gets activated. + let unfollowable_item = cx_b.new_view(TestItem::new); + workspace_b.update(cx_b, |workspace, cx| { + workspace.active_pane().update(cx, |pane, cx| { + pane.add_item(Box::new(unfollowable_item), true, true, None, cx) + }) + }); + executor.run_until_parked(); assert_eq!( - workspace.active_item(cx).unwrap().item_id(), - multibuffer_editor_a.item_id() - ) - }); + workspace_a.update(cx_a, |workspace, cx| workspace + .active_item(cx) + .unwrap() + .item_id()), + shared_screen.item_id() + ); - // Client B activates a panel, and the previously-opened screen-sharing item gets activated. - let panel = cx_b.new_view(|cx| TestPanel::new(DockPosition::Left, cx)); - workspace_b.update(cx_b, |workspace, cx| { - workspace.add_panel(panel, cx); - workspace.toggle_panel_focus::(cx); - }); - executor.run_until_parked(); - assert_eq!( - workspace_a.update(cx_a, |workspace, cx| workspace - .active_item(cx) - .unwrap() - .item_id()), - shared_screen.item_id() - ); - - // Toggling the focus back to the pane causes client A to return to the multibuffer. - workspace_b.update(cx_b, |workspace, cx| { - workspace.toggle_panel_focus::(cx); - }); - executor.run_until_parked(); - workspace_a.update(cx_a, |workspace, cx| { + // Following interrupts when client B disconnects. + client_b.disconnect(&cx_b.to_async()); + executor.advance_clock(RECONNECT_TIMEOUT); assert_eq!( - workspace.active_item(cx).unwrap().item_id(), - multibuffer_editor_a.item_id() - ) - }); - - // Client B activates an item that doesn't implement following, - // so the previously-opened screen-sharing item gets activated. - let unfollowable_item = cx_b.new_view(TestItem::new); - workspace_b.update(cx_b, |workspace, cx| { - workspace.active_pane().update(cx, |pane, cx| { - pane.add_item(Box::new(unfollowable_item), true, true, None, cx) - }) - }); - executor.run_until_parked(); - assert_eq!( - workspace_a.update(cx_a, |workspace, cx| workspace - .active_item(cx) - .unwrap() - .item_id()), - shared_screen.item_id() - ); - - // Following interrupts when client B disconnects. - client_b.disconnect(&cx_b.to_async()); - executor.advance_clock(RECONNECT_TIMEOUT); - assert_eq!( - workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)), - None - ); + workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)), + None + ); + } } #[gpui::test] diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 04b9a36fc7..a0b36ce5cc 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -25,7 +25,6 @@ use language::{ tree_sitter_rust, tree_sitter_typescript, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LineEnding, OffsetRangeExt, Point, Rope, }; -use live_kit_client::MacOSDisplay; use lsp::LanguageServerId; use parking_lot::Mutex; use project::lsp_store::FormatTarget; @@ -241,56 +240,60 @@ async fn test_basic_calls( } ); - // User A shares their screen - let display = MacOSDisplay::new(); - let events_b = active_call_events(cx_b); - let events_c = active_call_events(cx_c); - active_call_a - .update(cx_a, |call, cx| { - call.room().unwrap().update(cx, |room, cx| { - room.set_display_sources(vec![display.clone()]); - room.share_screen(cx) + // TODO: Re-enable this test once we can replace our swift Livekit SDK with the rust SDK + #[cfg(not(target_os = "macos"))] + { + // User A shares their screen + let display = gpui::TestScreenCaptureSource::new(); + let events_b = active_call_events(cx_b); + let events_c = active_call_events(cx_c); + cx_a.set_screen_capture_sources(vec![display]); + active_call_a + .update(cx_a, |call, cx| { + call.room() + .unwrap() + .update(cx, |room, cx| room.share_screen(cx)) }) - }) - .await - .unwrap(); + .await + .unwrap(); - executor.run_until_parked(); + executor.run_until_parked(); - // User B observes the remote screen sharing track. - assert_eq!(events_b.borrow().len(), 1); - let event_b = events_b.borrow().first().unwrap().clone(); - if let call::room::Event::RemoteVideoTracksChanged { participant_id } = event_b { - assert_eq!(participant_id, client_a.peer_id().unwrap()); + // User B observes the remote screen sharing track. + assert_eq!(events_b.borrow().len(), 1); + let event_b = events_b.borrow().first().unwrap().clone(); + if let call::room::Event::RemoteVideoTracksChanged { participant_id } = event_b { + assert_eq!(participant_id, client_a.peer_id().unwrap()); - room_b.read_with(cx_b, |room, _| { - assert_eq!( - room.remote_participants()[&client_a.user_id().unwrap()] - .video_tracks - .len(), - 1 - ); - }); - } else { - panic!("unexpected event") - } + room_b.read_with(cx_b, |room, _| { + assert_eq!( + room.remote_participants()[&client_a.user_id().unwrap()] + .video_tracks + .len(), + 1 + ); + }); + } else { + panic!("unexpected event") + } - // User C observes the remote screen sharing track. - assert_eq!(events_c.borrow().len(), 1); - let event_c = events_c.borrow().first().unwrap().clone(); - if let call::room::Event::RemoteVideoTracksChanged { participant_id } = event_c { - assert_eq!(participant_id, client_a.peer_id().unwrap()); + // User C observes the remote screen sharing track. + assert_eq!(events_c.borrow().len(), 1); + let event_c = events_c.borrow().first().unwrap().clone(); + if let call::room::Event::RemoteVideoTracksChanged { participant_id } = event_c { + assert_eq!(participant_id, client_a.peer_id().unwrap()); - room_c.read_with(cx_c, |room, _| { - assert_eq!( - room.remote_participants()[&client_a.user_id().unwrap()] - .video_tracks - .len(), - 1 - ); - }); - } else { - panic!("unexpected event") + room_c.read_with(cx_c, |room, _| { + assert_eq!( + room.remote_participants()[&client_a.user_id().unwrap()] + .video_tracks + .len(), + 1 + ); + }); + } else { + panic!("unexpected event") + } } // User A leaves the room. @@ -329,7 +332,7 @@ async fn test_basic_calls( // to automatically leave the room. User C leaves the room as well because // nobody else is in there. server - .test_live_kit_server + .test_livekit_server .disconnect_client(client_b.user_id().unwrap().to_string()) .await; executor.run_until_parked(); @@ -844,7 +847,7 @@ async fn test_client_disconnecting_from_room( // User B gets disconnected from the LiveKit server, which causes it // to automatically leave the room. server - .test_live_kit_server + .test_livekit_server .disconnect_client(client_b.user_id().unwrap().to_string()) .await; executor.run_until_parked(); @@ -1943,7 +1946,7 @@ async fn test_mute_deafen( room_a.read_with(cx_a, |room, _| assert!(!room.is_muted())); room_b.read_with(cx_b, |room, _| assert!(!room.is_muted())); - // Users A and B are both muted. + // Users A and B are both unmuted. assert_eq!( participant_audio_state(&room_a, cx_a), &[ParticipantAudioState { @@ -2075,7 +2078,17 @@ async fn test_mute_deafen( audio_tracks_playing: participant .audio_tracks .values() - .map(|track| track.is_playing()) + .map({ + #[cfg(target_os = "macos")] + { + |track| track.is_playing() + } + + #[cfg(not(target_os = "macos"))] + { + |(track, _)| track.rtc_track().enabled() + } + }) .collect(), }) .collect::>() @@ -6015,6 +6028,8 @@ async fn test_contact_requests( } } +// TODO: Re-enable this test once we can replace our swift Livekit SDK with the rust SDK +#[cfg(not(target_os = "macos"))] #[gpui::test(iterations = 10)] async fn test_join_call_after_screen_was_shared( executor: BackgroundExecutor, @@ -6057,13 +6072,13 @@ async fn test_join_call_after_screen_was_shared( assert_eq!(call_b.calling_user.github_login, "user_a"); // User A shares their screen - let display = MacOSDisplay::new(); + let display = gpui::TestScreenCaptureSource::new(); + cx_a.set_screen_capture_sources(vec![display]); active_call_a .update(cx_a, |call, cx| { - call.room().unwrap().update(cx, |room, cx| { - room.set_display_sources(vec![display.clone()]); - room.share_screen(cx) - }) + call.room() + .unwrap() + .update(cx, |room, cx| room.share_screen(cx)) }) .await .unwrap(); diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index 1528da2ff0..e66a828a77 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -45,9 +45,15 @@ use std::{ }; use workspace::{Workspace, WorkspaceStore}; +#[cfg(not(target_os = "macos"))] +use livekit_client::test::TestServer as LivekitTestServer; + +#[cfg(target_os = "macos")] +use livekit_client_macos::TestServer as LivekitTestServer; + pub struct TestServer { pub app_state: Arc, - pub test_live_kit_server: Arc, + pub test_livekit_server: Arc, server: Arc, next_github_user_id: i32, connection_killers: Arc>>>, @@ -79,7 +85,7 @@ pub struct ContactsSummary { impl TestServer { pub async fn start(deterministic: BackgroundExecutor) -> Self { - static NEXT_LIVE_KIT_SERVER_ID: AtomicUsize = AtomicUsize::new(0); + static NEXT_LIVEKIT_SERVER_ID: AtomicUsize = AtomicUsize::new(0); let use_postgres = env::var("USE_POSTGRES").ok(); let use_postgres = use_postgres.as_deref(); @@ -88,16 +94,16 @@ impl TestServer { } else { TestDb::sqlite(deterministic.clone()) }; - let live_kit_server_id = NEXT_LIVE_KIT_SERVER_ID.fetch_add(1, SeqCst); - let live_kit_server = live_kit_client::TestServer::create( - format!("http://livekit.{}.test", live_kit_server_id), - format!("devkey-{}", live_kit_server_id), - format!("secret-{}", live_kit_server_id), + let livekit_server_id = NEXT_LIVEKIT_SERVER_ID.fetch_add(1, SeqCst); + let livekit_server = LivekitTestServer::create( + format!("http://livekit.{}.test", livekit_server_id), + format!("devkey-{}", livekit_server_id), + format!("secret-{}", livekit_server_id), deterministic.clone(), ) .unwrap(); let executor = Executor::Deterministic(deterministic.clone()); - let app_state = Self::build_app_state(&test_db, &live_kit_server, executor.clone()).await; + let app_state = Self::build_app_state(&test_db, &livekit_server, executor.clone()).await; let epoch = app_state .db .create_server(&app_state.config.zed_environment) @@ -114,7 +120,7 @@ impl TestServer { forbid_connections: Default::default(), next_github_user_id: 0, _test_db: test_db, - test_live_kit_server: live_kit_server, + test_livekit_server: livekit_server, } } @@ -500,13 +506,13 @@ impl TestServer { pub async fn build_app_state( test_db: &TestDb, - live_kit_test_server: &live_kit_client::TestServer, + livekit_test_server: &LivekitTestServer, executor: Executor, ) -> Arc { Arc::new(AppState { db: test_db.db().clone(), llm_db: None, - live_kit_client: Some(Arc::new(live_kit_test_server.create_api_client())), + livekit_client: Some(Arc::new(livekit_test_server.create_api_client())), blob_store_client: None, stripe_client: None, stripe_billing: None, @@ -520,9 +526,9 @@ impl TestServer { database_max_connections: 0, api_token: "".into(), invite_link_prefix: "".into(), - live_kit_server: None, - live_kit_key: None, - live_kit_secret: None, + livekit_server: None, + livekit_key: None, + livekit_secret: None, llm_database_url: None, llm_database_max_connections: None, llm_database_migrations_path: None, @@ -572,7 +578,7 @@ impl Deref for TestServer { impl Drop for TestServer { fn drop(&mut self) { self.server.teardown(); - self.test_live_kit_server.teardown().unwrap(); + self.test_livekit_server.teardown().unwrap(); } } diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index c93a48096a..fa3ab0219b 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -474,11 +474,10 @@ impl CollabPanel { project_id: project.id, worktree_root_names: project.worktree_root_names.clone(), host_user_id: participant.user.id, - is_last: projects.peek().is_none() - && participant.video_tracks.is_empty(), + is_last: projects.peek().is_none() && !participant.has_video_tracks(), }); } - if !participant.video_tracks.is_empty() { + if participant.has_video_tracks() { self.entries.push(ListEntry::ParticipantScreen { peer_id: Some(participant.peer_id), is_last: true, diff --git a/crates/gpui/build.rs b/crates/gpui/build.rs index ef29d7cc82..045372b73c 100644 --- a/crates/gpui/build.rs +++ b/crates/gpui/build.rs @@ -50,6 +50,7 @@ mod macos { fn generate_dispatch_bindings() { println!("cargo:rustc-link-lib=framework=System"); + println!("cargo:rustc-link-lib=framework=ScreenCaptureKit"); println!("cargo:rerun-if-changed=src/platform/mac/dispatch.h"); let bindings = bindgen::Builder::default() diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 87ee3942dd..ca787587b9 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -33,8 +33,8 @@ use crate::{ Entity, EventEmitter, ForegroundExecutor, Global, KeyBinding, Keymap, Keystroke, LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform, PlatformDisplay, Point, PromptBuilder, PromptHandle, PromptLevel, Render, RenderablePromptHandle, Reservation, - SharedString, SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, View, ViewContext, - Window, WindowAppearance, WindowContext, WindowHandle, WindowId, + ScreenCaptureSource, SharedString, SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, + View, ViewContext, Window, WindowAppearance, WindowContext, WindowHandle, WindowId, }; mod async_context; @@ -599,6 +599,13 @@ impl AppContext { self.platform.primary_display() } + /// Returns a list of available screen capture sources. + pub fn screen_capture_sources( + &self, + ) -> oneshot::Receiver>>> { + self.platform.screen_capture_sources() + } + /// Returns the display with the given ID, if one exists. pub fn find_display(&self, id: DisplayId) -> Option> { self.displays() diff --git a/crates/gpui/src/app/test_context.rs b/crates/gpui/src/app/test_context.rs index 2fea804301..04ca7764c5 100644 --- a/crates/gpui/src/app/test_context.rs +++ b/crates/gpui/src/app/test_context.rs @@ -4,8 +4,8 @@ use crate::{ Element, Empty, Entity, EventEmitter, ForegroundExecutor, Global, InputEvent, Keystroke, Model, ModelContext, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Platform, Point, Render, Result, Size, Task, TestDispatcher, - TestPlatform, TestWindow, TextSystem, View, ViewContext, VisualContext, WindowBounds, - WindowContext, WindowHandle, WindowOptions, + TestPlatform, TestScreenCaptureSource, TestWindow, TextSystem, View, ViewContext, + VisualContext, WindowBounds, WindowContext, WindowHandle, WindowOptions, }; use anyhow::{anyhow, bail}; use futures::{channel::oneshot, Stream, StreamExt}; @@ -287,6 +287,12 @@ impl TestAppContext { self.test_window(window_handle).simulate_resize(size); } + /// Causes the given sources to be returned if the application queries for screen + /// capture sources. + pub fn set_screen_capture_sources(&self, sources: Vec) { + self.test_platform.set_screen_capture_sources(sources); + } + /// Returns all windows open in the test. pub fn windows(&self) -> Vec { self.app.borrow().windows().clone() diff --git a/crates/gpui/src/geometry.rs b/crates/gpui/src/geometry.rs index 9e0b9b9014..b636c95a61 100644 --- a/crates/gpui/src/geometry.rs +++ b/crates/gpui/src/geometry.rs @@ -704,6 +704,11 @@ pub struct Bounds { pub size: Size, } +/// Create a bounds with the given origin and size +pub fn bounds(origin: Point, size: Size) -> Bounds { + Bounds { origin, size } +} + impl Bounds { /// Generate a centered bounds for the given display or primary display if none is provided pub fn centered(display_id: Option, size: Size, cx: &AppContext) -> Self { diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 8228d44bb4..f3ffa323d8 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -71,6 +71,9 @@ pub(crate) use test::*; #[cfg(target_os = "windows")] pub(crate) use windows::*; +#[cfg(any(test, feature = "test-support"))] +pub use test::TestScreenCaptureSource; + #[cfg(target_os = "macos")] pub(crate) fn current_platform(headless: bool) -> Rc { Rc::new(MacPlatform::new(headless)) @@ -150,6 +153,10 @@ pub(crate) trait Platform: 'static { None } + fn screen_capture_sources( + &self, + ) -> oneshot::Receiver>>>; + fn open_window( &self, handle: AnyWindowHandle, @@ -229,6 +236,25 @@ pub trait PlatformDisplay: Send + Sync + Debug { } } +/// A source of on-screen video content that can be captured. +pub trait ScreenCaptureSource { + /// Returns the video resolution of this source. + fn resolution(&self) -> Result>; + + /// Start capture video from this source, invoking the given callback + /// with each frame. + fn stream( + &self, + frame_callback: Box, + ) -> oneshot::Receiver>>; +} + +/// A video stream captured from a screen. +pub trait ScreenCaptureStream {} + +/// A frame of video captured from a screen. +pub struct ScreenCaptureFrame(pub PlatformScreenCaptureFrame); + /// An opaque identifier for a hardware display #[derive(PartialEq, Eq, Hash, Copy, Clone)] pub struct DisplayId(pub(crate) u32); diff --git a/crates/gpui/src/platform/linux.rs b/crates/gpui/src/platform/linux.rs index 0499869361..089b52cf1e 100644 --- a/crates/gpui/src/platform/linux.rs +++ b/crates/gpui/src/platform/linux.rs @@ -20,3 +20,5 @@ pub(crate) use text_system::*; pub(crate) use wayland::*; #[cfg(feature = "x11")] pub(crate) use x11::*; + +pub(crate) type PlatformScreenCaptureFrame = (); diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index 650ed70af8..a85052a4f0 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -35,8 +35,8 @@ use crate::{ px, Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, ForegroundExecutor, Keymap, Keystroke, LinuxDispatcher, Menu, MenuItem, Modifiers, OwnedMenu, PathPromptOptions, Pixels, Platform, PlatformDisplay, PlatformInputHandler, PlatformTextSystem, - PlatformWindow, Point, PromptLevel, Result, SemanticVersion, SharedString, Size, Task, - WindowAppearance, WindowOptions, WindowParams, + PlatformWindow, Point, PromptLevel, Result, ScreenCaptureSource, SemanticVersion, SharedString, + Size, Task, WindowAppearance, WindowOptions, WindowParams, }; pub(crate) const SCROLL_LINES: f32 = 3.0; @@ -242,6 +242,14 @@ impl Platform for P { self.displays() } + fn screen_capture_sources( + &self, + ) -> oneshot::Receiver>>> { + let (mut tx, rx) = oneshot::channel(); + tx.send(Err(anyhow!("screen capture not implemented"))).ok(); + rx + } + fn active_window(&self) -> Option { self.active_window() } diff --git a/crates/gpui/src/platform/mac.rs b/crates/gpui/src/platform/mac.rs index 396fd49d04..bd3d8f35ac 100644 --- a/crates/gpui/src/platform/mac.rs +++ b/crates/gpui/src/platform/mac.rs @@ -4,12 +4,14 @@ mod dispatcher; mod display; mod display_link; mod events; +mod screen_capture; #[cfg(not(feature = "macos-blade"))] mod metal_atlas; #[cfg(not(feature = "macos-blade"))] pub mod metal_renderer; +use media::core_video::CVImageBuffer; #[cfg(not(feature = "macos-blade"))] use metal_renderer as renderer; @@ -49,6 +51,9 @@ pub(crate) use window::*; #[cfg(feature = "font-kit")] pub(crate) use text_system::*; +/// A frame of video captured from a screen. +pub(crate) type PlatformScreenCaptureFrame = CVImageBuffer; + trait BoolExt { fn to_objc(self) -> BOOL; } diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index 28f427af1b..f0fe560ca4 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -1,14 +1,14 @@ use super::{ attributed_string::{NSAttributedString, NSMutableAttributedString}, events::key_to_native, - BoolExt, + renderer, screen_capture, BoolExt, }; use crate::{ hash, Action, AnyWindowHandle, BackgroundExecutor, ClipboardEntry, ClipboardItem, ClipboardString, CursorStyle, ForegroundExecutor, Image, ImageFormat, Keymap, MacDispatcher, MacDisplay, MacWindow, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay, - PlatformTextSystem, PlatformWindow, Result, SemanticVersion, Task, WindowAppearance, - WindowParams, + PlatformTextSystem, PlatformWindow, Result, ScreenCaptureSource, SemanticVersion, Task, + WindowAppearance, WindowParams, }; use anyhow::anyhow; use block::ConcreteBlock; @@ -58,8 +58,6 @@ use std::{ }; use strum::IntoEnumIterator; -use super::renderer; - #[allow(non_upper_case_globals)] const NSUTF8StringEncoding: NSUInteger = 4; @@ -552,6 +550,12 @@ impl Platform for MacPlatform { .collect() } + fn screen_capture_sources( + &self, + ) -> oneshot::Receiver>>> { + screen_capture::get_sources() + } + fn active_window(&self) -> Option { MacWindow::active_window() } diff --git a/crates/gpui/src/platform/mac/screen_capture.rs b/crates/gpui/src/platform/mac/screen_capture.rs new file mode 100644 index 0000000000..a2b535996f --- /dev/null +++ b/crates/gpui/src/platform/mac/screen_capture.rs @@ -0,0 +1,239 @@ +use crate::{ + platform::{ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream}, + px, size, Pixels, Size, +}; +use anyhow::{anyhow, Result}; +use block::ConcreteBlock; +use cocoa::{ + base::{id, nil, YES}, + foundation::NSArray, +}; +use core_foundation::base::TCFType; +use ctor::ctor; +use futures::channel::oneshot; +use media::core_media::{CMSampleBuffer, CMSampleBufferRef}; +use metal::NSInteger; +use objc::{ + class, + declare::ClassDecl, + msg_send, + runtime::{Class, Object, Sel}, + sel, sel_impl, +}; +use std::{cell::RefCell, ffi::c_void, mem, ptr, rc::Rc}; + +#[derive(Clone)] +pub struct MacScreenCaptureSource { + sc_display: id, +} + +pub struct MacScreenCaptureStream { + sc_stream: id, + sc_stream_output: id, +} + +#[link(name = "ScreenCaptureKit", kind = "framework")] +extern "C" {} + +static mut DELEGATE_CLASS: *const Class = ptr::null(); +static mut OUTPUT_CLASS: *const Class = ptr::null(); +const FRAME_CALLBACK_IVAR: &str = "frame_callback"; + +#[allow(non_upper_case_globals)] +const SCStreamOutputTypeScreen: NSInteger = 0; + +impl ScreenCaptureSource for MacScreenCaptureSource { + fn resolution(&self) -> Result> { + unsafe { + let width: i64 = msg_send![self.sc_display, width]; + let height: i64 = msg_send![self.sc_display, height]; + Ok(size(px(width as f32), px(height as f32))) + } + } + + fn stream( + &self, + frame_callback: Box, + ) -> oneshot::Receiver>> { + unsafe { + let stream: id = msg_send![class!(SCStream), alloc]; + let filter: id = msg_send![class!(SCContentFilter), alloc]; + let configuration: id = msg_send![class!(SCStreamConfiguration), alloc]; + let delegate: id = msg_send![DELEGATE_CLASS, alloc]; + let output: id = msg_send![OUTPUT_CLASS, alloc]; + + let excluded_windows = NSArray::array(nil); + let filter: id = msg_send![filter, initWithDisplay:self.sc_display excludingWindows:excluded_windows]; + let configuration: id = msg_send![configuration, init]; + let delegate: id = msg_send![delegate, init]; + let output: id = msg_send![output, init]; + + output.as_mut().unwrap().set_ivar( + FRAME_CALLBACK_IVAR, + Box::into_raw(Box::new(frame_callback)) as *mut c_void, + ); + + let stream: id = msg_send![stream, initWithFilter:filter configuration:configuration delegate:delegate]; + + let (mut tx, rx) = oneshot::channel(); + + let mut error: id = nil; + let _: () = msg_send![stream, addStreamOutput:output type:SCStreamOutputTypeScreen sampleHandlerQueue:0 error:&mut error as *mut id]; + if error != nil { + let message: id = msg_send![error, localizedDescription]; + tx.send(Err(anyhow!("failed to add stream output {message:?}"))) + .ok(); + return rx; + } + + let tx = Rc::new(RefCell::new(Some(tx))); + let handler = ConcreteBlock::new({ + move |error: id| { + let result = if error == nil { + let stream = MacScreenCaptureStream { + sc_stream: stream, + sc_stream_output: output, + }; + Ok(Box::new(stream) as Box) + } else { + let message: id = msg_send![error, localizedDescription]; + Err(anyhow!("failed to stop screen capture stream {message:?}")) + }; + if let Some(tx) = tx.borrow_mut().take() { + tx.send(result).ok(); + } + } + }); + let handler = handler.copy(); + let _: () = msg_send![stream, startCaptureWithCompletionHandler:handler]; + rx + } + } +} + +impl Drop for MacScreenCaptureSource { + fn drop(&mut self) { + unsafe { + let _: () = msg_send![self.sc_display, release]; + } + } +} + +impl ScreenCaptureStream for MacScreenCaptureStream {} + +impl Drop for MacScreenCaptureStream { + fn drop(&mut self) { + unsafe { + let mut error: id = nil; + let _: () = msg_send![self.sc_stream, removeStreamOutput:self.sc_stream_output type:SCStreamOutputTypeScreen error:&mut error as *mut _]; + if error != nil { + let message: id = msg_send![error, localizedDescription]; + log::error!("failed to add stream output {message:?}"); + } + + let handler = ConcreteBlock::new(move |error: id| { + if error != nil { + let message: id = msg_send![error, localizedDescription]; + log::error!("failed to stop screen capture stream {message:?}"); + } + }); + let block = handler.copy(); + let _: () = msg_send![self.sc_stream, stopCaptureWithCompletionHandler:block]; + let _: () = msg_send![self.sc_stream, release]; + let _: () = msg_send![self.sc_stream_output, release]; + } + } +} + +pub(crate) fn get_sources() -> oneshot::Receiver>>> { + unsafe { + let (mut tx, rx) = oneshot::channel(); + let tx = Rc::new(RefCell::new(Some(tx))); + + let block = ConcreteBlock::new(move |shareable_content: id, error: id| { + let Some(mut tx) = tx.borrow_mut().take() else { + return; + }; + let result = if error == nil { + let displays: id = msg_send![shareable_content, displays]; + let mut result = Vec::new(); + for i in 0..displays.count() { + let display = displays.objectAtIndex(i); + let source = MacScreenCaptureSource { + sc_display: msg_send![display, retain], + }; + result.push(Box::new(source) as Box); + } + Ok(result) + } else { + let msg: id = msg_send![error, localizedDescription]; + Err(anyhow!("Failed to register: {:?}", msg)) + }; + tx.send(result).ok(); + }); + let block = block.copy(); + + let _: () = msg_send![ + class!(SCShareableContent), + getShareableContentExcludingDesktopWindows:YES + onScreenWindowsOnly:YES + completionHandler:block]; + rx + } +} + +#[ctor] +unsafe fn build_classes() { + let mut decl = ClassDecl::new("GPUIStreamDelegate", class!(NSObject)).unwrap(); + decl.add_method( + sel!(outputVideoEffectDidStartForStream:), + output_video_effect_did_start_for_stream as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(outputVideoEffectDidStopForStream:), + output_video_effect_did_stop_for_stream as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(stream:didStopWithError:), + stream_did_stop_with_error as extern "C" fn(&Object, Sel, id, id), + ); + DELEGATE_CLASS = decl.register(); + + let mut decl = ClassDecl::new("GPUIStreamOutput", class!(NSObject)).unwrap(); + decl.add_method( + sel!(stream:didOutputSampleBuffer:ofType:), + stream_did_output_sample_buffer_of_type as extern "C" fn(&Object, Sel, id, id, NSInteger), + ); + decl.add_ivar::<*mut c_void>(FRAME_CALLBACK_IVAR); + + OUTPUT_CLASS = decl.register(); +} + +extern "C" fn output_video_effect_did_start_for_stream(_this: &Object, _: Sel, _stream: id) {} + +extern "C" fn output_video_effect_did_stop_for_stream(_this: &Object, _: Sel, _stream: id) {} + +extern "C" fn stream_did_stop_with_error(_this: &Object, _: Sel, _stream: id, _error: id) {} + +extern "C" fn stream_did_output_sample_buffer_of_type( + this: &Object, + _: Sel, + _stream: id, + sample_buffer: id, + buffer_type: NSInteger, +) { + if buffer_type != SCStreamOutputTypeScreen { + return; + } + + unsafe { + let sample_buffer = sample_buffer as CMSampleBufferRef; + let sample_buffer = CMSampleBuffer::wrap_under_get_rule(sample_buffer); + if let Some(buffer) = sample_buffer.image_buffer() { + let callback: Box> = + Box::from_raw(*this.get_ivar::<*mut c_void>(FRAME_CALLBACK_IVAR) as *mut _); + callback(ScreenCaptureFrame(buffer)); + mem::forget(callback); + } + } +} diff --git a/crates/gpui/src/platform/test.rs b/crates/gpui/src/platform/test.rs index d17739239e..70462cb5e2 100644 --- a/crates/gpui/src/platform/test.rs +++ b/crates/gpui/src/platform/test.rs @@ -7,3 +7,5 @@ pub(crate) use dispatcher::*; pub(crate) use display::*; pub(crate) use platform::*; pub(crate) use window::*; + +pub use platform::TestScreenCaptureSource; diff --git a/crates/gpui/src/platform/test/platform.rs b/crates/gpui/src/platform/test/platform.rs index aadbe9b595..67227b60fe 100644 --- a/crates/gpui/src/platform/test/platform.rs +++ b/crates/gpui/src/platform/test/platform.rs @@ -1,7 +1,7 @@ use crate::{ - AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, ForegroundExecutor, Keymap, - Platform, PlatformDisplay, PlatformTextSystem, Task, TestDisplay, TestWindow, WindowAppearance, - WindowParams, + px, size, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, ForegroundExecutor, + Keymap, Platform, PlatformDisplay, PlatformTextSystem, ScreenCaptureFrame, ScreenCaptureSource, + ScreenCaptureStream, Task, TestDisplay, TestWindow, WindowAppearance, WindowParams, }; use anyhow::Result; use collections::VecDeque; @@ -31,6 +31,7 @@ pub(crate) struct TestPlatform { #[cfg(any(target_os = "linux", target_os = "freebsd"))] current_primary_item: Mutex>, pub(crate) prompts: RefCell, + screen_capture_sources: RefCell>, pub opened_url: RefCell>, pub text_system: Arc, #[cfg(target_os = "windows")] @@ -38,6 +39,31 @@ pub(crate) struct TestPlatform { weak: Weak, } +#[derive(Clone)] +/// A fake screen capture source, used for testing. +pub struct TestScreenCaptureSource {} + +pub struct TestScreenCaptureStream {} + +impl ScreenCaptureSource for TestScreenCaptureSource { + fn resolution(&self) -> Result> { + Ok(size(px(1.), px(1.))) + } + + fn stream( + &self, + _frame_callback: Box, + ) -> oneshot::Receiver>> { + let (mut tx, rx) = oneshot::channel(); + let stream = TestScreenCaptureStream {}; + tx.send(Ok(Box::new(stream) as Box)) + .ok(); + rx + } +} + +impl ScreenCaptureStream for TestScreenCaptureStream {} + #[derive(Default)] pub(crate) struct TestPrompts { multiple_choice: VecDeque>, @@ -72,6 +98,7 @@ impl TestPlatform { background_executor: executor, foreground_executor, prompts: Default::default(), + screen_capture_sources: Default::default(), active_cursor: Default::default(), active_display: Rc::new(TestDisplay::new()), active_window: Default::default(), @@ -114,6 +141,10 @@ impl TestPlatform { !self.prompts.borrow().multiple_choice.is_empty() } + pub(crate) fn set_screen_capture_sources(&self, sources: Vec) { + *self.screen_capture_sources.borrow_mut() = sources; + } + pub(crate) fn prompt(&self, msg: &str, detail: Option<&str>) -> oneshot::Receiver { let (tx, rx) = oneshot::channel(); self.background_executor() @@ -202,6 +233,20 @@ impl Platform for TestPlatform { Some(self.active_display.clone()) } + fn screen_capture_sources( + &self, + ) -> oneshot::Receiver>>> { + let (mut tx, rx) = oneshot::channel(); + tx.send(Ok(self + .screen_capture_sources + .borrow() + .iter() + .map(|source| Box::new(source.clone()) as Box) + .collect())) + .ok(); + rx + } + fn active_window(&self) -> Option { self.active_window .borrow() @@ -330,6 +375,13 @@ impl Platform for TestPlatform { } } +impl TestScreenCaptureSource { + /// Create a fake screen capture source, for testing. + pub fn new() -> Self { + Self {} + } +} + #[cfg(target_os = "windows")] impl Drop for TestPlatform { fn drop(&mut self) { diff --git a/crates/gpui/src/platform/windows.rs b/crates/gpui/src/platform/windows.rs index 84cf107c70..51d09f0013 100644 --- a/crates/gpui/src/platform/windows.rs +++ b/crates/gpui/src/platform/windows.rs @@ -21,3 +21,5 @@ pub(crate) use window::*; pub(crate) use wrapper::*; pub(crate) use windows::Win32::Foundation::HWND; + +pub(crate) type PlatformScreenCaptureFrame = (); diff --git a/crates/gpui/src/platform/windows/platform.rs b/crates/gpui/src/platform/windows/platform.rs index 389b90765d..0c23a4ef7a 100644 --- a/crates/gpui/src/platform/windows/platform.rs +++ b/crates/gpui/src/platform/windows/platform.rs @@ -325,6 +325,14 @@ impl Platform for WindowsPlatform { WindowsDisplay::primary_monitor().map(|display| Rc::new(display) as Rc) } + fn screen_capture_sources( + &self, + ) -> oneshot::Receiver>>> { + let (mut tx, rx) = oneshot::channel(); + tx.send(Err(anyhow!("screen capture not implemented"))).ok(); + rx + } + fn active_window(&self) -> Option { let active_window_hwnd = unsafe { GetActiveWindow() }; self.try_get_windows_inner_from_hwnd(active_window_hwnd) diff --git a/crates/http_client/Cargo.toml b/crates/http_client/Cargo.toml index ac8e254b84..a4f10cff18 100644 --- a/crates/http_client/Cargo.toml +++ b/crates/http_client/Cargo.toml @@ -20,7 +20,7 @@ bytes.workspace = true anyhow.workspace = true derive_more.workspace = true futures.workspace = true -http = "1.1" +http.workspace = true log.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/crates/live_kit_client/.cargo/config.toml b/crates/livekit_client/.cargo/config.toml similarity index 62% rename from crates/live_kit_client/.cargo/config.toml rename to crates/livekit_client/.cargo/config.toml index b33fe211bd..77f7c9dd6c 100644 --- a/crates/live_kit_client/.cargo/config.toml +++ b/crates/livekit_client/.cargo/config.toml @@ -1,2 +1,2 @@ -[live_kit_client_test] +[livekit_client_test] rustflags = ["-C", "link-args=-ObjC"] diff --git a/crates/livekit_client/Cargo.toml b/crates/livekit_client/Cargo.toml new file mode 100644 index 0000000000..ac0c3b5740 --- /dev/null +++ b/crates/livekit_client/Cargo.toml @@ -0,0 +1,65 @@ +[package] +name = "livekit_client" +version = "0.1.0" +edition = "2021" +description = "Logic for using LiveKit with GPUI" +publish = false +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/livekit_client.rs" +doctest = false + +[[example]] +name = "test_app" + +[features] +no-webrtc = [] +test-support = [ + "collections/test-support", + "gpui/test-support", + "nanoid", +] + +[dependencies] +anyhow.workspace = true +async-trait.workspace = true +collections.workspace = true +cpal = "0.15" +futures.workspace = true +gpui.workspace = true +http_2 = { package = "http", version = "0.2.1" } +livekit_server.workspace = true +log.workspace = true +media.workspace = true +nanoid = { workspace = true, optional = true} +parking_lot.workspace = true +postage.workspace = true +util.workspace = true +http_client.workspace = true +smallvec.workspace = true +image.workspace = true + +[target.'cfg(not(target_os = "windows"))'.dependencies] +livekit.workspace = true + +[target.'cfg(target_os = "macos")'.dependencies] +core-foundation.workspace = true +coreaudio-rs = "0.12.1" + +[dev-dependencies] +collections = { workspace = true, features = ["test-support"] } +gpui = { workspace = true, features = ["test-support"] } +nanoid.workspace = true +sha2.workspace = true +simplelog.workspace = true + +[build-dependencies] +serde.workspace = true +serde_json.workspace = true + +[package.metadata.cargo-machete] +ignored = ["serde_json"] diff --git a/crates/live_kit_client/LICENSE-GPL b/crates/livekit_client/LICENSE-GPL similarity index 100% rename from crates/live_kit_client/LICENSE-GPL rename to crates/livekit_client/LICENSE-GPL diff --git a/crates/livekit_client/examples/test_app.rs b/crates/livekit_client/examples/test_app.rs new file mode 100644 index 0000000000..ef7fc91d31 --- /dev/null +++ b/crates/livekit_client/examples/test_app.rs @@ -0,0 +1,442 @@ +#![cfg_attr(windows, allow(unused))] +// TODO: For some reason mac build complains about import of postage::stream::Stream, but removal of +// it causes compile errors. +#![cfg_attr(target_os = "macos", allow(unused_imports))] + +use gpui::{ + actions, bounds, div, point, + prelude::{FluentBuilder as _, IntoElement}, + px, rgb, size, AsyncAppContext, Bounds, InteractiveElement, KeyBinding, Menu, MenuItem, + ParentElement, Pixels, Render, ScreenCaptureStream, SharedString, + StatefulInteractiveElement as _, Styled, Task, View, ViewContext, VisualContext, WindowBounds, + WindowHandle, WindowOptions, +}; +#[cfg(not(target_os = "windows"))] +use livekit_client::{ + capture_local_audio_track, capture_local_video_track, + id::ParticipantIdentity, + options::{TrackPublishOptions, VideoCodec}, + participant::{Participant, RemoteParticipant}, + play_remote_audio_track, + publication::{LocalTrackPublication, RemoteTrackPublication}, + track::{LocalTrack, RemoteTrack, RemoteVideoTrack, TrackSource}, + AudioStream, RemoteVideoTrackView, Room, RoomEvent, RoomOptions, +}; +#[cfg(not(target_os = "windows"))] +use postage::stream::Stream; + +#[cfg(target_os = "windows")] +use livekit_client::{ + participant::{Participant, RemoteParticipant}, + publication::{LocalTrackPublication, RemoteTrackPublication}, + track::{LocalTrack, RemoteTrack, RemoteVideoTrack}, + AudioStream, RemoteVideoTrackView, Room, RoomEvent, +}; + +use livekit_server::token::{self, VideoGrant}; +use log::LevelFilter; +use simplelog::SimpleLogger; + +actions!(livekit_client, [Quit]); + +#[cfg(windows)] +fn main() {} + +#[cfg(not(windows))] +fn main() { + SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger"); + + gpui::App::new().run(|cx| { + livekit_client::init( + cx.background_executor().dispatcher.clone(), + cx.http_client(), + ); + + #[cfg(any(test, feature = "test-support"))] + println!("USING TEST LIVEKIT"); + + #[cfg(not(any(test, feature = "test-support")))] + println!("USING REAL LIVEKIT"); + + cx.activate(true); + cx.on_action(quit); + cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]); + cx.set_menus(vec![Menu { + name: "Zed".into(), + items: vec![MenuItem::Action { + name: "Quit".into(), + action: Box::new(Quit), + os_action: None, + }], + }]); + + let livekit_url = std::env::var("LIVEKIT_URL").unwrap_or("http://localhost:7880".into()); + let livekit_key = std::env::var("LIVEKIT_KEY").unwrap_or("devkey".into()); + let livekit_secret = std::env::var("LIVEKIT_SECRET").unwrap_or("secret".into()); + let height = px(800.); + let width = px(800.); + + cx.spawn(|cx| async move { + let mut windows = Vec::new(); + for i in 0..2 { + let token = token::create( + &livekit_key, + &livekit_secret, + Some(&format!("test-participant-{i}")), + VideoGrant::to_join("test-room"), + ) + .unwrap(); + + let bounds = bounds(point(width * i, px(0.0)), size(width, height)); + let window = + LivekitWindow::new(livekit_url.as_str(), token.as_str(), bounds, cx.clone()) + .await; + windows.push(window); + } + }) + .detach(); + }); +} + +fn quit(_: &Quit, cx: &mut gpui::AppContext) { + cx.quit(); +} + +struct LivekitWindow { + room: Room, + microphone_track: Option, + screen_share_track: Option, + microphone_stream: Option, + screen_share_stream: Option>, + #[cfg(not(target_os = "windows"))] + remote_participants: Vec<(ParticipantIdentity, ParticipantState)>, + _events_task: Task<()>, +} + +#[derive(Default)] +struct ParticipantState { + audio_output_stream: Option<(RemoteTrackPublication, AudioStream)>, + muted: bool, + screen_share_output_view: Option<(RemoteVideoTrack, View)>, + speaking: bool, +} + +#[cfg(not(windows))] +impl LivekitWindow { + async fn new( + url: &str, + token: &str, + bounds: Bounds, + cx: AsyncAppContext, + ) -> WindowHandle { + let (room, mut events) = Room::connect(url, token, RoomOptions::default()) + .await + .unwrap(); + + cx.update(|cx| { + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |cx| { + cx.new_view(|cx| { + let _events_task = cx.spawn(|this, mut cx| async move { + while let Some(event) = events.recv().await { + this.update(&mut cx, |this: &mut LivekitWindow, cx| { + this.handle_room_event(event, cx) + }) + .ok(); + } + }); + + Self { + room, + microphone_track: None, + microphone_stream: None, + screen_share_track: None, + screen_share_stream: None, + remote_participants: Vec::new(), + _events_task, + } + }) + }, + ) + .unwrap() + }) + .unwrap() + } + + fn handle_room_event(&mut self, event: RoomEvent, cx: &mut ViewContext) { + eprintln!("event: {event:?}"); + + match event { + RoomEvent::TrackUnpublished { + publication, + participant, + } => { + let output = self.remote_participant(participant); + let unpublish_sid = publication.sid(); + if output + .audio_output_stream + .as_ref() + .map_or(false, |(track, _)| track.sid() == unpublish_sid) + { + output.audio_output_stream.take(); + } + if output + .screen_share_output_view + .as_ref() + .map_or(false, |(track, _)| track.sid() == unpublish_sid) + { + output.screen_share_output_view.take(); + } + cx.notify(); + } + + RoomEvent::TrackSubscribed { + publication, + participant, + track, + } => { + let output = self.remote_participant(participant); + match track { + RemoteTrack::Audio(track) => { + output.audio_output_stream = Some(( + publication.clone(), + play_remote_audio_track(&track, cx.background_executor()).unwrap(), + )); + } + RemoteTrack::Video(track) => { + output.screen_share_output_view = Some(( + track.clone(), + cx.new_view(|cx| RemoteVideoTrackView::new(track, cx)), + )); + } + } + cx.notify(); + } + + RoomEvent::TrackMuted { participant, .. } => { + if let Participant::Remote(participant) = participant { + self.remote_participant(participant).muted = true; + cx.notify(); + } + } + + RoomEvent::TrackUnmuted { participant, .. } => { + if let Participant::Remote(participant) = participant { + self.remote_participant(participant).muted = false; + cx.notify(); + } + } + + RoomEvent::ActiveSpeakersChanged { speakers } => { + for (identity, output) in &mut self.remote_participants { + output.speaking = speakers.iter().any(|speaker| { + if let Participant::Remote(speaker) = speaker { + speaker.identity() == *identity + } else { + false + } + }); + } + cx.notify(); + } + + _ => {} + } + + cx.notify(); + } + + fn remote_participant(&mut self, participant: RemoteParticipant) -> &mut ParticipantState { + match self + .remote_participants + .binary_search_by_key(&&participant.identity(), |row| &row.0) + { + Ok(ix) => &mut self.remote_participants[ix].1, + Err(ix) => { + self.remote_participants + .insert(ix, (participant.identity(), ParticipantState::default())); + &mut self.remote_participants[ix].1 + } + } + } + + fn toggle_mute(&mut self, cx: &mut ViewContext) { + if let Some(track) = &self.microphone_track { + if track.is_muted() { + track.unmute(); + } else { + track.mute(); + } + cx.notify(); + } else { + let participant = self.room.local_participant(); + cx.spawn(|this, mut cx| async move { + let (track, stream) = capture_local_audio_track(cx.background_executor())?.await; + let publication = participant + .publish_track( + LocalTrack::Audio(track), + TrackPublishOptions { + source: TrackSource::Microphone, + ..Default::default() + }, + ) + .await + .unwrap(); + this.update(&mut cx, |this, cx| { + this.microphone_track = Some(publication); + this.microphone_stream = Some(stream); + cx.notify(); + }) + }) + .detach(); + } + } + + fn toggle_screen_share(&mut self, cx: &mut ViewContext) { + if let Some(track) = self.screen_share_track.take() { + self.screen_share_stream.take(); + let participant = self.room.local_participant(); + cx.background_executor() + .spawn(async move { + participant.unpublish_track(&track.sid()).await.unwrap(); + }) + .detach(); + cx.notify(); + } else { + let participant = self.room.local_participant(); + let sources = cx.screen_capture_sources(); + cx.spawn(|this, mut cx| async move { + let sources = sources.await.unwrap()?; + let source = sources.into_iter().next().unwrap(); + let (track, stream) = capture_local_video_track(&*source).await?; + let publication = participant + .publish_track( + LocalTrack::Video(track), + TrackPublishOptions { + source: TrackSource::Screenshare, + video_codec: VideoCodec::H264, + ..Default::default() + }, + ) + .await + .unwrap(); + this.update(&mut cx, |this, cx| { + this.screen_share_track = Some(publication); + this.screen_share_stream = Some(stream); + cx.notify(); + }) + }) + .detach(); + } + } + + fn toggle_remote_audio_for_participant( + &mut self, + identity: &ParticipantIdentity, + cx: &mut ViewContext, + ) -> Option<()> { + let participant = self.remote_participants.iter().find_map(|(id, state)| { + if id == identity { + Some(state) + } else { + None + } + })?; + let publication = &participant.audio_output_stream.as_ref()?.0; + publication.set_enabled(!publication.is_enabled()); + cx.notify(); + Some(()) + } +} + +#[cfg(not(windows))] +impl Render for LivekitWindow { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + fn button() -> gpui::Div { + div() + .w(px(180.0)) + .h(px(30.0)) + .px_2() + .m_2() + .bg(rgb(0x8888ff)) + } + + div() + .bg(rgb(0xffffff)) + .size_full() + .flex() + .flex_col() + .child( + div().bg(rgb(0xffd4a8)).flex().flex_row().children([ + button() + .id("toggle-mute") + .child(if let Some(track) = &self.microphone_track { + if track.is_muted() { + "Unmute" + } else { + "Mute" + } + } else { + "Publish mic" + }) + .on_click(cx.listener(|this, _, cx| this.toggle_mute(cx))), + button() + .id("toggle-screen-share") + .child(if self.screen_share_track.is_none() { + "Share screen" + } else { + "Unshare screen" + }) + .on_click(cx.listener(|this, _, cx| this.toggle_screen_share(cx))), + ]), + ) + .child( + div() + .id("remote-participants") + .overflow_y_scroll() + .flex() + .flex_col() + .flex_grow() + .children(self.remote_participants.iter().map(|(identity, state)| { + div() + .h(px(300.0)) + .flex() + .flex_col() + .m_2() + .px_2() + .bg(rgb(0x8888ff)) + .child(SharedString::from(if state.speaking { + format!("{} (speaking)", &identity.0) + } else if state.muted { + format!("{} (muted)", &identity.0) + } else { + identity.0.clone() + })) + .when_some(state.audio_output_stream.as_ref(), |el, state| { + el.child( + button() + .id(SharedString::from(identity.0.clone())) + .child(if state.0.is_enabled() { + "Deafen" + } else { + "Undeafen" + }) + .on_click(cx.listener({ + let identity = identity.clone(); + move |this, _, cx| { + this.toggle_remote_audio_for_participant( + &identity, cx, + ); + } + })), + ) + }) + .children(state.screen_share_output_view.as_ref().map(|e| e.1.clone())) + })), + ) + } +} diff --git a/crates/livekit_client/src/livekit_client.rs b/crates/livekit_client/src/livekit_client.rs new file mode 100644 index 0000000000..5031dfdb33 --- /dev/null +++ b/crates/livekit_client/src/livekit_client.rs @@ -0,0 +1,661 @@ +#![cfg_attr(target_os = "windows", allow(unused))] + +mod remote_video_track_view; +#[cfg(any(test, feature = "test-support", target_os = "windows"))] +pub mod test; + +use anyhow::{anyhow, Context as _, Result}; +use cpal::traits::{DeviceTrait, HostTrait, StreamTrait as _}; +use futures::{io, Stream, StreamExt as _}; +use gpui::{ + BackgroundExecutor, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, Task, +}; +use parking_lot::Mutex; +use std::{borrow::Cow, collections::VecDeque, future::Future, pin::Pin, sync::Arc, thread}; +use util::{debug_panic, ResultExt as _}; +#[cfg(not(target_os = "windows"))] +use webrtc::{ + audio_frame::AudioFrame, + audio_source::{native::NativeAudioSource, AudioSourceOptions, RtcAudioSource}, + audio_stream::native::NativeAudioStream, + video_frame::{VideoBuffer, VideoFrame, VideoRotation}, + video_source::{native::NativeVideoSource, RtcVideoSource, VideoResolution}, + video_stream::native::NativeVideoStream, +}; + +#[cfg(all(not(any(test, feature = "test-support")), not(target_os = "windows")))] +use livekit::track::RemoteAudioTrack; +#[cfg(all(not(any(test, feature = "test-support")), not(target_os = "windows")))] +pub use livekit::*; +#[cfg(any(test, feature = "test-support", target_os = "windows"))] +use test::track::RemoteAudioTrack; +#[cfg(any(test, feature = "test-support", target_os = "windows"))] +pub use test::*; + +pub use remote_video_track_view::{RemoteVideoTrackView, RemoteVideoTrackViewEvent}; + +pub enum AudioStream { + Input { + _thread_handle: std::sync::mpsc::Sender<()>, + _transmit_task: Task<()>, + }, + Output { + _task: Task<()>, + }, +} + +struct Dispatcher(Arc); + +#[cfg(not(target_os = "windows"))] +impl livekit::dispatcher::Dispatcher for Dispatcher { + fn dispatch(&self, runnable: livekit::dispatcher::Runnable) { + self.0.dispatch(runnable, None); + } + + fn dispatch_after( + &self, + duration: std::time::Duration, + runnable: livekit::dispatcher::Runnable, + ) { + self.0.dispatch_after(duration, runnable); + } +} + +struct HttpClientAdapter(Arc); + +fn http_2_status(status: http_client::http::StatusCode) -> http_2::StatusCode { + http_2::StatusCode::from_u16(status.as_u16()) + .expect("valid status code to status code conversion") +} + +#[cfg(not(target_os = "windows"))] +impl livekit::dispatcher::HttpClient for HttpClientAdapter { + fn get( + &self, + url: &str, + ) -> Pin> + Send>> { + let http_client = self.0.clone(); + let url = url.to_string(); + Box::pin(async move { + let response = http_client + .get(&url, http_client::AsyncBody::empty(), false) + .await + .map_err(io::Error::other)?; + Ok(livekit::dispatcher::Response { + status: http_2_status(response.status()), + body: Box::pin(response.into_body()), + }) + }) + } + + fn send_async( + &self, + request: http_2::Request>, + ) -> Pin> + Send>> { + let http_client = self.0.clone(); + let mut builder = http_client::http::Request::builder() + .method(request.method().as_str()) + .uri(request.uri().to_string()); + + for (key, value) in request.headers().iter() { + builder = builder.header(key.as_str(), value.as_bytes()); + } + + if !request.extensions().is_empty() { + debug_panic!( + "Livekit sent an HTTP request with a protocol extension that Zed doesn't support!" + ); + } + + let request = builder + .body(http_client::AsyncBody::from_bytes( + request.into_body().into(), + )) + .unwrap(); + + Box::pin(async move { + let response = http_client.send(request).await.map_err(io::Error::other)?; + Ok(livekit::dispatcher::Response { + status: http_2_status(response.status()), + body: Box::pin(response.into_body()), + }) + }) + } +} + +#[cfg(target_os = "windows")] +pub fn init( + dispatcher: Arc, + http_client: Arc, +) { +} + +#[cfg(not(target_os = "windows"))] +pub fn init( + dispatcher: Arc, + http_client: Arc, +) { + livekit::dispatcher::set_dispatcher(Dispatcher(dispatcher)); + livekit::dispatcher::set_http_client(HttpClientAdapter(http_client)); +} + +#[cfg(not(target_os = "windows"))] +pub async fn capture_local_video_track( + capture_source: &dyn ScreenCaptureSource, +) -> Result<(track::LocalVideoTrack, Box)> { + let resolution = capture_source.resolution()?; + let track_source = NativeVideoSource::new(VideoResolution { + width: resolution.width.0 as u32, + height: resolution.height.0 as u32, + }); + + let capture_stream = capture_source + .stream({ + let track_source = track_source.clone(); + Box::new(move |frame| { + if let Some(buffer) = video_frame_buffer_to_webrtc(frame) { + track_source.capture_frame(&VideoFrame { + rotation: VideoRotation::VideoRotation0, + timestamp_us: 0, + buffer, + }); + } + }) + }) + .await??; + + Ok(( + track::LocalVideoTrack::create_video_track( + "screen share", + RtcVideoSource::Native(track_source), + ), + capture_stream, + )) +} + +#[cfg(not(target_os = "windows"))] +pub fn capture_local_audio_track( + background_executor: &BackgroundExecutor, +) -> Result> { + use util::maybe; + + let (frame_tx, mut frame_rx) = futures::channel::mpsc::unbounded(); + let (thread_handle, thread_kill_rx) = std::sync::mpsc::channel::<()>(); + let sample_rate; + let channels; + + if cfg!(any(test, feature = "test-support")) { + sample_rate = 2; + channels = 1; + } else { + let (device, config) = default_device(true)?; + sample_rate = config.sample_rate().0; + channels = config.channels() as u32; + thread::spawn(move || { + maybe!({ + if let Some(name) = device.name().ok() { + log::info!("Using microphone: {}", name) + } else { + log::info!("Using microphone: "); + } + + let stream = device + .build_input_stream_raw( + &config.config(), + cpal::SampleFormat::I16, + move |data, _: &_| { + frame_tx + .unbounded_send(AudioFrame { + data: Cow::Owned(data.as_slice::().unwrap().to_vec()), + sample_rate, + num_channels: channels, + samples_per_channel: data.len() as u32 / channels, + }) + .ok(); + }, + |err| log::error!("error capturing audio track: {:?}", err), + None, + ) + .context("failed to build input stream")?; + + stream.play()?; + // Keep the thread alive and holding onto the `stream` + thread_kill_rx.recv().ok(); + anyhow::Ok(Some(())) + }) + .log_err(); + }); + } + + Ok(background_executor.spawn({ + let background_executor = background_executor.clone(); + async move { + let source = NativeAudioSource::new( + AudioSourceOptions { + echo_cancellation: true, + noise_suppression: true, + auto_gain_control: true, + }, + sample_rate, + channels, + 100, + ); + let transmit_task = background_executor.spawn({ + let source = source.clone(); + async move { + while let Some(frame) = frame_rx.next().await { + source.capture_frame(&frame).await.log_err(); + } + } + }); + + let track = track::LocalAudioTrack::create_audio_track( + "microphone", + RtcAudioSource::Native(source), + ); + + ( + track, + AudioStream::Input { + _thread_handle: thread_handle, + _transmit_task: transmit_task, + }, + ) + } + })) +} + +#[cfg(not(target_os = "windows"))] +pub fn play_remote_audio_track( + track: &RemoteAudioTrack, + background_executor: &BackgroundExecutor, +) -> Result { + let track = track.clone(); + // We track device changes in our output because Livekit has a resampler built in, + // and it's easy to create a new native audio stream when the device changes. + if cfg!(any(test, feature = "test-support")) { + Ok(AudioStream::Output { + _task: background_executor.spawn(async {}), + }) + } else { + let mut default_change_listener = DeviceChangeListener::new(false)?; + let (output_device, output_config) = default_device(false)?; + + let _task = background_executor.spawn({ + let background_executor = background_executor.clone(); + async move { + let (mut _receive_task, mut _thread) = + start_output_stream(output_config, output_device, &track, &background_executor); + + while let Some(_) = default_change_listener.next().await { + let Some((output_device, output_config)) = get_default_output().log_err() + else { + continue; + }; + + if let Ok(name) = output_device.name() { + log::info!("Using speaker: {}", name) + } else { + log::info!("Using speaker: ") + } + + (_receive_task, _thread) = start_output_stream( + output_config, + output_device, + &track, + &background_executor, + ); + } + + futures::future::pending::<()>().await; + } + }); + + Ok(AudioStream::Output { _task }) + } +} + +fn default_device(input: bool) -> anyhow::Result<(cpal::Device, cpal::SupportedStreamConfig)> { + let device; + let config; + if input { + device = cpal::default_host() + .default_input_device() + .ok_or_else(|| anyhow!("no audio input device available"))?; + config = device + .default_input_config() + .context("failed to get default input config")?; + } else { + device = cpal::default_host() + .default_output_device() + .ok_or_else(|| anyhow!("no audio output device available"))?; + config = device + .default_output_config() + .context("failed to get default output config")?; + } + Ok((device, config)) +} + +#[cfg(not(target_os = "windows"))] +fn get_default_output() -> anyhow::Result<(cpal::Device, cpal::SupportedStreamConfig)> { + let host = cpal::default_host(); + let output_device = host + .default_output_device() + .context("failed to read default output device")?; + let output_config = output_device.default_output_config()?; + Ok((output_device, output_config)) +} + +#[cfg(not(target_os = "windows"))] +fn start_output_stream( + output_config: cpal::SupportedStreamConfig, + output_device: cpal::Device, + track: &track::RemoteAudioTrack, + background_executor: &BackgroundExecutor, +) -> (Task<()>, std::sync::mpsc::Sender<()>) { + let buffer = Arc::new(Mutex::new(VecDeque::::new())); + let sample_rate = output_config.sample_rate(); + + let mut stream = NativeAudioStream::new( + track.rtc_track(), + sample_rate.0 as i32, + output_config.channels() as i32, + ); + + let receive_task = background_executor.spawn({ + let buffer = buffer.clone(); + async move { + const MS_OF_BUFFER: u32 = 100; + const MS_IN_SEC: u32 = 1000; + while let Some(frame) = stream.next().await { + let frame_size = frame.samples_per_channel * frame.num_channels; + debug_assert!(frame.data.len() == frame_size as usize); + + let buffer_size = + ((frame.sample_rate * frame.num_channels) / MS_IN_SEC * MS_OF_BUFFER) as usize; + + let mut buffer = buffer.lock(); + let new_size = buffer.len() + frame.data.len(); + if new_size > buffer_size { + let overflow = new_size - buffer_size; + buffer.drain(0..overflow); + } + + buffer.extend(frame.data.iter()); + } + } + }); + + // The _output_stream needs to be on it's own thread because it's !Send + // and we experienced a deadlock when it's created on the main thread. + let (thread, end_on_drop_rx) = std::sync::mpsc::channel::<()>(); + thread::spawn(move || { + if cfg!(any(test, feature = "test-support")) { + // Can't play audio in tests + return; + } + + let output_stream = output_device.build_output_stream( + &output_config.config(), + { + let buffer = buffer.clone(); + move |data, _info| { + let mut buffer = buffer.lock(); + if buffer.len() < data.len() { + // Instead of partially filling a buffer, output silence. If a partial + // buffer was outputted then this could lead to a perpetual state of + // outputting partial buffers as it never gets filled enough for a full + // frame. + data.fill(0); + } else { + // SAFETY: We know that buffer has at least data.len() values in it. + // because we just checked + let mut drain = buffer.drain(..data.len()); + data.fill_with(|| unsafe { drain.next().unwrap_unchecked() }); + } + } + }, + |error| log::error!("error playing audio track: {:?}", error), + None, + ); + + let Some(output_stream) = output_stream.log_err() else { + return; + }; + + output_stream.play().log_err(); + // Block forever to keep the output stream alive + end_on_drop_rx.recv().ok(); + }); + + (receive_task, thread) +} + +#[cfg(target_os = "windows")] +pub fn play_remote_video_track( + track: &track::RemoteVideoTrack, +) -> impl Stream { + futures::stream::empty() +} + +#[cfg(not(target_os = "windows"))] +pub fn play_remote_video_track( + track: &track::RemoteVideoTrack, +) -> impl Stream { + NativeVideoStream::new(track.rtc_track()) + .filter_map(|frame| async move { video_frame_buffer_from_webrtc(frame.buffer) }) +} + +#[cfg(target_os = "macos")] +pub type RemoteVideoFrame = media::core_video::CVImageBuffer; + +#[cfg(target_os = "macos")] +fn video_frame_buffer_from_webrtc(buffer: Box) -> Option { + use core_foundation::base::TCFType as _; + use media::core_video::CVImageBuffer; + + let buffer = buffer.as_native()?; + let pixel_buffer = buffer.get_cv_pixel_buffer(); + if pixel_buffer.is_null() { + return None; + } + + unsafe { Some(CVImageBuffer::wrap_under_get_rule(pixel_buffer as _)) } +} + +#[cfg(not(target_os = "macos"))] +pub type RemoteVideoFrame = Arc; + +#[cfg(not(any(target_os = "macos", target_os = "windows")))] +fn video_frame_buffer_from_webrtc(buffer: Box) -> Option { + use gpui::RenderImage; + use image::{Frame, RgbaImage}; + use livekit::webrtc::prelude::VideoFormatType; + use smallvec::SmallVec; + use std::alloc::{alloc, Layout}; + + let width = buffer.width(); + let height = buffer.height(); + let stride = width * 4; + let byte_len = (stride * height) as usize; + let argb_image = unsafe { + // Motivation for this unsafe code is to avoid initializing the frame data, since to_argb + // will write all bytes anyway. + let start_ptr = alloc(Layout::array::(byte_len).log_err()?); + if start_ptr.is_null() { + return None; + } + let bgra_frame_slice = std::slice::from_raw_parts_mut(start_ptr, byte_len); + buffer.to_argb( + VideoFormatType::ARGB, // For some reason, this displays correctly while RGBA (the correct format) does not + bgra_frame_slice, + stride, + width as i32, + height as i32, + ); + Vec::from_raw_parts(start_ptr, byte_len, byte_len) + }; + + Some(Arc::new(RenderImage::new(SmallVec::from_elem( + Frame::new( + RgbaImage::from_raw(width, height, argb_image) + .with_context(|| "Bug: not enough bytes allocated for image.") + .log_err()?, + ), + 1, + )))) +} + +#[cfg(target_os = "macos")] +fn video_frame_buffer_to_webrtc(frame: ScreenCaptureFrame) -> Option> { + use core_foundation::base::TCFType as _; + + let pixel_buffer = frame.0.as_concrete_TypeRef(); + std::mem::forget(frame.0); + unsafe { + Some(webrtc::video_frame::native::NativeBuffer::from_cv_pixel_buffer(pixel_buffer as _)) + } +} + +#[cfg(not(any(target_os = "macos", target_os = "windows")))] +fn video_frame_buffer_to_webrtc(_frame: ScreenCaptureFrame) -> Option> { + None as Option> +} + +trait DeviceChangeListenerApi: Stream + Sized { + fn new(input: bool) -> Result; +} + +#[cfg(target_os = "macos")] +mod macos { + + use coreaudio::sys::{ + kAudioHardwarePropertyDefaultInputDevice, kAudioHardwarePropertyDefaultOutputDevice, + kAudioObjectPropertyElementMaster, kAudioObjectPropertyScopeGlobal, + kAudioObjectSystemObject, AudioObjectAddPropertyListener, AudioObjectID, + AudioObjectPropertyAddress, AudioObjectRemovePropertyListener, OSStatus, + }; + use futures::{channel::mpsc::UnboundedReceiver, StreamExt}; + + use crate::DeviceChangeListenerApi; + + /// Implementation from: https://github.com/zed-industries/cpal/blob/fd8bc2fd39f1f5fdee5a0690656caff9a26d9d50/src/host/coreaudio/macos/property_listener.rs#L15 + pub struct CoreAudioDefaultDeviceChangeListener { + rx: UnboundedReceiver<()>, + callback: Box, + input: bool, + } + + trait _AssertSend: Send {} + impl _AssertSend for CoreAudioDefaultDeviceChangeListener {} + + struct PropertyListenerCallbackWrapper(Box); + + unsafe extern "C" fn property_listener_handler_shim( + _: AudioObjectID, + _: u32, + _: *const AudioObjectPropertyAddress, + callback: *mut ::std::os::raw::c_void, + ) -> OSStatus { + let wrapper = callback as *mut PropertyListenerCallbackWrapper; + (*wrapper).0(); + 0 + } + + impl DeviceChangeListenerApi for CoreAudioDefaultDeviceChangeListener { + fn new(input: bool) -> gpui::Result { + let (tx, rx) = futures::channel::mpsc::unbounded(); + + let callback = Box::new(PropertyListenerCallbackWrapper(Box::new(move || { + tx.unbounded_send(()).ok(); + }))); + + unsafe { + coreaudio::Error::from_os_status(AudioObjectAddPropertyListener( + kAudioObjectSystemObject, + &AudioObjectPropertyAddress { + mSelector: if input { + kAudioHardwarePropertyDefaultInputDevice + } else { + kAudioHardwarePropertyDefaultOutputDevice + }, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMaster, + }, + Some(property_listener_handler_shim), + &*callback as *const _ as *mut _, + ))?; + } + + Ok(Self { + rx, + callback, + input, + }) + } + } + + impl Drop for CoreAudioDefaultDeviceChangeListener { + fn drop(&mut self) { + unsafe { + AudioObjectRemovePropertyListener( + kAudioObjectSystemObject, + &AudioObjectPropertyAddress { + mSelector: if self.input { + kAudioHardwarePropertyDefaultInputDevice + } else { + kAudioHardwarePropertyDefaultOutputDevice + }, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMaster, + }, + Some(property_listener_handler_shim), + &*self.callback as *const _ as *mut _, + ); + } + } + } + + impl futures::Stream for CoreAudioDefaultDeviceChangeListener { + type Item = (); + + fn poll_next( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + self.rx.poll_next_unpin(cx) + } + } +} + +#[cfg(target_os = "macos")] +type DeviceChangeListener = macos::CoreAudioDefaultDeviceChangeListener; + +#[cfg(not(target_os = "macos"))] +mod noop_change_listener { + use std::task::Poll; + + use crate::DeviceChangeListenerApi; + + pub struct NoopOutputDeviceChangelistener {} + + impl DeviceChangeListenerApi for NoopOutputDeviceChangelistener { + fn new(_input: bool) -> anyhow::Result { + Ok(NoopOutputDeviceChangelistener {}) + } + } + + impl futures::Stream for NoopOutputDeviceChangelistener { + type Item = (); + + fn poll_next( + self: std::pin::Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + ) -> Poll> { + Poll::Pending + } + } +} + +#[cfg(not(target_os = "macos"))] +type DeviceChangeListener = noop_change_listener::NoopOutputDeviceChangelistener; diff --git a/crates/livekit_client/src/remote_video_track_view.rs b/crates/livekit_client/src/remote_video_track_view.rs new file mode 100644 index 0000000000..d7618391d6 --- /dev/null +++ b/crates/livekit_client/src/remote_video_track_view.rs @@ -0,0 +1,99 @@ +use crate::track::RemoteVideoTrack; +use anyhow::Result; +use futures::StreamExt as _; +use gpui::{Empty, EventEmitter, IntoElement, Render, Task, View, ViewContext, VisualContext as _}; + +pub struct RemoteVideoTrackView { + track: RemoteVideoTrack, + latest_frame: Option, + #[cfg(not(target_os = "macos"))] + current_rendered_frame: Option, + #[cfg(not(target_os = "macos"))] + previous_rendered_frame: Option, + _maintain_frame: Task>, +} + +#[derive(Debug)] +pub enum RemoteVideoTrackViewEvent { + Close, +} + +impl RemoteVideoTrackView { + pub fn new(track: RemoteVideoTrack, cx: &mut ViewContext) -> Self { + cx.focus_handle(); + let frames = super::play_remote_video_track(&track); + + Self { + track, + latest_frame: None, + _maintain_frame: cx.spawn(|this, mut cx| async move { + futures::pin_mut!(frames); + while let Some(frame) = frames.next().await { + this.update(&mut cx, |this, cx| { + this.latest_frame = Some(frame); + cx.notify(); + })?; + } + this.update(&mut cx, |_this, cx| { + #[cfg(not(target_os = "macos"))] + { + use util::ResultExt as _; + if let Some(frame) = _this.previous_rendered_frame.take() { + cx.window_context().drop_image(frame).log_err(); + } + // TODO(mgsloan): This might leak the last image of the screenshare if + // render is called after the screenshare ends. + if let Some(frame) = _this.current_rendered_frame.take() { + cx.window_context().drop_image(frame).log_err(); + } + } + cx.emit(RemoteVideoTrackViewEvent::Close) + })?; + Ok(()) + }), + #[cfg(not(target_os = "macos"))] + current_rendered_frame: None, + #[cfg(not(target_os = "macos"))] + previous_rendered_frame: None, + } + } + + pub fn clone(&self, cx: &mut ViewContext) -> View { + cx.new_view(|cx| Self::new(self.track.clone(), cx)) + } +} + +impl EventEmitter for RemoteVideoTrackView {} + +impl Render for RemoteVideoTrackView { + fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { + #[cfg(target_os = "macos")] + if let Some(latest_frame) = &self.latest_frame { + use gpui::Styled as _; + return gpui::surface(latest_frame.clone()) + .size_full() + .into_any_element(); + } + + #[cfg(not(target_os = "macos"))] + if let Some(latest_frame) = &self.latest_frame { + use gpui::Styled as _; + if let Some(current_rendered_frame) = self.current_rendered_frame.take() { + if let Some(frame) = self.previous_rendered_frame.take() { + // Only drop the frame if it's not also the current frame. + if frame.id != current_rendered_frame.id { + use util::ResultExt as _; + _cx.window_context().drop_image(frame).log_err(); + } + } + self.previous_rendered_frame = Some(current_rendered_frame) + } + self.current_rendered_frame = Some(latest_frame.clone()); + return gpui::img(latest_frame.clone()) + .size_full() + .into_any_element(); + } + + Empty.into_any_element() + } +} diff --git a/crates/livekit_client/src/test.rs b/crates/livekit_client/src/test.rs new file mode 100644 index 0000000000..e67189c09c --- /dev/null +++ b/crates/livekit_client/src/test.rs @@ -0,0 +1,825 @@ +pub mod participant; +pub mod publication; +pub mod track; + +#[cfg(not(windows))] +pub mod webrtc; + +#[cfg(not(windows))] +use self::id::*; +use self::{participant::*, publication::*, track::*}; +use anyhow::{anyhow, Context, Result}; +use async_trait::async_trait; +use collections::{btree_map::Entry as BTreeEntry, hash_map::Entry, BTreeMap, HashMap, HashSet}; +use gpui::BackgroundExecutor; +#[cfg(not(windows))] +use livekit::options::TrackPublishOptions; +use livekit_server::{proto, token}; +use parking_lot::Mutex; +use postage::{mpsc, sink::Sink}; +use std::sync::{ + atomic::{AtomicBool, Ordering::SeqCst}, + Arc, Weak, +}; + +#[cfg(not(windows))] +pub use livekit::{id, options, ConnectionState, DisconnectReason, RoomOptions}; + +static SERVERS: Mutex>> = Mutex::new(BTreeMap::new()); + +pub struct TestServer { + pub url: String, + pub api_key: String, + pub secret_key: String, + #[cfg(not(target_os = "windows"))] + rooms: Mutex>, + executor: BackgroundExecutor, +} + +#[cfg(not(target_os = "windows"))] +impl TestServer { + pub fn create( + url: String, + api_key: String, + secret_key: String, + executor: BackgroundExecutor, + ) -> Result> { + let mut servers = SERVERS.lock(); + if let BTreeEntry::Vacant(e) = servers.entry(url.clone()) { + let server = Arc::new(TestServer { + url, + api_key, + secret_key, + rooms: Default::default(), + executor, + }); + e.insert(server.clone()); + Ok(server) + } else { + Err(anyhow!("a server with url {:?} already exists", url)) + } + } + + fn get(url: &str) -> Result> { + Ok(SERVERS + .lock() + .get(url) + .ok_or_else(|| anyhow!("no server found for url"))? + .clone()) + } + + pub fn teardown(&self) -> Result<()> { + SERVERS + .lock() + .remove(&self.url) + .ok_or_else(|| anyhow!("server with url {:?} does not exist", self.url))?; + Ok(()) + } + + pub fn create_api_client(&self) -> TestApiClient { + TestApiClient { + url: self.url.clone(), + } + } + + pub async fn create_room(&self, room: String) -> Result<()> { + self.executor.simulate_random_delay().await; + + let mut server_rooms = self.rooms.lock(); + if let Entry::Vacant(e) = server_rooms.entry(room.clone()) { + e.insert(Default::default()); + Ok(()) + } else { + Err(anyhow!("room {:?} already exists", room)) + } + } + + async fn delete_room(&self, room: String) -> Result<()> { + self.executor.simulate_random_delay().await; + + let mut server_rooms = self.rooms.lock(); + server_rooms + .remove(&room) + .ok_or_else(|| anyhow!("room {:?} does not exist", room))?; + Ok(()) + } + + async fn join_room(&self, token: String, client_room: Room) -> Result { + self.executor.simulate_random_delay().await; + + let claims = livekit_server::token::validate(&token, &self.secret_key)?; + let identity = ParticipantIdentity(claims.sub.unwrap().to_string()); + let room_name = claims.video.room.unwrap(); + let mut server_rooms = self.rooms.lock(); + let room = (*server_rooms).entry(room_name.to_string()).or_default(); + + if let Entry::Vacant(e) = room.client_rooms.entry(identity.clone()) { + for server_track in &room.video_tracks { + let track = RemoteTrack::Video(RemoteVideoTrack { + server_track: server_track.clone(), + _room: client_room.downgrade(), + }); + client_room + .0 + .lock() + .updates_tx + .blocking_send(RoomEvent::TrackSubscribed { + track: track.clone(), + publication: RemoteTrackPublication { + sid: server_track.sid.clone(), + room: client_room.downgrade(), + track, + }, + participant: RemoteParticipant { + room: client_room.downgrade(), + identity: server_track.publisher_id.clone(), + }, + }) + .unwrap(); + } + for server_track in &room.audio_tracks { + let track = RemoteTrack::Audio(RemoteAudioTrack { + server_track: server_track.clone(), + room: client_room.downgrade(), + }); + client_room + .0 + .lock() + .updates_tx + .blocking_send(RoomEvent::TrackSubscribed { + track: track.clone(), + publication: RemoteTrackPublication { + sid: server_track.sid.clone(), + room: client_room.downgrade(), + track, + }, + participant: RemoteParticipant { + room: client_room.downgrade(), + identity: server_track.publisher_id.clone(), + }, + }) + .unwrap(); + } + e.insert(client_room); + Ok(identity) + } else { + Err(anyhow!( + "{:?} attempted to join room {:?} twice", + identity, + room_name + )) + } + } + + async fn leave_room(&self, token: String) -> Result<()> { + self.executor.simulate_random_delay().await; + + let claims = livekit_server::token::validate(&token, &self.secret_key)?; + let identity = ParticipantIdentity(claims.sub.unwrap().to_string()); + let room_name = claims.video.room.unwrap(); + let mut server_rooms = self.rooms.lock(); + let room = server_rooms + .get_mut(&*room_name) + .ok_or_else(|| anyhow!("room {} does not exist", room_name))?; + room.client_rooms.remove(&identity).ok_or_else(|| { + anyhow!( + "{:?} attempted to leave room {:?} before joining it", + identity, + room_name + ) + })?; + Ok(()) + } + + fn remote_participants( + &self, + token: String, + ) -> Result> { + let claims = livekit_server::token::validate(&token, &self.secret_key)?; + let local_identity = ParticipantIdentity(claims.sub.unwrap().to_string()); + let room_name = claims.video.room.unwrap().to_string(); + + if let Some(server_room) = self.rooms.lock().get(&room_name) { + let room = server_room + .client_rooms + .get(&local_identity) + .unwrap() + .downgrade(); + Ok(server_room + .client_rooms + .iter() + .filter(|(identity, _)| *identity != &local_identity) + .map(|(identity, _)| { + ( + identity.clone(), + RemoteParticipant { + room: room.clone(), + identity: identity.clone(), + }, + ) + }) + .collect()) + } else { + Ok(Default::default()) + } + } + + async fn remove_participant( + &self, + room_name: String, + identity: ParticipantIdentity, + ) -> Result<()> { + self.executor.simulate_random_delay().await; + + let mut server_rooms = self.rooms.lock(); + let room = server_rooms + .get_mut(&room_name) + .ok_or_else(|| anyhow!("room {} does not exist", room_name))?; + room.client_rooms.remove(&identity).ok_or_else(|| { + anyhow!( + "participant {:?} did not join room {:?}", + identity, + room_name + ) + })?; + Ok(()) + } + + async fn update_participant( + &self, + room_name: String, + identity: String, + permission: proto::ParticipantPermission, + ) -> Result<()> { + self.executor.simulate_random_delay().await; + + let mut server_rooms = self.rooms.lock(); + let room = server_rooms + .get_mut(&room_name) + .ok_or_else(|| anyhow!("room {} does not exist", room_name))?; + room.participant_permissions + .insert(ParticipantIdentity(identity), permission); + Ok(()) + } + + pub async fn disconnect_client(&self, client_identity: String) { + let client_identity = ParticipantIdentity(client_identity); + + self.executor.simulate_random_delay().await; + + let mut server_rooms = self.rooms.lock(); + for room in server_rooms.values_mut() { + if let Some(room) = room.client_rooms.remove(&client_identity) { + let mut room = room.0.lock(); + room.connection_state = ConnectionState::Disconnected; + room.updates_tx + .blocking_send(RoomEvent::Disconnected { + reason: DisconnectReason::SignalClose, + }) + .ok(); + } + } + } + + async fn publish_video_track( + &self, + token: String, + _local_track: LocalVideoTrack, + ) -> Result { + self.executor.simulate_random_delay().await; + + let claims = livekit_server::token::validate(&token, &self.secret_key)?; + let identity = ParticipantIdentity(claims.sub.unwrap().to_string()); + let room_name = claims.video.room.unwrap(); + + let mut server_rooms = self.rooms.lock(); + let room = server_rooms + .get_mut(&*room_name) + .ok_or_else(|| anyhow!("room {} does not exist", room_name))?; + + let can_publish = room + .participant_permissions + .get(&identity) + .map(|permission| permission.can_publish) + .or(claims.video.can_publish) + .unwrap_or(true); + + if !can_publish { + return Err(anyhow!("user is not allowed to publish")); + } + + let sid: TrackSid = format!("TR_{}", nanoid::nanoid!(17)).try_into().unwrap(); + let server_track = Arc::new(TestServerVideoTrack { + sid: sid.clone(), + publisher_id: identity.clone(), + }); + + room.video_tracks.push(server_track.clone()); + + for (room_identity, client_room) in &room.client_rooms { + if *room_identity != identity { + let track = RemoteTrack::Video(RemoteVideoTrack { + server_track: server_track.clone(), + _room: client_room.downgrade(), + }); + let publication = RemoteTrackPublication { + sid: sid.clone(), + room: client_room.downgrade(), + track: track.clone(), + }; + let participant = RemoteParticipant { + identity: identity.clone(), + room: client_room.downgrade(), + }; + client_room + .0 + .lock() + .updates_tx + .blocking_send(RoomEvent::TrackSubscribed { + track, + publication, + participant, + }) + .unwrap(); + } + } + + Ok(sid) + } + + async fn publish_audio_track( + &self, + token: String, + _local_track: &LocalAudioTrack, + ) -> Result { + self.executor.simulate_random_delay().await; + + let claims = livekit_server::token::validate(&token, &self.secret_key)?; + let identity = ParticipantIdentity(claims.sub.unwrap().to_string()); + let room_name = claims.video.room.unwrap(); + + let mut server_rooms = self.rooms.lock(); + let room = server_rooms + .get_mut(&*room_name) + .ok_or_else(|| anyhow!("room {} does not exist", room_name))?; + + let can_publish = room + .participant_permissions + .get(&identity) + .map(|permission| permission.can_publish) + .or(claims.video.can_publish) + .unwrap_or(true); + + if !can_publish { + return Err(anyhow!("user is not allowed to publish")); + } + + let sid: TrackSid = format!("TR_{}", nanoid::nanoid!(17)).try_into().unwrap(); + let server_track = Arc::new(TestServerAudioTrack { + sid: sid.clone(), + publisher_id: identity.clone(), + muted: AtomicBool::new(false), + }); + + room.audio_tracks.push(server_track.clone()); + + for (room_identity, client_room) in &room.client_rooms { + if *room_identity != identity { + let track = RemoteTrack::Audio(RemoteAudioTrack { + server_track: server_track.clone(), + room: client_room.downgrade(), + }); + let publication = RemoteTrackPublication { + sid: sid.clone(), + room: client_room.downgrade(), + track: track.clone(), + }; + let participant = RemoteParticipant { + identity: identity.clone(), + room: client_room.downgrade(), + }; + client_room + .0 + .lock() + .updates_tx + .blocking_send(RoomEvent::TrackSubscribed { + track, + publication, + participant, + }) + .ok(); + } + } + + Ok(sid) + } + + async fn unpublish_track(&self, _token: String, _track: &TrackSid) -> Result<()> { + Ok(()) + } + + fn set_track_muted(&self, token: &str, track_sid: &TrackSid, muted: bool) -> Result<()> { + let claims = livekit_server::token::validate(&token, &self.secret_key)?; + let room_name = claims.video.room.unwrap(); + let identity = ParticipantIdentity(claims.sub.unwrap().to_string()); + let mut server_rooms = self.rooms.lock(); + let room = server_rooms + .get_mut(&*room_name) + .ok_or_else(|| anyhow!("room {} does not exist", room_name))?; + if let Some(track) = room + .audio_tracks + .iter_mut() + .find(|track| track.sid == *track_sid) + { + track.muted.store(muted, SeqCst); + for (id, client_room) in room.client_rooms.iter() { + if *id != identity { + let participant = Participant::Remote(RemoteParticipant { + identity: identity.clone(), + room: client_room.downgrade(), + }); + let track = RemoteTrack::Audio(RemoteAudioTrack { + server_track: track.clone(), + room: client_room.downgrade(), + }); + let publication = TrackPublication::Remote(RemoteTrackPublication { + sid: track_sid.clone(), + room: client_room.downgrade(), + track, + }); + + let event = if muted { + RoomEvent::TrackMuted { + participant, + publication, + } + } else { + RoomEvent::TrackUnmuted { + participant, + publication, + } + }; + + client_room + .0 + .lock() + .updates_tx + .blocking_send(event) + .unwrap(); + } + } + } + Ok(()) + } + + fn is_track_muted(&self, token: &str, track_sid: &TrackSid) -> Option { + let claims = livekit_server::token::validate(&token, &self.secret_key).ok()?; + let room_name = claims.video.room.unwrap(); + + let mut server_rooms = self.rooms.lock(); + let room = server_rooms.get_mut(&*room_name)?; + room.audio_tracks.iter().find_map(|track| { + if track.sid == *track_sid { + Some(track.muted.load(SeqCst)) + } else { + None + } + }) + } + + fn video_tracks(&self, token: String) -> Result> { + let claims = livekit_server::token::validate(&token, &self.secret_key)?; + let room_name = claims.video.room.unwrap(); + let identity = ParticipantIdentity(claims.sub.unwrap().to_string()); + + let mut server_rooms = self.rooms.lock(); + let room = server_rooms + .get_mut(&*room_name) + .ok_or_else(|| anyhow!("room {} does not exist", room_name))?; + let client_room = room + .client_rooms + .get(&identity) + .ok_or_else(|| anyhow!("not a participant in room"))?; + Ok(room + .video_tracks + .iter() + .map(|track| RemoteVideoTrack { + server_track: track.clone(), + _room: client_room.downgrade(), + }) + .collect()) + } + + fn audio_tracks(&self, token: String) -> Result> { + let claims = livekit_server::token::validate(&token, &self.secret_key)?; + let room_name = claims.video.room.unwrap(); + let identity = ParticipantIdentity(claims.sub.unwrap().to_string()); + + let mut server_rooms = self.rooms.lock(); + let room = server_rooms + .get_mut(&*room_name) + .ok_or_else(|| anyhow!("room {} does not exist", room_name))?; + let client_room = room + .client_rooms + .get(&identity) + .ok_or_else(|| anyhow!("not a participant in room"))?; + Ok(room + .audio_tracks + .iter() + .map(|track| RemoteAudioTrack { + server_track: track.clone(), + room: client_room.downgrade(), + }) + .collect()) + } +} + +#[cfg(not(target_os = "windows"))] +#[derive(Default, Debug)] +struct TestServerRoom { + client_rooms: HashMap, + video_tracks: Vec>, + audio_tracks: Vec>, + participant_permissions: HashMap, +} + +#[cfg(not(target_os = "windows"))] +#[derive(Debug)] +struct TestServerVideoTrack { + sid: TrackSid, + publisher_id: ParticipantIdentity, + // frames_rx: async_broadcast::Receiver, +} + +#[cfg(not(target_os = "windows"))] +#[derive(Debug)] +struct TestServerAudioTrack { + sid: TrackSid, + publisher_id: ParticipantIdentity, + muted: AtomicBool, +} + +pub struct TestApiClient { + url: String, +} + +#[derive(Clone, Debug)] +#[non_exhaustive] +pub enum RoomEvent { + ParticipantConnected(RemoteParticipant), + ParticipantDisconnected(RemoteParticipant), + LocalTrackPublished { + publication: LocalTrackPublication, + track: LocalTrack, + participant: LocalParticipant, + }, + LocalTrackUnpublished { + publication: LocalTrackPublication, + participant: LocalParticipant, + }, + TrackSubscribed { + track: RemoteTrack, + publication: RemoteTrackPublication, + participant: RemoteParticipant, + }, + TrackUnsubscribed { + track: RemoteTrack, + publication: RemoteTrackPublication, + participant: RemoteParticipant, + }, + TrackSubscriptionFailed { + participant: RemoteParticipant, + error: String, + #[cfg(not(target_os = "windows"))] + track_sid: TrackSid, + }, + TrackPublished { + publication: RemoteTrackPublication, + participant: RemoteParticipant, + }, + TrackUnpublished { + publication: RemoteTrackPublication, + participant: RemoteParticipant, + }, + TrackMuted { + participant: Participant, + publication: TrackPublication, + }, + TrackUnmuted { + participant: Participant, + publication: TrackPublication, + }, + RoomMetadataChanged { + old_metadata: String, + metadata: String, + }, + ParticipantMetadataChanged { + participant: Participant, + old_metadata: String, + metadata: String, + }, + ParticipantNameChanged { + participant: Participant, + old_name: String, + name: String, + }, + ActiveSpeakersChanged { + speakers: Vec, + }, + #[cfg(not(target_os = "windows"))] + ConnectionStateChanged(ConnectionState), + Connected { + participants_with_tracks: Vec<(RemoteParticipant, Vec)>, + }, + #[cfg(not(target_os = "windows"))] + Disconnected { + reason: DisconnectReason, + }, + Reconnecting, + Reconnected, +} + +#[cfg(not(target_os = "windows"))] +#[async_trait] +impl livekit_server::api::Client for TestApiClient { + fn url(&self) -> &str { + &self.url + } + + async fn create_room(&self, name: String) -> Result<()> { + let server = TestServer::get(&self.url)?; + server.create_room(name).await?; + Ok(()) + } + + async fn delete_room(&self, name: String) -> Result<()> { + let server = TestServer::get(&self.url)?; + server.delete_room(name).await?; + Ok(()) + } + + async fn remove_participant(&self, room: String, identity: String) -> Result<()> { + let server = TestServer::get(&self.url)?; + server + .remove_participant(room, ParticipantIdentity(identity)) + .await?; + Ok(()) + } + + async fn update_participant( + &self, + room: String, + identity: String, + permission: livekit_server::proto::ParticipantPermission, + ) -> Result<()> { + let server = TestServer::get(&self.url)?; + server + .update_participant(room, identity, permission) + .await?; + Ok(()) + } + + fn room_token(&self, room: &str, identity: &str) -> Result { + let server = TestServer::get(&self.url)?; + token::create( + &server.api_key, + &server.secret_key, + Some(identity), + token::VideoGrant::to_join(room), + ) + } + + fn guest_token(&self, room: &str, identity: &str) -> Result { + let server = TestServer::get(&self.url)?; + token::create( + &server.api_key, + &server.secret_key, + Some(identity), + token::VideoGrant::for_guest(room), + ) + } +} + +struct RoomState { + url: String, + token: String, + #[cfg(not(target_os = "windows"))] + local_identity: ParticipantIdentity, + #[cfg(not(target_os = "windows"))] + connection_state: ConnectionState, + #[cfg(not(target_os = "windows"))] + paused_audio_tracks: HashSet, + updates_tx: mpsc::Sender, +} + +#[derive(Clone, Debug)] +pub struct Room(Arc>); + +#[derive(Clone, Debug)] +pub(crate) struct WeakRoom(Weak>); + +#[cfg(not(target_os = "windows"))] +impl std::fmt::Debug for RoomState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Room") + .field("url", &self.url) + .field("token", &self.token) + .field("local_identity", &self.local_identity) + .field("connection_state", &self.connection_state) + .field("paused_audio_tracks", &self.paused_audio_tracks) + .finish() + } +} + +#[cfg(target_os = "windows")] +impl std::fmt::Debug for RoomState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Room") + .field("url", &self.url) + .field("token", &self.token) + .finish() + } +} + +#[cfg(not(target_os = "windows"))] +impl Room { + fn downgrade(&self) -> WeakRoom { + WeakRoom(Arc::downgrade(&self.0)) + } + + pub fn connection_state(&self) -> ConnectionState { + self.0.lock().connection_state + } + + pub fn local_participant(&self) -> LocalParticipant { + let identity = self.0.lock().local_identity.clone(); + LocalParticipant { + identity, + room: self.clone(), + } + } + + pub async fn connect( + url: &str, + token: &str, + _options: RoomOptions, + ) -> Result<(Self, mpsc::Receiver)> { + let server = TestServer::get(&url)?; + let (updates_tx, updates_rx) = mpsc::channel(1024); + let this = Self(Arc::new(Mutex::new(RoomState { + local_identity: ParticipantIdentity(String::new()), + url: url.to_string(), + token: token.to_string(), + connection_state: ConnectionState::Disconnected, + paused_audio_tracks: Default::default(), + updates_tx, + }))); + + let identity = server + .join_room(token.to_string(), this.clone()) + .await + .context("room join")?; + { + let mut state = this.0.lock(); + state.local_identity = identity; + state.connection_state = ConnectionState::Connected; + } + + Ok((this, updates_rx)) + } + + pub fn remote_participants(&self) -> HashMap { + self.test_server() + .remote_participants(self.0.lock().token.clone()) + .unwrap() + } + + fn test_server(&self) -> Arc { + TestServer::get(&self.0.lock().url).unwrap() + } + + fn token(&self) -> String { + self.0.lock().token.clone() + } +} + +#[cfg(not(target_os = "windows"))] +impl Drop for RoomState { + fn drop(&mut self) { + if self.connection_state == ConnectionState::Connected { + if let Ok(server) = TestServer::get(&self.url) { + let executor = server.executor.clone(); + let token = self.token.clone(); + executor + .spawn(async move { server.leave_room(token).await.ok() }) + .detach(); + } + } + } +} + +impl WeakRoom { + fn upgrade(&self) -> Option { + self.0.upgrade().map(Room) + } +} diff --git a/crates/livekit_client/src/test/participant.rs b/crates/livekit_client/src/test/participant.rs new file mode 100644 index 0000000000..8d476b1537 --- /dev/null +++ b/crates/livekit_client/src/test/participant.rs @@ -0,0 +1,111 @@ +use super::*; + +#[derive(Clone, Debug)] +pub enum Participant { + Local(LocalParticipant), + Remote(RemoteParticipant), +} + +#[derive(Clone, Debug)] +pub struct LocalParticipant { + #[cfg(not(target_os = "windows"))] + pub(super) identity: ParticipantIdentity, + pub(super) room: Room, +} + +#[derive(Clone, Debug)] +pub struct RemoteParticipant { + #[cfg(not(target_os = "windows"))] + pub(super) identity: ParticipantIdentity, + pub(super) room: WeakRoom, +} + +#[cfg(not(target_os = "windows"))] +impl Participant { + pub fn identity(&self) -> ParticipantIdentity { + match self { + Participant::Local(participant) => participant.identity.clone(), + Participant::Remote(participant) => participant.identity.clone(), + } + } +} + +#[cfg(not(target_os = "windows"))] +impl LocalParticipant { + pub async fn unpublish_track(&self, track: &TrackSid) -> Result<()> { + self.room + .test_server() + .unpublish_track(self.room.token(), track) + .await + } + + pub async fn publish_track( + &self, + track: LocalTrack, + _options: TrackPublishOptions, + ) -> Result { + let this = self.clone(); + let track = track.clone(); + let server = this.room.test_server(); + let sid = match track { + LocalTrack::Video(track) => { + server.publish_video_track(this.room.token(), track).await? + } + LocalTrack::Audio(track) => { + server + .publish_audio_track(this.room.token(), &track) + .await? + } + }; + Ok(LocalTrackPublication { + room: self.room.downgrade(), + sid, + }) + } +} + +#[cfg(not(target_os = "windows"))] +impl RemoteParticipant { + pub fn track_publications(&self) -> HashMap { + if let Some(room) = self.room.upgrade() { + let server = room.test_server(); + let audio = server + .audio_tracks(room.token()) + .unwrap() + .into_iter() + .filter(|track| track.publisher_id() == self.identity) + .map(|track| { + ( + track.sid(), + RemoteTrackPublication { + sid: track.sid(), + room: self.room.clone(), + track: RemoteTrack::Audio(track), + }, + ) + }); + let video = server + .video_tracks(room.token()) + .unwrap() + .into_iter() + .filter(|track| track.publisher_id() == self.identity) + .map(|track| { + ( + track.sid(), + RemoteTrackPublication { + sid: track.sid(), + room: self.room.clone(), + track: RemoteTrack::Video(track), + }, + ) + }); + audio.chain(video).collect() + } else { + HashMap::default() + } + } + + pub fn identity(&self) -> ParticipantIdentity { + self.identity.clone() + } +} diff --git a/crates/livekit_client/src/test/publication.rs b/crates/livekit_client/src/test/publication.rs new file mode 100644 index 0000000000..6a3dfa0a51 --- /dev/null +++ b/crates/livekit_client/src/test/publication.rs @@ -0,0 +1,116 @@ +use super::*; + +#[derive(Clone, Debug)] +pub enum TrackPublication { + Local(LocalTrackPublication), + Remote(RemoteTrackPublication), +} + +#[derive(Clone, Debug)] +pub struct LocalTrackPublication { + #[cfg(not(target_os = "windows"))] + pub(crate) sid: TrackSid, + pub(crate) room: WeakRoom, +} + +#[derive(Clone, Debug)] +pub struct RemoteTrackPublication { + #[cfg(not(target_os = "windows"))] + pub(crate) sid: TrackSid, + pub(crate) room: WeakRoom, + pub(crate) track: RemoteTrack, +} + +#[cfg(not(target_os = "windows"))] +impl TrackPublication { + pub fn sid(&self) -> TrackSid { + match self { + TrackPublication::Local(track) => track.sid(), + TrackPublication::Remote(track) => track.sid(), + } + } + + pub fn is_muted(&self) -> bool { + match self { + TrackPublication::Local(track) => track.is_muted(), + TrackPublication::Remote(track) => track.is_muted(), + } + } +} + +#[cfg(not(target_os = "windows"))] +impl LocalTrackPublication { + pub fn sid(&self) -> TrackSid { + self.sid.clone() + } + + pub fn mute(&self) { + self.set_mute(true) + } + + pub fn unmute(&self) { + self.set_mute(false) + } + + fn set_mute(&self, mute: bool) { + if let Some(room) = self.room.upgrade() { + room.test_server() + .set_track_muted(&room.token(), &self.sid, mute) + .ok(); + } + } + + pub fn is_muted(&self) -> bool { + if let Some(room) = self.room.upgrade() { + room.test_server() + .is_track_muted(&room.token(), &self.sid) + .unwrap_or(false) + } else { + false + } + } +} + +#[cfg(not(target_os = "windows"))] +impl RemoteTrackPublication { + pub fn sid(&self) -> TrackSid { + self.sid.clone() + } + + pub fn track(&self) -> Option { + Some(self.track.clone()) + } + + pub fn kind(&self) -> TrackKind { + self.track.kind() + } + + pub fn is_muted(&self) -> bool { + if let Some(room) = self.room.upgrade() { + room.test_server() + .is_track_muted(&room.token(), &self.sid) + .unwrap_or(false) + } else { + false + } + } + + pub fn is_enabled(&self) -> bool { + if let Some(room) = self.room.upgrade() { + !room.0.lock().paused_audio_tracks.contains(&self.sid) + } else { + false + } + } + + pub fn set_enabled(&self, enabled: bool) { + if let Some(room) = self.room.upgrade() { + let paused_audio_tracks = &mut room.0.lock().paused_audio_tracks; + if enabled { + paused_audio_tracks.remove(&self.sid); + } else { + paused_audio_tracks.insert(self.sid.clone()); + } + } + } +} diff --git a/crates/livekit_client/src/test/track.rs b/crates/livekit_client/src/test/track.rs new file mode 100644 index 0000000000..302177a10a --- /dev/null +++ b/crates/livekit_client/src/test/track.rs @@ -0,0 +1,201 @@ +use super::*; +#[cfg(not(windows))] +use webrtc::{audio_source::RtcAudioSource, video_source::RtcVideoSource}; + +#[cfg(not(windows))] +pub use livekit::track::{TrackKind, TrackSource}; + +#[derive(Clone, Debug)] +pub enum LocalTrack { + Audio(LocalAudioTrack), + Video(LocalVideoTrack), +} + +#[derive(Clone, Debug)] +pub enum RemoteTrack { + Audio(RemoteAudioTrack), + Video(RemoteVideoTrack), +} + +#[derive(Clone, Debug)] +pub struct LocalVideoTrack {} + +#[derive(Clone, Debug)] +pub struct LocalAudioTrack {} + +#[derive(Clone, Debug)] +pub struct RemoteVideoTrack { + #[cfg(not(target_os = "windows"))] + pub(super) server_track: Arc, + pub(super) _room: WeakRoom, +} + +#[derive(Clone, Debug)] +pub struct RemoteAudioTrack { + #[cfg(not(target_os = "windows"))] + pub(super) server_track: Arc, + pub(super) room: WeakRoom, +} + +pub enum RtcTrack { + Audio(RtcAudioTrack), + Video(RtcVideoTrack), +} + +pub struct RtcAudioTrack { + #[cfg(not(target_os = "windows"))] + pub(super) server_track: Arc, + pub(super) room: WeakRoom, +} + +pub struct RtcVideoTrack { + #[cfg(not(target_os = "windows"))] + pub(super) _server_track: Arc, +} + +#[cfg(not(target_os = "windows"))] +impl RemoteTrack { + pub fn sid(&self) -> TrackSid { + match self { + RemoteTrack::Audio(track) => track.sid(), + RemoteTrack::Video(track) => track.sid(), + } + } + + pub fn kind(&self) -> TrackKind { + match self { + RemoteTrack::Audio(_) => TrackKind::Audio, + RemoteTrack::Video(_) => TrackKind::Video, + } + } + + pub fn publisher_id(&self) -> ParticipantIdentity { + match self { + RemoteTrack::Audio(track) => track.publisher_id(), + RemoteTrack::Video(track) => track.publisher_id(), + } + } + + pub fn rtc_track(&self) -> RtcTrack { + match self { + RemoteTrack::Audio(track) => RtcTrack::Audio(track.rtc_track()), + RemoteTrack::Video(track) => RtcTrack::Video(track.rtc_track()), + } + } +} + +#[cfg(not(windows))] +impl LocalVideoTrack { + pub fn create_video_track(_name: &str, _source: RtcVideoSource) -> Self { + Self {} + } +} + +#[cfg(not(windows))] +impl LocalAudioTrack { + pub fn create_audio_track(_name: &str, _source: RtcAudioSource) -> Self { + Self {} + } +} + +#[cfg(not(target_os = "windows"))] +impl RemoteAudioTrack { + pub fn sid(&self) -> TrackSid { + self.server_track.sid.clone() + } + + pub fn publisher_id(&self) -> ParticipantIdentity { + self.server_track.publisher_id.clone() + } + + pub fn start(&self) { + if let Some(room) = self.room.upgrade() { + room.0 + .lock() + .paused_audio_tracks + .remove(&self.server_track.sid); + } + } + + pub fn stop(&self) { + if let Some(room) = self.room.upgrade() { + room.0 + .lock() + .paused_audio_tracks + .insert(self.server_track.sid.clone()); + } + } + + pub fn rtc_track(&self) -> RtcAudioTrack { + RtcAudioTrack { + server_track: self.server_track.clone(), + room: self.room.clone(), + } + } +} + +#[cfg(not(target_os = "windows"))] +impl RemoteVideoTrack { + pub fn sid(&self) -> TrackSid { + self.server_track.sid.clone() + } + + pub fn publisher_id(&self) -> ParticipantIdentity { + self.server_track.publisher_id.clone() + } + + pub fn rtc_track(&self) -> RtcVideoTrack { + RtcVideoTrack { + _server_track: self.server_track.clone(), + } + } +} + +#[cfg(not(target_os = "windows"))] +impl RtcTrack { + pub fn enabled(&self) -> bool { + match self { + RtcTrack::Audio(track) => track.enabled(), + RtcTrack::Video(track) => track.enabled(), + } + } + + pub fn set_enabled(&self, enabled: bool) { + match self { + RtcTrack::Audio(track) => track.set_enabled(enabled), + RtcTrack::Video(_) => {} + } + } +} + +#[cfg(not(target_os = "windows"))] +impl RtcAudioTrack { + pub fn set_enabled(&self, enabled: bool) { + if let Some(room) = self.room.upgrade() { + let paused_audio_tracks = &mut room.0.lock().paused_audio_tracks; + if enabled { + paused_audio_tracks.remove(&self.server_track.sid); + } else { + paused_audio_tracks.insert(self.server_track.sid.clone()); + } + } + } + + pub fn enabled(&self) -> bool { + if let Some(room) = self.room.upgrade() { + !room + .0 + .lock() + .paused_audio_tracks + .contains(&self.server_track.sid) + } else { + false + } + } +} + +impl RtcVideoTrack { + pub fn enabled(&self) -> bool { + true + } +} diff --git a/crates/livekit_client/src/test/webrtc.rs b/crates/livekit_client/src/test/webrtc.rs new file mode 100644 index 0000000000..6ac06e0484 --- /dev/null +++ b/crates/livekit_client/src/test/webrtc.rs @@ -0,0 +1,136 @@ +use super::track::{RtcAudioTrack, RtcVideoTrack}; +use futures::Stream; +use livekit::webrtc as real; +use std::{ + pin::Pin, + task::{Context, Poll}, +}; + +pub mod video_stream { + use super::*; + + pub mod native { + use super::*; + use real::video_frame::BoxVideoFrame; + + pub struct NativeVideoStream { + pub track: RtcVideoTrack, + } + + impl NativeVideoStream { + pub fn new(track: RtcVideoTrack) -> Self { + Self { track } + } + } + + impl Stream for NativeVideoStream { + type Item = BoxVideoFrame; + + fn poll_next(self: Pin<&mut Self>, _cx: &mut Context) -> Poll> { + Poll::Pending + } + } + } +} + +pub mod audio_stream { + use super::*; + + pub mod native { + use super::*; + use real::audio_frame::AudioFrame; + + pub struct NativeAudioStream { + pub track: RtcAudioTrack, + } + + impl NativeAudioStream { + pub fn new(track: RtcAudioTrack, _sample_rate: i32, _num_channels: i32) -> Self { + Self { track } + } + } + + impl Stream for NativeAudioStream { + type Item = AudioFrame<'static>; + + fn poll_next(self: Pin<&mut Self>, _cx: &mut Context) -> Poll> { + Poll::Pending + } + } + } +} + +pub mod audio_source { + use super::*; + + pub use real::audio_source::AudioSourceOptions; + + pub mod native { + use std::sync::Arc; + + use super::*; + use real::{audio_frame::AudioFrame, RtcError}; + + #[derive(Clone)] + pub struct NativeAudioSource { + pub options: Arc, + pub sample_rate: u32, + pub num_channels: u32, + } + + impl NativeAudioSource { + pub fn new( + options: AudioSourceOptions, + sample_rate: u32, + num_channels: u32, + _queue_size_ms: u32, + ) -> Self { + Self { + options: Arc::new(options), + sample_rate, + num_channels, + } + } + + pub async fn capture_frame(&self, _frame: &AudioFrame<'_>) -> Result<(), RtcError> { + Ok(()) + } + } + } + + pub enum RtcAudioSource { + Native(native::NativeAudioSource), + } +} + +pub use livekit::webrtc::audio_frame; +pub use livekit::webrtc::video_frame; + +pub mod video_source { + use super::*; + pub use real::video_source::VideoResolution; + + pub struct RTCVideoSource; + + pub mod native { + use super::*; + use real::video_frame::{VideoBuffer, VideoFrame}; + + #[derive(Clone)] + pub struct NativeVideoSource { + pub resolution: VideoResolution, + } + + impl NativeVideoSource { + pub fn new(resolution: super::VideoResolution) -> Self { + Self { resolution } + } + + pub fn capture_frame>(&self, _frame: &VideoFrame) {} + } + } + + pub enum RtcVideoSource { + Native(native::NativeVideoSource), + } +} diff --git a/crates/livekit_client_macos/.cargo/config.toml b/crates/livekit_client_macos/.cargo/config.toml new file mode 100644 index 0000000000..77f7c9dd6c --- /dev/null +++ b/crates/livekit_client_macos/.cargo/config.toml @@ -0,0 +1,2 @@ +[livekit_client_test] +rustflags = ["-C", "link-args=-ObjC"] diff --git a/crates/live_kit_client/Cargo.toml b/crates/livekit_client_macos/Cargo.toml similarity index 87% rename from crates/live_kit_client/Cargo.toml rename to crates/livekit_client_macos/Cargo.toml index e23c63453e..6a5a8d0ea2 100644 --- a/crates/live_kit_client/Cargo.toml +++ b/crates/livekit_client_macos/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "live_kit_client" +name = "livekit_client_macos" version = "0.1.0" edition = "2021" description = "Bindings to LiveKit Swift client SDK" @@ -10,7 +10,7 @@ license = "GPL-3.0-or-later" workspace = true [lib] -path = "src/live_kit_client.rs" +path = "src/livekit_client.rs" doctest = false [[example]] @@ -22,7 +22,7 @@ test-support = [ "async-trait", "collections/test-support", "gpui/test-support", - "live_kit_server", + "livekit_server", "nanoid", ] @@ -33,7 +33,7 @@ async-trait = { workspace = true, optional = true } collections = { workspace = true, optional = true } futures.workspace = true gpui = { workspace = true, optional = true } -live_kit_server = { workspace = true, optional = true } +livekit_server = { workspace = true, optional = true } log.workspace = true media.workspace = true nanoid = { workspace = true, optional = true} @@ -47,14 +47,14 @@ core-foundation.workspace = true async-trait = { workspace = true } collections = { workspace = true } gpui = { workspace = true } -live_kit_server.workspace = true +livekit_server.workspace = true nanoid.workspace = true [dev-dependencies] async-trait.workspace = true collections = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] } -live_kit_server.workspace = true +livekit_server.workspace = true nanoid.workspace = true sha2.workspace = true simplelog.workspace = true diff --git a/crates/livekit_client_macos/LICENSE-GPL b/crates/livekit_client_macos/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/livekit_client_macos/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/live_kit_client/LiveKitBridge/Package.resolved b/crates/livekit_client_macos/LiveKitBridge/Package.resolved similarity index 100% rename from crates/live_kit_client/LiveKitBridge/Package.resolved rename to crates/livekit_client_macos/LiveKitBridge/Package.resolved diff --git a/crates/live_kit_client/LiveKitBridge/Package.swift b/crates/livekit_client_macos/LiveKitBridge/Package.swift similarity index 100% rename from crates/live_kit_client/LiveKitBridge/Package.swift rename to crates/livekit_client_macos/LiveKitBridge/Package.swift diff --git a/crates/live_kit_client/LiveKitBridge/README.md b/crates/livekit_client_macos/LiveKitBridge/README.md similarity index 100% rename from crates/live_kit_client/LiveKitBridge/README.md rename to crates/livekit_client_macos/LiveKitBridge/README.md diff --git a/crates/live_kit_client/LiveKitBridge/Sources/LiveKitBridge/LiveKitBridge.swift b/crates/livekit_client_macos/LiveKitBridge/Sources/LiveKitBridge/LiveKitBridge.swift similarity index 100% rename from crates/live_kit_client/LiveKitBridge/Sources/LiveKitBridge/LiveKitBridge.swift rename to crates/livekit_client_macos/LiveKitBridge/Sources/LiveKitBridge/LiveKitBridge.swift diff --git a/crates/live_kit_client/build.rs b/crates/livekit_client_macos/build.rs similarity index 100% rename from crates/live_kit_client/build.rs rename to crates/livekit_client_macos/build.rs diff --git a/crates/live_kit_client/examples/test_app.rs b/crates/livekit_client_macos/examples/test_app.rs similarity index 97% rename from crates/live_kit_client/examples/test_app.rs rename to crates/livekit_client_macos/examples/test_app.rs index de8be97e86..c6ae2cc478 100644 --- a/crates/live_kit_client/examples/test_app.rs +++ b/crates/livekit_client_macos/examples/test_app.rs @@ -2,12 +2,12 @@ use std::time::Duration; use futures::StreamExt; use gpui::{actions, KeyBinding, Menu, MenuItem}; -use live_kit_client::{LocalAudioTrack, LocalVideoTrack, Room, RoomUpdate}; -use live_kit_server::token::{self, VideoGrant}; +use livekit_client_macos::{LocalAudioTrack, LocalVideoTrack, Room, RoomUpdate}; +use livekit_server::token::{self, VideoGrant}; use log::LevelFilter; use simplelog::SimpleLogger; -actions!(live_kit_client, [Quit]); +actions!(livekit_client_macos, [Quit]); fn main() { SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger"); diff --git a/crates/live_kit_client/src/live_kit_client.rs b/crates/livekit_client_macos/src/livekit_client.rs similarity index 100% rename from crates/live_kit_client/src/live_kit_client.rs rename to crates/livekit_client_macos/src/livekit_client.rs diff --git a/crates/live_kit_client/src/prod.rs b/crates/livekit_client_macos/src/prod.rs similarity index 100% rename from crates/live_kit_client/src/prod.rs rename to crates/livekit_client_macos/src/prod.rs diff --git a/crates/live_kit_client/src/test.rs b/crates/livekit_client_macos/src/test.rs similarity index 97% rename from crates/live_kit_client/src/test.rs rename to crates/livekit_client_macos/src/test.rs index 2c26c88f72..6db24174ff 100644 --- a/crates/live_kit_client/src/test.rs +++ b/crates/livekit_client_macos/src/test.rs @@ -4,7 +4,7 @@ use async_trait::async_trait; use collections::{btree_map::Entry as BTreeEntry, hash_map::Entry, BTreeMap, HashMap, HashSet}; use futures::Stream; use gpui::{BackgroundExecutor, SurfaceSource}; -use live_kit_server::{proto, token}; +use livekit_server::{proto, token}; use parking_lot::Mutex; use postage::watch; @@ -102,7 +102,7 @@ impl TestServer { #[cfg(any(test, feature = "test-support"))] self.executor.simulate_random_delay().await; - let claims = live_kit_server::token::validate(&token, &self.secret_key)?; + let claims = livekit_server::token::validate(&token, &self.secret_key)?; let identity = claims.sub.unwrap().to_string(); let room_name = claims.video.room.unwrap(); let mut server_rooms = self.rooms.lock(); @@ -150,7 +150,7 @@ impl TestServer { // todo(linux): Remove this once the cross-platform LiveKit implementation is merged #[cfg(any(test, feature = "test-support"))] self.executor.simulate_random_delay().await; - let claims = live_kit_server::token::validate(&token, &self.secret_key)?; + let claims = livekit_server::token::validate(&token, &self.secret_key)?; let identity = claims.sub.unwrap().to_string(); let room_name = claims.video.room.unwrap(); let mut server_rooms = self.rooms.lock(); @@ -224,7 +224,7 @@ impl TestServer { // todo(linux): Remove this once the cross-platform LiveKit implementation is merged #[cfg(any(test, feature = "test-support"))] self.executor.simulate_random_delay().await; - let claims = live_kit_server::token::validate(&token, &self.secret_key)?; + let claims = livekit_server::token::validate(&token, &self.secret_key)?; let identity = claims.sub.unwrap().to_string(); let room_name = claims.video.room.unwrap(); @@ -280,7 +280,7 @@ impl TestServer { #[cfg(any(test, feature = "test-support"))] self.executor.simulate_random_delay().await; - let claims = live_kit_server::token::validate(&token, &self.secret_key)?; + let claims = livekit_server::token::validate(&token, &self.secret_key)?; let identity = claims.sub.unwrap().to_string(); let room_name = claims.video.room.unwrap(); @@ -332,7 +332,7 @@ impl TestServer { } fn set_track_muted(&self, token: &str, track_sid: &str, muted: bool) -> Result<()> { - let claims = live_kit_server::token::validate(token, &self.secret_key)?; + let claims = livekit_server::token::validate(token, &self.secret_key)?; let room_name = claims.video.room.unwrap(); let identity = claims.sub.unwrap(); let mut server_rooms = self.rooms.lock(); @@ -363,7 +363,7 @@ impl TestServer { } fn is_track_muted(&self, token: &str, track_sid: &str) -> Option { - let claims = live_kit_server::token::validate(token, &self.secret_key).ok()?; + let claims = livekit_server::token::validate(token, &self.secret_key).ok()?; let room_name = claims.video.room.unwrap(); let mut server_rooms = self.rooms.lock(); @@ -378,7 +378,7 @@ impl TestServer { } fn video_tracks(&self, token: String) -> Result>> { - let claims = live_kit_server::token::validate(&token, &self.secret_key)?; + let claims = livekit_server::token::validate(&token, &self.secret_key)?; let room_name = claims.video.room.unwrap(); let identity = claims.sub.unwrap(); @@ -401,7 +401,7 @@ impl TestServer { } fn audio_tracks(&self, token: String) -> Result>> { - let claims = live_kit_server::token::validate(&token, &self.secret_key)?; + let claims = livekit_server::token::validate(&token, &self.secret_key)?; let room_name = claims.video.room.unwrap(); let identity = claims.sub.unwrap(); @@ -455,7 +455,7 @@ pub struct TestApiClient { } #[async_trait] -impl live_kit_server::api::Client for TestApiClient { +impl livekit_server::api::Client for TestApiClient { fn url(&self) -> &str { &self.url } @@ -482,7 +482,7 @@ impl live_kit_server::api::Client for TestApiClient { &self, room: String, identity: String, - permission: live_kit_server::proto::ParticipantPermission, + permission: livekit_server::proto::ParticipantPermission, ) -> Result<()> { let server = TestServer::get(&self.url)?; server diff --git a/crates/live_kit_server/Cargo.toml b/crates/livekit_server/Cargo.toml similarity index 90% rename from crates/live_kit_server/Cargo.toml rename to crates/livekit_server/Cargo.toml index 4b4b5e13da..c76cb1580c 100644 --- a/crates/live_kit_server/Cargo.toml +++ b/crates/livekit_server/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "live_kit_server" +name = "livekit_server" version = "0.1.0" edition = "2021" description = "SDK for the LiveKit server API" @@ -10,7 +10,7 @@ license = "AGPL-3.0-or-later" workspace = true [lib] -path = "src/live_kit_server.rs" +path = "src/livekit_server.rs" doctest = false [dependencies] diff --git a/crates/live_kit_server/LICENSE-AGPL b/crates/livekit_server/LICENSE-AGPL similarity index 100% rename from crates/live_kit_server/LICENSE-AGPL rename to crates/livekit_server/LICENSE-AGPL diff --git a/crates/live_kit_server/build.rs b/crates/livekit_server/build.rs similarity index 100% rename from crates/live_kit_server/build.rs rename to crates/livekit_server/build.rs diff --git a/crates/live_kit_server/src/api.rs b/crates/livekit_server/src/api.rs similarity index 100% rename from crates/live_kit_server/src/api.rs rename to crates/livekit_server/src/api.rs diff --git a/crates/live_kit_server/src/live_kit_server.rs b/crates/livekit_server/src/livekit_server.rs similarity index 100% rename from crates/live_kit_server/src/live_kit_server.rs rename to crates/livekit_server/src/livekit_server.rs diff --git a/crates/live_kit_server/src/proto.rs b/crates/livekit_server/src/proto.rs similarity index 100% rename from crates/live_kit_server/src/proto.rs rename to crates/livekit_server/src/proto.rs diff --git a/crates/live_kit_server/src/token.rs b/crates/livekit_server/src/token.rs similarity index 100% rename from crates/live_kit_server/src/token.rs rename to crates/livekit_server/src/token.rs diff --git a/crates/live_kit_server/vendored/protocol/README.md b/crates/livekit_server/vendored/protocol/README.md similarity index 100% rename from crates/live_kit_server/vendored/protocol/README.md rename to crates/livekit_server/vendored/protocol/README.md diff --git a/crates/live_kit_server/vendored/protocol/livekit_analytics.proto b/crates/livekit_server/vendored/protocol/livekit_analytics.proto similarity index 100% rename from crates/live_kit_server/vendored/protocol/livekit_analytics.proto rename to crates/livekit_server/vendored/protocol/livekit_analytics.proto diff --git a/crates/live_kit_server/vendored/protocol/livekit_egress.proto b/crates/livekit_server/vendored/protocol/livekit_egress.proto similarity index 100% rename from crates/live_kit_server/vendored/protocol/livekit_egress.proto rename to crates/livekit_server/vendored/protocol/livekit_egress.proto diff --git a/crates/live_kit_server/vendored/protocol/livekit_ingress.proto b/crates/livekit_server/vendored/protocol/livekit_ingress.proto similarity index 100% rename from crates/live_kit_server/vendored/protocol/livekit_ingress.proto rename to crates/livekit_server/vendored/protocol/livekit_ingress.proto diff --git a/crates/live_kit_server/vendored/protocol/livekit_internal.proto b/crates/livekit_server/vendored/protocol/livekit_internal.proto similarity index 100% rename from crates/live_kit_server/vendored/protocol/livekit_internal.proto rename to crates/livekit_server/vendored/protocol/livekit_internal.proto diff --git a/crates/live_kit_server/vendored/protocol/livekit_models.proto b/crates/livekit_server/vendored/protocol/livekit_models.proto similarity index 100% rename from crates/live_kit_server/vendored/protocol/livekit_models.proto rename to crates/livekit_server/vendored/protocol/livekit_models.proto diff --git a/crates/live_kit_server/vendored/protocol/livekit_room.proto b/crates/livekit_server/vendored/protocol/livekit_room.proto similarity index 100% rename from crates/live_kit_server/vendored/protocol/livekit_room.proto rename to crates/livekit_server/vendored/protocol/livekit_room.proto diff --git a/crates/live_kit_server/vendored/protocol/livekit_rpc_internal.proto b/crates/livekit_server/vendored/protocol/livekit_rpc_internal.proto similarity index 100% rename from crates/live_kit_server/vendored/protocol/livekit_rpc_internal.proto rename to crates/livekit_server/vendored/protocol/livekit_rpc_internal.proto diff --git a/crates/live_kit_server/vendored/protocol/livekit_rtc.proto b/crates/livekit_server/vendored/protocol/livekit_rtc.proto similarity index 100% rename from crates/live_kit_server/vendored/protocol/livekit_rtc.proto rename to crates/livekit_server/vendored/protocol/livekit_rtc.proto diff --git a/crates/live_kit_server/vendored/protocol/livekit_webhook.proto b/crates/livekit_server/vendored/protocol/livekit_webhook.proto similarity index 100% rename from crates/live_kit_server/vendored/protocol/livekit_webhook.proto rename to crates/livekit_server/vendored/protocol/livekit_webhook.proto diff --git a/crates/media/Cargo.toml b/crates/media/Cargo.toml index 92940d1c52..70478eeb75 100644 --- a/crates/media/Cargo.toml +++ b/crates/media/Cargo.toml @@ -17,6 +17,7 @@ anyhow.workspace = true [target.'cfg(target_os = "macos")'.dependencies] core-foundation.workspace = true +ctor.workspace = true foreign-types = "0.5" metal = "0.29" objc = "0.2" diff --git a/crates/media/src/media.rs b/crates/media/src/media.rs index 8757249c31..3f55475589 100644 --- a/crates/media/src/media.rs +++ b/crates/media/src/media.rs @@ -253,11 +253,14 @@ pub mod core_media { } } - pub fn image_buffer(&self) -> CVImageBuffer { + pub fn image_buffer(&self) -> Option { unsafe { - CVImageBuffer::wrap_under_get_rule(CMSampleBufferGetImageBuffer( - self.as_concrete_TypeRef(), - )) + let ptr = CMSampleBufferGetImageBuffer(self.as_concrete_TypeRef()); + if ptr.is_null() { + None + } else { + Some(CVImageBuffer::wrap_under_get_rule(ptr)) + } } } diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index f0d8f27131..e177dc2763 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -432,7 +432,7 @@ message Room { repeated Participant participants = 2; repeated PendingParticipant pending_participants = 3; repeated Follower followers = 4; - string live_kit_room = 5; + string livekit_room = 5; } message Participant { diff --git a/crates/title_bar/src/collab.rs b/crates/title_bar/src/collab.rs index 649dfb34f7..7d977bb458 100644 --- a/crates/title_bar/src/collab.rs +++ b/crates/title_bar/src/collab.rs @@ -294,9 +294,9 @@ impl TitleBar { let is_muted = room.is_muted(); let is_deafened = room.is_deafened().unwrap_or(false); let is_screen_sharing = room.is_screen_sharing(); - let can_use_microphone = room.can_use_microphone(); + let can_use_microphone = room.can_use_microphone(cx); let can_share_projects = room.can_share_projects(); - let platform_supported = match self.platform_style { + let screen_sharing_supported = match self.platform_style { PlatformStyle::Mac => true, PlatformStyle::Linux | PlatformStyle::Windows => false, }; @@ -363,9 +363,7 @@ impl TitleBar { ) .tooltip(move |cx| { Tooltip::text( - if !platform_supported { - "Cannot share microphone" - } else if is_muted { + if is_muted { "Unmute microphone" } else { "Mute microphone" @@ -375,56 +373,45 @@ impl TitleBar { }) .style(ButtonStyle::Subtle) .icon_size(IconSize::Small) - .selected(platform_supported && is_muted) - .disabled(!platform_supported) + .selected(is_muted) .selected_style(ButtonStyle::Tinted(TintColor::Negative)) .on_click(move |_, cx| { toggle_mute(&Default::default(), cx); }) .into_any_element(), ); + + children.push( + IconButton::new( + "mute-sound", + if is_deafened { + ui::IconName::AudioOff + } else { + ui::IconName::AudioOn + }, + ) + .style(ButtonStyle::Subtle) + .selected_style(ButtonStyle::Tinted(TintColor::Negative)) + .icon_size(IconSize::Small) + .selected(is_deafened) + .tooltip(move |cx| { + Tooltip::with_meta("Deafen Audio", None, "Mic will be muted", cx) + }) + .on_click(move |_, cx| toggle_deafen(&Default::default(), cx)) + .into_any_element(), + ); } - children.push( - IconButton::new( - "mute-sound", - if is_deafened { - ui::IconName::AudioOff - } else { - ui::IconName::AudioOn - }, - ) - .style(ButtonStyle::Subtle) - .selected_style(ButtonStyle::Tinted(TintColor::Negative)) - .icon_size(IconSize::Small) - .selected(is_deafened) - .disabled(!platform_supported) - .tooltip(move |cx| { - if !platform_supported { - Tooltip::text("Cannot share microphone", cx) - } else if can_use_microphone { - Tooltip::with_meta("Deafen Audio", None, "Mic will be muted", cx) - } else { - Tooltip::text("Deafen Audio", cx) - } - }) - .on_click(move |_, cx| toggle_deafen(&Default::default(), cx)) - .into_any_element(), - ); - - if can_share_projects { + if screen_sharing_supported { children.push( IconButton::new("screen-share", ui::IconName::Screen) .style(ButtonStyle::Subtle) .icon_size(IconSize::Small) .selected(is_screen_sharing) - .disabled(!platform_supported) .selected_style(ButtonStyle::Tinted(TintColor::Accent)) .tooltip(move |cx| { Tooltip::text( - if !platform_supported { - "Cannot share screen" - } else if is_screen_sharing { + if is_screen_sharing { "Stop Sharing Screen" } else { "Share Screen" diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 3b17ed8dab..be2dfb06bd 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -24,6 +24,8 @@ test-support = [ "gpui/test-support", "fs/test-support", ] +livekit-macos = ["call/livekit-macos"] +livekit-cross-platform = ["call/livekit-cross-platform"] [dependencies] anyhow.workspace = true diff --git a/crates/workspace/src/shared_screen.rs b/crates/workspace/src/shared_screen.rs index 59df859488..f7a1ccf760 100644 --- a/crates/workspace/src/shared_screen.rs +++ b/crates/workspace/src/shared_screen.rs @@ -1,126 +1,282 @@ -use crate::{ - item::{Item, ItemEvent}, - ItemNavHistory, WorkspaceId, -}; -use anyhow::Result; -use call::participant::{Frame, RemoteVideoTrack}; -use client::{proto::PeerId, User}; -use futures::StreamExt; -use gpui::{ - div, surface, AppContext, EventEmitter, FocusHandle, FocusableView, InteractiveElement, - ParentElement, Render, SharedString, Styled, Task, View, ViewContext, VisualContext, - WindowContext, -}; -use std::sync::{Arc, Weak}; -use ui::{prelude::*, Icon, IconName}; +#[cfg(any( + all( + target_os = "macos", + feature = "livekit-cross-platform", + not(feature = "livekit-macos"), + ), + all(not(target_os = "macos"), feature = "livekit-cross-platform"), +))] +mod cross_platform { + use crate::{ + item::{Item, ItemEvent}, + ItemNavHistory, WorkspaceId, + }; + use call::{RemoteVideoTrack, RemoteVideoTrackView}; + use client::{proto::PeerId, User}; + use gpui::{ + div, AppContext, EventEmitter, FocusHandle, FocusableView, InteractiveElement, + ParentElement, Render, SharedString, Styled, View, ViewContext, VisualContext, + WindowContext, + }; + use std::sync::Arc; + use ui::{prelude::*, Icon, IconName}; -pub enum Event { - Close, -} + pub enum Event { + Close, + } -pub struct SharedScreen { - track: Weak, - frame: Option, - pub peer_id: PeerId, - user: Arc, - nav_history: Option, - _maintain_frame: Task>, - focus: FocusHandle, -} - -impl SharedScreen { - pub fn new( - track: &Arc, - peer_id: PeerId, + pub struct SharedScreen { + pub peer_id: PeerId, user: Arc, - cx: &mut ViewContext, - ) -> Self { - cx.focus_handle(); - let mut frames = track.frames(); - Self { - track: Arc::downgrade(track), - frame: None, - peer_id, - user, - nav_history: Default::default(), - _maintain_frame: cx.spawn(|this, mut cx| async move { - while let Some(frame) = frames.next().await { - this.update(&mut cx, |this, cx| { - this.frame = Some(frame); - cx.notify(); - })?; - } - this.update(&mut cx, |_, cx| cx.emit(Event::Close))?; - Ok(()) - }), - focus: cx.focus_handle(), + nav_history: Option, + view: View, + focus: FocusHandle, + } + + impl SharedScreen { + pub fn new( + track: RemoteVideoTrack, + peer_id: PeerId, + user: Arc, + cx: &mut ViewContext, + ) -> Self { + let view = cx.new_view(|cx| RemoteVideoTrackView::new(track.clone(), cx)); + cx.subscribe(&view, |_, _, ev, cx| match ev { + call::RemoteVideoTrackViewEvent::Close => cx.emit(Event::Close), + }) + .detach(); + Self { + view, + peer_id, + user, + nav_history: Default::default(), + focus: cx.focus_handle(), + } + } + } + + impl EventEmitter for SharedScreen {} + + impl FocusableView for SharedScreen { + fn focus_handle(&self, _: &AppContext) -> FocusHandle { + self.focus.clone() + } + } + impl Render for SharedScreen { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + div() + .bg(cx.theme().colors().editor_background) + .track_focus(&self.focus) + .key_context("SharedScreen") + .size_full() + .child(self.view.clone()) + } + } + + impl Item for SharedScreen { + type Event = Event; + + fn tab_tooltip_text(&self, _: &AppContext) -> Option { + Some(format!("{}'s screen", self.user.github_login).into()) + } + + fn deactivated(&mut self, cx: &mut ViewContext) { + if let Some(nav_history) = self.nav_history.as_mut() { + nav_history.push::<()>(None, cx); + } + } + + fn tab_icon(&self, _cx: &WindowContext) -> Option { + Some(Icon::new(IconName::Screen)) + } + + fn tab_content_text(&self, _cx: &WindowContext) -> Option { + Some(format!("{}'s screen", self.user.github_login).into()) + } + + fn telemetry_event_text(&self) -> Option<&'static str> { + None + } + + fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext) { + self.nav_history = Some(history); + } + + fn clone_on_split( + &self, + _workspace_id: Option, + cx: &mut ViewContext, + ) -> Option> { + Some(cx.new_view(|cx| Self { + view: self.view.update(cx, |view, cx| view.clone(cx)), + peer_id: self.peer_id, + user: self.user.clone(), + nav_history: Default::default(), + focus: cx.focus_handle(), + })) + } + + fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) { + match event { + Event::Close => f(ItemEvent::CloseItem), + } } } } -impl EventEmitter for SharedScreen {} +#[cfg(any( + all( + target_os = "macos", + feature = "livekit-cross-platform", + not(feature = "livekit-macos"), + ), + all(not(target_os = "macos"), feature = "livekit-cross-platform"), +))] +pub use cross_platform::*; -impl FocusableView for SharedScreen { - fn focus_handle(&self, _: &AppContext) -> FocusHandle { - self.focus.clone() - } -} -impl Render for SharedScreen { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - div() - .bg(cx.theme().colors().editor_background) - .track_focus(&self.focus) - .key_context("SharedScreen") - .size_full() - .children( - self.frame - .as_ref() - .map(|frame| surface(frame.image()).size_full()), - ) - } -} +#[cfg(any( + all(target_os = "macos", feature = "livekit-macos"), + all( + not(target_os = "macos"), + feature = "livekit-macos", + not(feature = "livekit-cross-platform") + ) +))] +mod macos { + use crate::{ + item::{Item, ItemEvent}, + ItemNavHistory, WorkspaceId, + }; + use anyhow::Result; + use call::participant::{Frame, RemoteVideoTrack}; + use client::{proto::PeerId, User}; + use futures::StreamExt; + use gpui::{ + div, surface, AppContext, EventEmitter, FocusHandle, FocusableView, InteractiveElement, + ParentElement, Render, SharedString, Styled, Task, View, ViewContext, VisualContext, + WindowContext, + }; + use std::sync::{Arc, Weak}; + use ui::{prelude::*, Icon, IconName}; -impl Item for SharedScreen { - type Event = Event; - - fn tab_tooltip_text(&self, _: &AppContext) -> Option { - Some(format!("{}'s screen", self.user.github_login).into()) + pub enum Event { + Close, } - fn deactivated(&mut self, cx: &mut ViewContext) { - if let Some(nav_history) = self.nav_history.as_mut() { - nav_history.push::<()>(None, cx); + pub struct SharedScreen { + track: Weak, + frame: Option, + pub peer_id: PeerId, + user: Arc, + nav_history: Option, + _maintain_frame: Task>, + focus: FocusHandle, + } + + impl SharedScreen { + pub fn new( + track: Arc, + peer_id: PeerId, + user: Arc, + cx: &mut ViewContext, + ) -> Self { + cx.focus_handle(); + let mut frames = track.frames(); + Self { + track: Arc::downgrade(&track), + frame: None, + peer_id, + user, + nav_history: Default::default(), + _maintain_frame: cx.spawn(|this, mut cx| async move { + while let Some(frame) = frames.next().await { + this.update(&mut cx, |this, cx| { + this.frame = Some(frame); + cx.notify(); + })?; + } + this.update(&mut cx, |_, cx| cx.emit(Event::Close))?; + Ok(()) + }), + focus: cx.focus_handle(), + } } } - fn tab_icon(&self, _cx: &WindowContext) -> Option { - Some(Icon::new(IconName::Screen)) + impl EventEmitter for SharedScreen {} + + impl FocusableView for SharedScreen { + fn focus_handle(&self, _: &AppContext) -> FocusHandle { + self.focus.clone() + } + } + impl Render for SharedScreen { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + div() + .bg(cx.theme().colors().editor_background) + .track_focus(&self.focus) + .key_context("SharedScreen") + .size_full() + .children( + self.frame + .as_ref() + .map(|frame| surface(frame.image()).size_full()), + ) + } } - fn tab_content_text(&self, _cx: &WindowContext) -> Option { - Some(format!("{}'s screen", self.user.github_login).into()) - } + impl Item for SharedScreen { + type Event = Event; - fn telemetry_event_text(&self) -> Option<&'static str> { - None - } + fn tab_tooltip_text(&self, _: &AppContext) -> Option { + Some(format!("{}'s screen", self.user.github_login).into()) + } - fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext) { - self.nav_history = Some(history); - } + fn deactivated(&mut self, cx: &mut ViewContext) { + if let Some(nav_history) = self.nav_history.as_mut() { + nav_history.push::<()>(None, cx); + } + } - fn clone_on_split( - &self, - _workspace_id: Option, - cx: &mut ViewContext, - ) -> Option> { - let track = self.track.upgrade()?; - Some(cx.new_view(|cx| Self::new(&track, self.peer_id, self.user.clone(), cx))) - } + fn tab_icon(&self, _cx: &WindowContext) -> Option { + Some(Icon::new(IconName::Screen)) + } - fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) { - match event { - Event::Close => f(ItemEvent::CloseItem), + fn tab_content_text(&self, _cx: &WindowContext) -> Option { + Some(format!("{}'s screen", self.user.github_login).into()) + } + + fn telemetry_event_text(&self) -> Option<&'static str> { + None + } + + fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext) { + self.nav_history = Some(history); + } + + fn clone_on_split( + &self, + _workspace_id: Option, + cx: &mut ViewContext, + ) -> Option> { + let track = self.track.upgrade()?; + Some(cx.new_view(|cx| Self::new(track, self.peer_id, self.user.clone(), cx))) + } + + fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) { + match event { + Event::Close => f(ItemEvent::CloseItem), + } } } } + +#[cfg(any( + all(target_os = "macos", feature = "livekit-macos"), + all( + not(target_os = "macos"), + feature = "livekit-macos", + not(feature = "livekit-cross-platform") + ) +))] +pub use macos::*; diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 0d47cec441..ec6e9015d4 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -3944,6 +3944,17 @@ impl Workspace { None } + #[cfg(target_os = "windows")] + fn shared_screen_for_peer( + &self, + _peer_id: PeerId, + _pane: &View, + _cx: &mut WindowContext, + ) -> Option> { + None + } + + #[cfg(not(target_os = "windows"))] fn shared_screen_for_peer( &self, peer_id: PeerId, @@ -3962,7 +3973,7 @@ impl Workspace { } } - Some(cx.new_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx))) + Some(cx.new_view(|cx| SharedScreen::new(track, peer_id, user.clone(), cx))) } pub fn on_window_activation_changed(&mut self, cx: &mut ViewContext) { diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 74dd2601ad..6b26a01f27 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -126,6 +126,12 @@ welcome.workspace = true workspace.workspace = true zed_actions.workspace = true +[target.'cfg(target_os = "macos")'.dependencies] +workspace = { workspace = true, features = ["livekit-macos"] } + +[target.'cfg(not(target_os = "macos"))'.dependencies] +workspace = { workspace = true, features = ["livekit-cross-platform"] } + [target.'cfg(target_os = "windows")'.dependencies] windows.workspace = true diff --git a/crates/zed/build.rs b/crates/zed/build.rs index 3013773f91..bf2a0d99fe 100644 --- a/crates/zed/build.rs +++ b/crates/zed/build.rs @@ -13,6 +13,14 @@ fn main() { println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path"); } + if std::env::var("ZED_BUNDLE").ok().as_deref() == Some("true") { + // Find WebRTC.framework in the Frameworks folder when running as part of an application bundle. + println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path/../Frameworks"); + } else { + // Find WebRTC.framework as a sibling of the executable when running outside of an application bundle. + println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path"); + } + // Weakly link ReplayKit to ensure Zed can be used on macOS 10.15+. println!("cargo:rustc-link-arg=-Wl,-weak_framework,ReplayKit"); diff --git a/script/bundle-linux b/script/bundle-linux index 98b49ae4da..c05037b6cc 100755 --- a/script/bundle-linux +++ b/script/bundle-linux @@ -92,7 +92,7 @@ cp "${target_dir}/${target_triple}/release/cli" "${zed_dir}/bin/zed" find_libs() { ldd ${target_dir}/${target_triple}/release/zed |\ cut -d' ' -f3 |\ - grep -v '\<\(libstdc++.so\|libc.so\|libgcc_s.so\|libm.so\|libpthread.so\|libdl.so\)' + grep -v '\<\(libstdc++.so\|libc.so\|libgcc_s.so\|libm.so\|libpthread.so\|libdl.so\|libasound.so\)' } mkdir -p "${zed_dir}/lib" diff --git a/typos.toml b/typos.toml index 0682d0a3a9..dc724dd50d 100644 --- a/typos.toml +++ b/typos.toml @@ -22,7 +22,7 @@ extend-exclude = [ # Stripe IDs are flagged as typos. "crates/collab/src/db/tests/processed_stripe_event_tests.rs", # Not our typos. - "crates/live_kit_server/", + "crates/livekit_server/", # Vim makes heavy use of partial typing tables. "crates/vim/", # Editor and file finder rely on partial typing and custom in-string syntax. From 28650b2fac0bd8c782e1b962784ed7edafa3909e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 15:23:08 -0800 Subject: [PATCH 307/886] Update Rust crate blake3 to v1.5.5 (#21554) 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 | |---|---|---|---| | [blake3](https://redirect.github.com/BLAKE3-team/BLAKE3) | workspace.dependencies | patch | `1.5.4` -> `1.5.5` | --- ### Release Notes
BLAKE3-team/BLAKE3 (blake3) ### [`v1.5.5`](https://redirect.github.com/BLAKE3-team/BLAKE3/releases/tag/1.5.5) [Compare Source](https://redirect.github.com/BLAKE3-team/BLAKE3/compare/1.5.4...1.5.5) version 1.5.5 Changes since 1.5.4: - `b3sum --check` now supports checkfiles with Windows-style newlines. `b3sum` still emits Unix-style newlines, even on Windows, but sometimes text editors or version control tools will swap them. - The "digest" feature (deleted in v1.5.2) has been added back to the `blake3` crate. This is for backwards compatibility only, and it's insta-deprecated. All callers should prefer the "traits-preview" feature.
--- ### 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 4d040c581c..ac1e4edabe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1838,9 +1838,9 @@ dependencies = [ [[package]] name = "blake3" -version = "1.5.4" +version = "1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d82033247fd8e890df8f740e407ad4d038debb9eb1f40533fffb32e7d17dc6f7" +checksum = "b8ee0c1824c4dea5b5f81736aff91bae041d2c07ee1192bec91054e10e3e601e" dependencies = [ "arrayref", "arrayvec", From aff17322f31fa3c1037bf8ffcdc6b6aa470027dc Mon Sep 17 00:00:00 2001 From: Nick Breaton Date: Thu, 5 Dec 2024 18:23:37 -0500 Subject: [PATCH 308/886] Detect wider variety of usernames for SSH-based remotes (#21508) Closes #21507 Release Notes: - Fixed detection of git remotes when using SSH and username is not "git". --- Cargo.lock | 1 + crates/git/Cargo.toml | 1 + crates/git/src/remote.rs | 16 ++++++++++++++-- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ac1e4edabe..3167456349 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5131,6 +5131,7 @@ dependencies = [ "log", "parking_lot", "pretty_assertions", + "regex", "rope", "serde", "serde_json", diff --git a/crates/git/Cargo.toml b/crates/git/Cargo.toml index c0f43e08a8..d31538353e 100644 --- a/crates/git/Cargo.toml +++ b/crates/git/Cargo.toml @@ -21,6 +21,7 @@ gpui.workspace = true http_client.workspace = true log.workspace = true parking_lot.workspace = true +regex.workspace = true rope.workspace = true serde.workspace = true smol.workspace = true diff --git a/crates/git/src/remote.rs b/crates/git/src/remote.rs index 430836fcf3..e9814afc51 100644 --- a/crates/git/src/remote.rs +++ b/crates/git/src/remote.rs @@ -1,17 +1,23 @@ +use std::sync::LazyLock; + use derive_more::Deref; +use regex::Regex; use url::Url; /// The URL to a Git remote. #[derive(Debug, PartialEq, Eq, Clone, Deref)] pub struct RemoteUrl(Url); +static USERNAME_REGEX: LazyLock = + LazyLock::new(|| Regex::new(r"^[0-9a-zA-Z\-_]+@").expect("Failed to create USERNAME_REGEX")); + impl std::str::FromStr for RemoteUrl { type Err = url::ParseError; fn from_str(input: &str) -> Result { - if input.starts_with("git@") { + if USERNAME_REGEX.is_match(input) { // Rewrite remote URLs like `git@github.com:user/repo.git` to `ssh://git@github.com/user/repo.git` - let ssh_url = input.replacen(':', "/", 1).replace("git@", "ssh://git@"); + let ssh_url = format!("ssh://{}", input.replacen(':', "/", 1)); Ok(RemoteUrl(Url::parse(&ssh_url)?)) } else { Ok(RemoteUrl(Url::parse(input)?)) @@ -40,6 +46,12 @@ mod tests { "github.com", "/octocat/zed.git", ), + ( + "org-000000@github.com:octocat/zed.git", + "ssh", + "github.com", + "/octocat/zed.git", + ), ( "ssh://git@github.com/octocat/zed.git", "ssh", From cf4e847c62e435a8c2daa8d750386609c9b67461 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 5 Dec 2024 16:32:17 -0800 Subject: [PATCH 309/886] Implement session-global include_warnings in the diagnostic item (#21618) Release Notes: - Make the include warnings toggle in the diagnostic tab global for a zed session. --- crates/diagnostics/src/diagnostics.rs | 38 ++++++++++++++++++--- crates/diagnostics/src/diagnostics_tests.rs | 32 ++++++++++++++--- 2 files changed, 61 insertions(+), 9 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 48a92d906e..9f02033237 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -16,8 +16,8 @@ use editor::{ }; use gpui::{ actions, div, svg, AnyElement, AnyView, AppContext, Context, EventEmitter, FocusHandle, - FocusableView, HighlightStyle, InteractiveElement, IntoElement, Model, ParentElement, Render, - SharedString, Styled, StyledText, Subscription, Task, View, ViewContext, VisualContext, + FocusableView, Global, HighlightStyle, InteractiveElement, IntoElement, Model, ParentElement, + Render, SharedString, Styled, StyledText, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowContext, }; use language::{ @@ -46,6 +46,9 @@ use workspace::{ actions!(diagnostics, [Deploy, ToggleWarnings]); +struct IncludeWarnings(bool); +impl Global for IncludeWarnings {} + pub fn init(cx: &mut AppContext) { ProjectDiagnosticsSettings::register(cx); cx.observe_new_views(ProjectDiagnosticsEditor::register) @@ -117,6 +120,7 @@ impl ProjectDiagnosticsEditor { fn new_with_context( context: u32, + include_warnings: bool, project_handle: Model, workspace: WeakView, cx: &mut ViewContext, @@ -186,19 +190,24 @@ impl ProjectDiagnosticsEditor { } }) .detach(); + cx.observe_global::(|this, cx| { + this.include_warnings = cx.global::().0; + this.update_all_excerpts(cx); + }) + .detach(); let project = project_handle.read(cx); let mut this = Self { project: project_handle.clone(), context, summary: project.diagnostic_summary(false, cx), + include_warnings, workspace, excerpts, focus_handle, editor, path_states: Default::default(), paths_to_update: Default::default(), - include_warnings: ProjectDiagnosticsSettings::get_global(cx).include_warnings, update_excerpts_task: None, _subscription: project_event_subscription, }; @@ -243,11 +252,13 @@ impl ProjectDiagnosticsEditor { fn new( project_handle: Model, + include_warnings: bool, workspace: WeakView, cx: &mut ViewContext, ) -> Self { Self::new_with_context( editor::DEFAULT_MULTIBUFFER_CONTEXT, + include_warnings, project_handle, workspace, cx, @@ -259,8 +270,19 @@ impl ProjectDiagnosticsEditor { workspace.activate_item(&existing, true, true, cx); } else { let workspace_handle = cx.view().downgrade(); + + let include_warnings = match cx.try_global::() { + Some(include_warnings) => include_warnings.0, + None => ProjectDiagnosticsSettings::get_global(cx).include_warnings, + }; + let diagnostics = cx.new_view(|cx| { - ProjectDiagnosticsEditor::new(workspace.project().clone(), workspace_handle, cx) + ProjectDiagnosticsEditor::new( + workspace.project().clone(), + include_warnings, + workspace_handle, + cx, + ) }); workspace.add_item_to_active_pane(Box::new(diagnostics), None, true, cx); } @@ -268,6 +290,7 @@ impl ProjectDiagnosticsEditor { fn toggle_warnings(&mut self, _: &ToggleWarnings, cx: &mut ViewContext) { self.include_warnings = !self.include_warnings; + cx.set_global(IncludeWarnings(self.include_warnings)); self.update_all_excerpts(cx); cx.notify(); } @@ -740,7 +763,12 @@ impl Item for ProjectDiagnosticsEditor { Self: Sized, { Some(cx.new_view(|cx| { - ProjectDiagnosticsEditor::new(self.project.clone(), self.workspace.clone(), cx) + ProjectDiagnosticsEditor::new( + self.project.clone(), + self.include_warnings, + self.workspace.clone(), + cx, + ) })) } diff --git a/crates/diagnostics/src/diagnostics_tests.rs b/crates/diagnostics/src/diagnostics_tests.rs index ff305e45a2..6ee1a90511 100644 --- a/crates/diagnostics/src/diagnostics_tests.rs +++ b/crates/diagnostics/src/diagnostics_tests.rs @@ -151,7 +151,13 @@ async fn test_diagnostics(cx: &mut TestAppContext) { // Open the project diagnostics view while there are already diagnostics. let view = window.build_view(cx, |cx| { - ProjectDiagnosticsEditor::new_with_context(1, project.clone(), workspace.downgrade(), cx) + ProjectDiagnosticsEditor::new_with_context( + 1, + true, + project.clone(), + workspace.downgrade(), + cx, + ) }); let editor = view.update(cx, |view, _| view.editor.clone()); @@ -459,7 +465,13 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) { let workspace = window.root(cx).unwrap(); let view = window.build_view(cx, |cx| { - ProjectDiagnosticsEditor::new_with_context(1, project.clone(), workspace.downgrade(), cx) + ProjectDiagnosticsEditor::new_with_context( + 1, + true, + project.clone(), + workspace.downgrade(), + cx, + ) }); let editor = view.update(cx, |view, _| view.editor.clone()); @@ -720,7 +732,13 @@ async fn test_random_diagnostics(cx: &mut TestAppContext, mut rng: StdRng) { let workspace = window.root(cx).unwrap(); let mutated_view = window.build_view(cx, |cx| { - ProjectDiagnosticsEditor::new_with_context(1, project.clone(), workspace.downgrade(), cx) + ProjectDiagnosticsEditor::new_with_context( + 1, + true, + project.clone(), + workspace.downgrade(), + cx, + ) }); workspace.update(cx, |workspace, cx| { @@ -816,7 +834,13 @@ async fn test_random_diagnostics(cx: &mut TestAppContext, mut rng: StdRng) { log::info!("constructing reference diagnostics view"); let reference_view = window.build_view(cx, |cx| { - ProjectDiagnosticsEditor::new_with_context(1, project.clone(), workspace.downgrade(), cx) + ProjectDiagnosticsEditor::new_with_context( + 1, + true, + project.clone(), + workspace.downgrade(), + cx, + ) }); cx.run_until_parked(); From f6b5e1734ea6f92781aca8f4a17afad9ab709cc1 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 5 Dec 2024 16:52:14 -0800 Subject: [PATCH 310/886] Get unstaged changes when excerpts of new buffers are added (#21619) This fixes an error on nightly, introduced in https://github.com/zed-industries/zed/pull/21258, where diffs were not shown for buffers that were added to multi-buffers after construction. Release Notes: - N/A --- crates/editor/src/editor.rs | 54 ++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index bb4a2788a7..132a5e04fb 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2023,28 +2023,7 @@ impl Editor { let mut code_action_providers = Vec::new(); if let Some(project) = project.clone() { - let mut tasks = Vec::new(); - buffer.update(cx, |multibuffer, cx| { - project.update(cx, |project, cx| { - multibuffer.for_each_buffer(|buffer| { - tasks.push(project.open_unstaged_changes(buffer.clone(), cx)) - }); - }); - }); - - cx.spawn(|this, mut cx| async move { - let change_sets = futures::future::join_all(tasks).await; - this.update(&mut cx, |this, cx| { - for change_set in change_sets { - if let Some(change_set) = change_set.log_err() { - this.diff_map.add_change_set(change_set, cx); - } - } - }) - .ok(); - }) - .detach(); - + get_unstaged_changes_for_buffers(&project, buffer.read(cx).all_buffers(), cx); code_action_providers.push(Arc::new(project) as Arc<_>); } @@ -12646,6 +12625,12 @@ impl Editor { excerpts, } => { self.tasks_update_task = Some(self.refresh_runnables(cx)); + let buffer_id = buffer.read(cx).remote_id(); + if !self.diff_map.diff_bases.contains_key(&buffer_id) { + if let Some(project) = &self.project { + get_unstaged_changes_for_buffers(project, [buffer.clone()], cx); + } + } cx.emit(EditorEvent::ExcerptsAdded { buffer: buffer.clone(), predecessor: *predecessor, @@ -13342,6 +13327,31 @@ impl Editor { } } +fn get_unstaged_changes_for_buffers( + project: &Model, + buffers: impl IntoIterator>, + cx: &mut ViewContext, +) { + let mut tasks = Vec::new(); + project.update(cx, |project, cx| { + for buffer in buffers { + tasks.push(project.open_unstaged_changes(buffer.clone(), cx)) + } + }); + cx.spawn(|this, mut cx| async move { + let change_sets = futures::future::join_all(tasks).await; + this.update(&mut cx, |this, cx| { + for change_set in change_sets { + if let Some(change_set) = change_set.log_err() { + this.diff_map.add_change_set(change_set, cx); + } + } + }) + .ok(); + }) + .detach(); +} + fn char_len_with_expanded_tabs(offset: usize, text: &str, tab_size: NonZeroU32) -> usize { let tab_size = tab_size.get() as usize; let mut width = offset; From 7e40addb5fd33a460978fe6f5ffe1118a15b1571 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Fri, 6 Dec 2024 10:01:57 +0100 Subject: [PATCH 311/886] markdown preview: Fix panic when parsing empty image tag (#21616) Closes #21534 While investigating the panic, I noticed that the code was pretty complicated and decided to refactor parts of it to reduce redundancy. Release Notes: - Fixed an issue where the app could crash when opening the markdown preview with a malformed image tag --- .../markdown_preview/src/markdown_elements.rs | 118 ++-------- .../markdown_preview/src/markdown_parser.rs | 103 ++++----- .../src/markdown_preview_view.rs | 17 +- .../markdown_preview/src/markdown_renderer.rs | 206 +++--------------- 4 files changed, 104 insertions(+), 340 deletions(-) diff --git a/crates/markdown_preview/src/markdown_elements.rs b/crates/markdown_preview/src/markdown_elements.rs index ff43fab08a..256ce6ee4a 100644 --- a/crates/markdown_preview/src/markdown_elements.rs +++ b/crates/markdown_preview/src/markdown_elements.rs @@ -18,22 +18,19 @@ pub enum ParsedMarkdownElement { } impl ParsedMarkdownElement { - pub fn source_range(&self) -> Range { - match self { + pub fn source_range(&self) -> Option> { + Some(match self { Self::Heading(heading) => heading.source_range.clone(), Self::ListItem(list_item) => list_item.source_range.clone(), Self::Table(table) => table.source_range.clone(), Self::BlockQuote(block_quote) => block_quote.source_range.clone(), Self::CodeBlock(code_block) => code_block.source_range.clone(), - Self::Paragraph(text) => match &text[0] { + Self::Paragraph(text) => match text.get(0)? { MarkdownParagraphChunk::Text(t) => t.source_range.clone(), - MarkdownParagraphChunk::Image(image) => match image { - Image::Web { source_range, .. } => source_range.clone(), - Image::Path { source_range, .. } => source_range.clone(), - }, + MarkdownParagraphChunk::Image(image) => image.source_range.clone(), }, Self::HorizontalRule(range) => range.clone(), - } + }) } pub fn is_list_item(&self) -> bool { @@ -289,104 +286,27 @@ impl Display for Link { /// A Markdown Image #[derive(Debug, Clone)] #[cfg_attr(test, derive(PartialEq))] -pub enum Image { - Web { - source_range: Range, - /// The URL of the Image. - url: String, - /// Link URL if exists. - link: Option, - /// alt text if it exists - alt_text: Option, - }, - /// Image path on the filesystem. - Path { - source_range: Range, - /// The path as provided in the Markdown document. - display_path: PathBuf, - /// The absolute path to the item. - path: PathBuf, - /// Link URL if exists. - link: Option, - /// alt text if it exists - alt_text: Option, - }, +pub struct Image { + pub link: Link, + pub source_range: Range, + pub alt_text: Option, } impl Image { pub fn identify( + text: String, source_range: Range, file_location_directory: Option, - text: String, - link: Option, - ) -> Option { - if text.starts_with("http") { - return Some(Image::Web { - source_range, - url: text, - link, - alt_text: None, - }); - } - let path = PathBuf::from(&text); - if path.is_absolute() { - return Some(Image::Path { - source_range, - display_path: path.clone(), - path, - link, - alt_text: None, - }); - } - if let Some(file_location_directory) = file_location_directory { - let display_path = path; - let path = file_location_directory.join(text); - return Some(Image::Path { - source_range, - display_path, - path, - link, - alt_text: None, - }); - } - None + ) -> Option { + let link = Link::identify(file_location_directory, text)?; + Some(Self { + source_range, + link, + alt_text: None, + }) } - pub fn with_alt_text(&self, alt_text: ParsedMarkdownText) -> Self { - match self { - Image::Web { - ref source_range, - ref url, - ref link, - .. - } => Image::Web { - source_range: source_range.clone(), - url: url.clone(), - link: link.clone(), - alt_text: Some(alt_text), - }, - Image::Path { - ref source_range, - ref display_path, - ref path, - ref link, - .. - } => Image::Path { - source_range: source_range.clone(), - display_path: display_path.clone(), - path: path.clone(), - link: link.clone(), - alt_text: Some(alt_text), - }, - } - } -} - -impl Display for Image { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Image::Web { url, .. } => write!(f, "{}", url), - Image::Path { display_path, .. } => write!(f, "{}", display_path.display()), - } + pub fn set_alt_text(&mut self, alt_text: SharedString) { + self.alt_text = Some(alt_text); } } diff --git a/crates/markdown_preview/src/markdown_parser.rs b/crates/markdown_preview/src/markdown_parser.rs index 211cca2494..f433edf8b3 100644 --- a/crates/markdown_preview/src/markdown_parser.rs +++ b/crates/markdown_preview/src/markdown_parser.rs @@ -214,7 +214,7 @@ impl<'a> MarkdownParser<'a> { break; } - let (current, _source_range) = self.current().unwrap(); + let (current, _) = self.current().unwrap(); let prev_len = text.len(); match current { Event::SoftBreak => { @@ -314,56 +314,29 @@ impl<'a> MarkdownParser<'a> { )); } } - if let Some(mut image) = image.clone() { - let is_valid_image = match image.clone() { - Image::Path { display_path, .. } => { - gpui::ImageSource::try_from(display_path).is_ok() - } - Image::Web { url, .. } => gpui::ImageSource::try_from(url).is_ok(), - }; - if is_valid_image { - text.truncate(text.len() - t.len()); - if !t.is_empty() { - let alt_text = ParsedMarkdownText { - source_range: source_range.clone(), - contents: t.to_string(), - highlights: highlights.clone(), - region_ranges: region_ranges.clone(), - regions: regions.clone(), - }; - image = image.with_alt_text(alt_text); - } else { - let alt_text = ParsedMarkdownText { - source_range: source_range.clone(), - contents: "img".to_string(), - highlights: highlights.clone(), - region_ranges: region_ranges.clone(), - regions: regions.clone(), - }; - image = image.with_alt_text(alt_text); - } - if !text.is_empty() { - let parsed_regions = - MarkdownParagraphChunk::Text(ParsedMarkdownText { - source_range: source_range.clone(), - contents: text.clone(), - highlights: highlights.clone(), - region_ranges: region_ranges.clone(), - regions: regions.clone(), - }); - text = String::new(); - highlights = vec![]; - region_ranges = vec![]; - regions = vec![]; - markdown_text_like.push(parsed_regions); - } - - let parsed_image = MarkdownParagraphChunk::Image(image.clone()); - markdown_text_like.push(parsed_image); - style = MarkdownHighlightStyle::default(); + if let Some(image) = image.as_mut() { + text.truncate(text.len() - t.len()); + image.set_alt_text(t.to_string().into()); + if !text.is_empty() { + let parsed_regions = MarkdownParagraphChunk::Text(ParsedMarkdownText { + source_range: source_range.clone(), + contents: text.clone(), + highlights: highlights.clone(), + region_ranges: region_ranges.clone(), + regions: regions.clone(), + }); + text = String::new(); + highlights = vec![]; + region_ranges = vec![]; + regions = vec![]; + markdown_text_like.push(parsed_regions); } + + let parsed_image = MarkdownParagraphChunk::Image(image.clone()); + markdown_text_like.push(parsed_image); + style = MarkdownHighlightStyle::default(); style.underline = true; - }; + } } Event::Code(t) => { text.push_str(t.as_ref()); @@ -395,10 +368,9 @@ impl<'a> MarkdownParser<'a> { } Tag::Image { dest_url, .. } => { image = Image::identify( + dest_url.to_string(), source_range.clone(), self.file_location_directory.clone(), - dest_url.to_string(), - link.clone(), ); } _ => { @@ -926,6 +898,18 @@ mod tests { ); } + #[gpui::test] + async fn test_empty_image() { + let parsed = parse("![]()").await; + + let paragraph = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] { + text + } else { + panic!("Expected a paragraph"); + }; + assert_eq!(paragraph.len(), 0); + } + #[gpui::test] async fn test_image_links_detection() { let parsed = parse("![test](https://blog.logrocket.com/wp-content/uploads/2024/04/exploring-zed-open-source-code-editor-rust-2.png)").await; @@ -937,19 +921,12 @@ mod tests { }; assert_eq!( paragraph[0], - MarkdownParagraphChunk::Image(Image::Web { + MarkdownParagraphChunk::Image(Image { source_range: 0..111, - url: "https://blog.logrocket.com/wp-content/uploads/2024/04/exploring-zed-open-source-code-editor-rust-2.png".to_string(), - link: None, - alt_text: Some( - ParsedMarkdownText { - source_range: 0..111, - contents: "test".to_string(), - highlights: vec![], - region_ranges: vec![], - regions: vec![], - }, - ), + link: Link::Web { + url: "https://blog.logrocket.com/wp-content/uploads/2024/04/exploring-zed-open-source-code-editor-rust-2.png".to_string(), + }, + alt_text: Some("test".into()), },) ); } diff --git a/crates/markdown_preview/src/markdown_preview_view.rs b/crates/markdown_preview/src/markdown_preview_view.rs index 07fbd94b29..8d9c7e4145 100644 --- a/crates/markdown_preview/src/markdown_preview_view.rs +++ b/crates/markdown_preview/src/markdown_preview_view.rs @@ -192,11 +192,16 @@ impl MarkdownPreviewView { .group("markdown-block") .on_click(cx.listener(move |this, event: &ClickEvent, cx| { if event.down.click_count == 2 { - if let Some(block) = - this.contents.as_ref().and_then(|c| c.children.get(ix)) + if let Some(source_range) = this + .contents + .as_ref() + .and_then(|c| c.children.get(ix)) + .and_then(|block| block.source_range()) { - let start = block.source_range().start; - this.move_cursor_to_block(cx, start..start); + this.move_cursor_to_block( + cx, + source_range.start..source_range.start, + ); } } })) @@ -410,7 +415,9 @@ impl MarkdownPreviewView { let mut last_end = 0; if let Some(content) = &self.contents { for (i, block) in content.children.iter().enumerate() { - let Range { start, end } = block.source_range(); + let Some(Range { start, end }) = block.source_range() else { + continue; + }; // Check if the cursor is between the last block and the current block if last_end <= cursor && cursor < start { diff --git a/crates/markdown_preview/src/markdown_renderer.rs b/crates/markdown_preview/src/markdown_renderer.rs index 39bcd546df..7a13077194 100644 --- a/crates/markdown_preview/src/markdown_renderer.rs +++ b/crates/markdown_preview/src/markdown_renderer.rs @@ -1,8 +1,8 @@ use crate::markdown_elements::{ - HeadingLevel, Image, Link, MarkdownParagraph, MarkdownParagraphChunk, ParsedMarkdown, + HeadingLevel, Link, MarkdownParagraph, MarkdownParagraphChunk, ParsedMarkdown, ParsedMarkdownBlockQuote, ParsedMarkdownCodeBlock, ParsedMarkdownElement, ParsedMarkdownHeading, ParsedMarkdownListItem, ParsedMarkdownListItemType, ParsedMarkdownTable, - ParsedMarkdownTableAlignment, ParsedMarkdownTableRow, ParsedMarkdownText, + ParsedMarkdownTableAlignment, ParsedMarkdownTableRow, }; use gpui::{ div, img, px, rems, AbsoluteLength, AnyElement, ClipboardItem, DefiniteLength, Div, Element, @@ -13,7 +13,6 @@ use gpui::{ use settings::Settings; use std::{ ops::{Mul, Range}, - path::Path, sync::Arc, vec, }; @@ -505,103 +504,41 @@ fn render_markdown_text(parsed_new: &MarkdownParagraph, cx: &mut RenderContext) } MarkdownParagraphChunk::Image(image) => { - let (link, source_range, image_source, alt_text) = match image { - Image::Web { - link, - source_range, - url, - alt_text, - } => ( - link, - source_range, - Resource::Uri(url.clone().into()), - alt_text, - ), - Image::Path { - link, - source_range, - path, - alt_text, - .. - } => { - let image_path = Path::new(path.to_str().unwrap()); - ( - link, - source_range, - Resource::Path(Arc::from(image_path)), - alt_text, - ) - } + let image_resource = match image.link.clone() { + Link::Web { url } => Resource::Uri(url.into()), + Link::Path { path, .. } => Resource::Path(Arc::from(path)), }; - let element_id = cx.next_id(source_range); + let element_id = cx.next_id(&image.source_range); - match link { - None => { - let fallback_workspace = workspace_clone.clone(); - let fallback_syntax_theme = syntax_theme.clone(); - let fallback_text_style = text_style.clone(); - let fallback_alt_text = alt_text.clone(); - let element_id_new = element_id.clone(); - let element = div() - .child(img(ImageSource::Resource(image_source)).with_fallback({ - move || { - fallback_text( - fallback_alt_text.clone().unwrap(), - element_id.clone(), - &fallback_syntax_theme, - code_span_bg_color, - fallback_workspace.clone(), - &fallback_text_style, - ) + let image_element = div() + .id(element_id) + .child(img(ImageSource::Resource(image_resource)).with_fallback({ + let alt_text = image.alt_text.clone(); + { + move || div().children(alt_text.clone()).into_any_element() + } + })) + .tooltip({ + let link = image.link.clone(); + move |cx| LinkPreview::new(&link.to_string(), cx) + }) + .on_click({ + let workspace = workspace_clone.clone(); + let link = image.link.clone(); + move |_event, window_cx| match &link { + Link::Web { url } => window_cx.open_url(url), + Link::Path { path, .. } => { + if let Some(workspace) = &workspace { + _ = workspace.update(window_cx, |workspace, cx| { + workspace.open_abs_path(path.clone(), false, cx).detach(); + }); } - })) - .id(element_id_new) - .into_any(); - any_element.push(element); - } - Some(link) => { - let link_click = link.clone(); - let link_tooltip = link.clone(); - let fallback_workspace = workspace_clone.clone(); - let fallback_syntax_theme = syntax_theme.clone(); - let fallback_text_style = text_style.clone(); - let fallback_alt_text = alt_text.clone(); - let element_id_new = element_id.clone(); - let image_element = div() - .child(img(ImageSource::Resource(image_source)).with_fallback({ - move || { - fallback_text( - fallback_alt_text.clone().unwrap(), - element_id.clone(), - &fallback_syntax_theme, - code_span_bg_color, - fallback_workspace.clone(), - &fallback_text_style, - ) - } - })) - .id(element_id_new) - .tooltip(move |cx| LinkPreview::new(&link_tooltip.to_string(), cx)) - .on_click({ - let workspace = workspace_clone.clone(); - move |_event, window_cx| match &link_click { - Link::Web { url } => window_cx.open_url(url), - Link::Path { path, .. } => { - if let Some(workspace) = &workspace { - _ = workspace.update(window_cx, |workspace, cx| { - workspace - .open_abs_path(path.clone(), false, cx) - .detach(); - }); - } - } - } - }) - .into_any(); - any_element.push(image_element); - } - } + } + } + }) + .into_any(); + any_element.push(image_element); } } } @@ -613,80 +550,3 @@ fn render_markdown_rule(cx: &mut RenderContext) -> AnyElement { let rule = div().w_full().h(px(2.)).bg(cx.border_color); div().pt_3().pb_3().child(rule).into_any() } - -fn fallback_text( - parsed: ParsedMarkdownText, - source_range: ElementId, - syntax_theme: &theme::SyntaxTheme, - code_span_bg_color: Hsla, - workspace: Option>, - text_style: &TextStyle, -) -> AnyElement { - let element_id = source_range; - - let highlights = gpui::combine_highlights( - parsed.highlights.iter().filter_map(|(range, highlight)| { - let highlight = highlight.to_highlight_style(syntax_theme)?; - Some((range.clone(), highlight)) - }), - parsed - .regions - .iter() - .zip(&parsed.region_ranges) - .filter_map(|(region, range)| { - if region.code { - Some(( - range.clone(), - HighlightStyle { - background_color: Some(code_span_bg_color), - ..Default::default() - }, - )) - } else { - None - } - }), - ); - let mut links = Vec::new(); - let mut link_ranges = Vec::new(); - for (range, region) in parsed.region_ranges.iter().zip(&parsed.regions) { - if let Some(link) = region.link.clone() { - links.push(link); - link_ranges.push(range.clone()); - } - } - let element = div() - .child( - InteractiveText::new( - element_id, - StyledText::new(parsed.contents.clone()).with_highlights(text_style, highlights), - ) - .tooltip({ - let links = links.clone(); - let link_ranges = link_ranges.clone(); - move |idx, cx| { - for (ix, range) in link_ranges.iter().enumerate() { - if range.contains(&idx) { - return Some(LinkPreview::new(&links[ix].to_string(), cx)); - } - } - None - } - }) - .on_click( - link_ranges, - move |clicked_range_ix, window_cx| match &links[clicked_range_ix] { - Link::Web { url } => window_cx.open_url(url), - Link::Path { path, .. } => { - if let Some(workspace) = &workspace { - _ = workspace.update(window_cx, |workspace, cx| { - workspace.open_abs_path(path.clone(), false, cx).detach(); - }); - } - } - }, - ), - ) - .into_any(); - return element; -} From 4b16b73f8003be8d26d3452f8008930b936154da Mon Sep 17 00:00:00 2001 From: Nils Koch Date: Fri, 6 Dec 2024 12:17:24 +0000 Subject: [PATCH 312/886] Fix panel.background color override (#21559) Closes #21266 Release Notes: - Fixes not using the `panel.background` color in the file tree See comments in https://github.com/zed-industries/zed/issues/21266 for more details. Co-authored-by: Danilo Leal --- crates/project_panel/src/project_panel.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index ca6f89f69a..12c90e2195 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -268,7 +268,7 @@ fn get_item_color(cx: &ViewContext) -> ItemColors { let colors = cx.theme().colors(); ItemColors { - default: colors.surface_background, + default: colors.panel_background, hover: colors.ghost_element_hover, drag_over: colors.drop_target_background, marked_active: colors.ghost_element_selected, From 7b1d1bf79e7faa1b2024c58b3afdb9363710b588 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Fri, 6 Dec 2024 09:17:34 -0300 Subject: [PATCH 313/886] Update `panel.focused_border` token across themes (#21612) Follow up to https://github.com/zed-industries/zed/pull/21593 This PR updates all built-in themes `panel.focused_border` tokens using the same HEX code used for `text_accent`. There shouldn't be any visual change here given the project panel item, when focused, was using `Color::Selected`, which maps to `text_accent`, to color its border. In the linked PR above, the project panel item was updated to use the dedicated token for that. This is good because now theme markers will be able to customize them separately (e.g., having a different `text_accent` color than `panel.focused_border`). Release Notes: - N/A --- assets/themes/andromeda/andromeda.json | 2 +- assets/themes/atelier/atelier.json | 4 ++-- assets/themes/ayu/ayu.json | 4 ++-- assets/themes/gruvbox/gruvbox.json | 4 ++-- assets/themes/rose_pine/rose_pine.json | 6 +++--- assets/themes/sandcastle/sandcastle.json | 2 +- assets/themes/solarized/solarized.json | 4 ++-- assets/themes/summercamp/summercamp.json | 2 +- 8 files changed, 14 insertions(+), 14 deletions(-) diff --git a/assets/themes/andromeda/andromeda.json b/assets/themes/andromeda/andromeda.json index 633b5c308f..9a9ab5356e 100644 --- a/assets/themes/andromeda/andromeda.json +++ b/assets/themes/andromeda/andromeda.json @@ -46,7 +46,7 @@ "tab.active_background": "#1e2025ff", "search.match_background": "#11a79366", "panel.background": "#21242bff", - "panel.focused_border": null, + "panel.focused_border": "#10a793ff", "pane.focused_border": null, "scrollbar.thumb.background": "#f7f7f84c", "scrollbar.thumb.hover_background": "#252931ff", diff --git a/assets/themes/atelier/atelier.json b/assets/themes/atelier/atelier.json index f72e8e84ee..cbfb6bea85 100644 --- a/assets/themes/atelier/atelier.json +++ b/assets/themes/atelier/atelier.json @@ -46,7 +46,7 @@ "tab.active_background": "#19171cff", "search.match_background": "#576dda66", "panel.background": "#221f26ff", - "panel.focused_border": null, + "panel.focused_border": "#566ddaff", "pane.focused_border": null, "scrollbar.thumb.background": "#efecf44c", "scrollbar.thumb.hover_background": "#332f38ff", @@ -431,7 +431,7 @@ "tab.active_background": "#efecf4ff", "search.match_background": "#586dda66", "panel.background": "#e6e3ebff", - "panel.focused_border": null, + "panel.focused_border": "#586cdaff", "pane.focused_border": null, "scrollbar.thumb.background": "#19171c4c", "scrollbar.thumb.hover_background": "#cbc8d1ff", diff --git a/assets/themes/ayu/ayu.json b/assets/themes/ayu/ayu.json index d511ebf84a..a7c86ef0ba 100644 --- a/assets/themes/ayu/ayu.json +++ b/assets/themes/ayu/ayu.json @@ -46,7 +46,7 @@ "tab.active_background": "#0d1016ff", "search.match_background": "#5ac2fe66", "panel.background": "#1f2127ff", - "panel.focused_border": null, + "panel.focused_border": "#5ac1feff", "pane.focused_border": null, "scrollbar.thumb.background": "#bfbdb64c", "scrollbar.thumb.hover_background": "#2d2f34ff", @@ -416,7 +416,7 @@ "tab.active_background": "#fcfcfcff", "search.match_background": "#3b9ee566", "panel.background": "#ececedff", - "panel.focused_border": null, + "panel.focused_border": "#3b9ee5ff", "pane.focused_border": null, "scrollbar.thumb.background": "#5c61664c", "scrollbar.thumb.hover_background": "#dfe0e1ff", diff --git a/assets/themes/gruvbox/gruvbox.json b/assets/themes/gruvbox/gruvbox.json index 908ce3a28a..4f599cdfe6 100644 --- a/assets/themes/gruvbox/gruvbox.json +++ b/assets/themes/gruvbox/gruvbox.json @@ -55,7 +55,7 @@ "tab.active_background": "#282828ff", "search.match_background": "#83a59866", "panel.background": "#3a3735ff", - "panel.focused_border": null, + "panel.focused_border": "#83a598ff", "pane.focused_border": null, "scrollbar.thumb.background": "#fbf1c74c", "scrollbar.thumb.hover_background": "#494340ff", @@ -439,7 +439,7 @@ "tab.active_background": "#1d2021ff", "search.match_background": "#83a59866", "panel.background": "#393634ff", - "panel.focused_border": null, + "panel.focused_border": "#83a598ff", "pane.focused_border": null, "scrollbar.thumb.background": "#fbf1c74c", "scrollbar.thumb.hover_background": "#494340ff", diff --git a/assets/themes/rose_pine/rose_pine.json b/assets/themes/rose_pine/rose_pine.json index 2ff97da117..b081f5e133 100644 --- a/assets/themes/rose_pine/rose_pine.json +++ b/assets/themes/rose_pine/rose_pine.json @@ -46,7 +46,7 @@ "tab.active_background": "#191724ff", "search.match_background": "#57949f66", "panel.background": "#1c1b2aff", - "panel.focused_border": null, + "panel.focused_border": "#9bced6ff", "pane.focused_border": null, "scrollbar.thumb.background": "#e0def44c", "scrollbar.thumb.hover_background": "#232132ff", @@ -426,7 +426,7 @@ "tab.active_background": "#faf4edff", "search.match_background": "#9cced766", "panel.background": "#fef9f2ff", - "panel.focused_border": null, + "panel.focused_border": "#57949fff", "pane.focused_border": null, "scrollbar.thumb.background": "#5752794c", "scrollbar.thumb.hover_background": "#e5e0dfff", @@ -806,7 +806,7 @@ "tab.active_background": "#232136ff", "search.match_background": "#9cced766", "panel.background": "#28253cff", - "panel.focused_border": null, + "panel.focused_border": "#9bced6ff", "pane.focused_border": null, "scrollbar.thumb.background": "#e0def44c", "scrollbar.thumb.hover_background": "#322f48ff", diff --git a/assets/themes/sandcastle/sandcastle.json b/assets/themes/sandcastle/sandcastle.json index ba9e6f50fd..87030607dc 100644 --- a/assets/themes/sandcastle/sandcastle.json +++ b/assets/themes/sandcastle/sandcastle.json @@ -46,7 +46,7 @@ "tab.active_background": "#282c33ff", "search.match_background": "#528b8b66", "panel.background": "#2b3038ff", - "panel.focused_border": null, + "panel.focused_border": "#518b8bff", "pane.focused_border": null, "scrollbar.thumb.background": "#fdf4c14c", "scrollbar.thumb.hover_background": "#313741ff", diff --git a/assets/themes/solarized/solarized.json b/assets/themes/solarized/solarized.json index fe86793cdc..42341d6770 100644 --- a/assets/themes/solarized/solarized.json +++ b/assets/themes/solarized/solarized.json @@ -46,7 +46,7 @@ "tab.active_background": "#002a35ff", "search.match_background": "#288bd166", "panel.background": "#04313bff", - "panel.focused_border": null, + "panel.focused_border": "#278ad1ff", "pane.focused_border": null, "scrollbar.thumb.background": "#fdf6e34c", "scrollbar.thumb.hover_background": "#053541ff", @@ -416,7 +416,7 @@ "tab.active_background": "#fdf6e3ff", "search.match_background": "#298bd166", "panel.background": "#f3eddaff", - "panel.focused_border": null, + "panel.focused_border": "#288bd1ff", "pane.focused_border": null, "scrollbar.thumb.background": "#002a354c", "scrollbar.thumb.hover_background": "#dcdacbff", diff --git a/assets/themes/summercamp/summercamp.json b/assets/themes/summercamp/summercamp.json index c2206f9aab..0c5cfa0c6f 100644 --- a/assets/themes/summercamp/summercamp.json +++ b/assets/themes/summercamp/summercamp.json @@ -46,7 +46,7 @@ "tab.active_background": "#1b1810ff", "search.match_background": "#499bef66", "panel.background": "#231f16ff", - "panel.focused_border": null, + "panel.focused_border": "#499befff", "pane.focused_border": null, "scrollbar.thumb.background": "#f8f5de4c", "scrollbar.thumb.hover_background": "#29251bff", From e8f0ebc881dd3d686bf2ac8a6deb3611b2a67455 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Fri, 6 Dec 2024 09:17:48 -0300 Subject: [PATCH 314/886] Refine diagnostic icons in tabs (#21637) Follow up to https://github.com/zed-industries/zed/pull/21383 Mostly adjusting the alignment when there are no file icons. Screenshot 2024-12-06 at 08 35 48 Release Notes: - N/A --- assets/icons/triangle.svg | 4 ++-- assets/icons/x.svg | 4 ++-- crates/workspace/src/pane.rs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/assets/icons/triangle.svg b/assets/icons/triangle.svg index 8c44b91b78..0ecf071e24 100644 --- a/assets/icons/triangle.svg +++ b/assets/icons/triangle.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/icons/x.svg b/assets/icons/x.svg index d090cb55bf..5d91a9edd9 100644 --- a/assets/icons/x.svg +++ b/assets/icons/x.svg @@ -1,3 +1,3 @@ - - + + diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index c0a80cc943..8264cb2a4a 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -2145,7 +2145,7 @@ impl Pane { .child(if let Some(decorated_icon) = decorated_icon { div().child(decorated_icon.into_any_element()) } else if let Some(icon) = icon { - div().child(icon.into_any_element()) + div().mt(px(2.5)).child(icon.into_any_element()) } else { div() }) From 304158ed795a2f133d8b1a6859a59e2f810f5c98 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Fri, 6 Dec 2024 08:45:03 -0500 Subject: [PATCH 315/886] Catch panic from oo7 when reading credentials (#21617) --- crates/gpui/src/platform/linux/platform.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index a85052a4f0..d8bdcf1052 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -8,7 +8,7 @@ use std::fs::File; use std::io::Read; use std::ops::{Deref, DerefMut}; use std::os::fd::{AsFd, AsRawFd, FromRawFd}; -use std::panic::Location; +use std::panic::{AssertUnwindSafe, Location}; use std::rc::Weak; use std::{ path::{Path, PathBuf}, @@ -23,7 +23,7 @@ use async_task::Runnable; use calloop::channel::Channel; use calloop::{EventLoop, LoopHandle, LoopSignal}; use flume::{Receiver, Sender}; -use futures::channel::oneshot; +use futures::{channel::oneshot, future::FutureExt}; use parking_lot::Mutex; use util::ResultExt; @@ -489,7 +489,12 @@ impl Platform for P { let username = attributes .get("username") .ok_or_else(|| anyhow!("Cannot find username in stored credentials"))?; - let secret = item.secret().await?; + // oo7 panics if the retrieved secret can't be decrypted due to + // unexpected padding. + let secret = AssertUnwindSafe(item.secret()) + .catch_unwind() + .await + .map_err(|_| anyhow!("oo7 panicked while trying to read credentials"))??; // we lose the zeroizing capabilities at this boundary, // a current limitation GPUI's credentials api From e5251f40914dac87db247b2a03e2a49c0b696ad6 Mon Sep 17 00:00:00 2001 From: tims <0xtimsb@gmail.com> Date: Fri, 6 Dec 2024 22:33:58 +0530 Subject: [PATCH 316/886] Fix incorrect language selected in language selector (#21648) Due to filtering after enumeration, initial candidate ids are assigned incorrectly. This later causes the wrong item to be picked up when accessed via index in the vector. --- crates/language_selector/src/language_selector.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/language_selector/src/language_selector.rs b/crates/language_selector/src/language_selector.rs index 60da837baa..760a94000d 100644 --- a/crates/language_selector/src/language_selector.rs +++ b/crates/language_selector/src/language_selector.rs @@ -104,14 +104,15 @@ impl LanguageSelectorDelegate { let candidates = language_registry .language_names() .into_iter() - .enumerate() - .filter_map(|(candidate_id, name)| { + .filter_map(|name| { language_registry .available_language_for_name(&name)? .hidden() .not() - .then(|| StringMatchCandidate::new(candidate_id, name)) + .then_some(name) }) + .enumerate() + .map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, name)) .collect::>(); Self { From 9ca0d99cfd64ca186df735fc593b0f0bbd2bee05 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 6 Dec 2024 12:22:35 -0500 Subject: [PATCH 317/886] Update Rust crate ctor to v0.2.9 (#21561) 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 3167456349..e421917d8f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3480,9 +3480,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 bffdc55d63449ca1a80e649df6f8b35f20937b91 Mon Sep 17 00:00:00 2001 From: tims <0xtimsb@gmail.com> Date: Fri, 6 Dec 2024 22:56:47 +0530 Subject: [PATCH 318/886] linux: Make prompt detail selectable (#21405) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #21305 As Linux doesn’t have native prompts, Zed uses a custom GPU-based prompt, like the "About Zed" prompt. Currently, the detail in the prompt isn’t selectable. This PR fixes that by using the editor's multi-line selectable functionality to make the detail selectable (and thus copyable). It achieves this by disabling editing and setting the cursor to transparent. The editor also does all the heavy lifting, like double-clicking to select a word or triple-clicking to select a line, like what user expects from selectable. Before/After: before after When detail is `None` or empty string: none Release Notes: - N/A --- Cargo.lock | 1 + crates/zed/Cargo.toml | 1 + crates/zed/src/zed/linux_prompts.rs | 43 ++++++++++++++++++++--------- 3 files changed, 32 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e421917d8f..b93ebce571 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16083,6 +16083,7 @@ dependencies = [ "languages", "libc", "log", + "markdown", "markdown_preview", "menu", "mimalloc", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 6b26a01f27..9a672757a6 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -69,6 +69,7 @@ language_tools.workspace = true languages = { workspace = true, features = ["load-grammars"] } libc.workspace = true log.workspace = true +markdown.workspace = true markdown_preview.workspace = true menu.workspace = true mimalloc = { version = "0.1", optional = true } diff --git a/crates/zed/src/zed/linux_prompts.rs b/crates/zed/src/zed/linux_prompts.rs index 1961a5f9cd..aa262a11b9 100644 --- a/crates/zed/src/zed/linux_prompts.rs +++ b/crates/zed/src/zed/linux_prompts.rs @@ -1,13 +1,15 @@ use gpui::{ div, AppContext, EventEmitter, FocusHandle, FocusableView, FontWeight, InteractiveElement, - IntoElement, ParentElement, PromptHandle, PromptLevel, PromptResponse, Render, - RenderablePromptHandle, Styled, ViewContext, VisualContext, WindowContext, + IntoElement, ParentElement, PromptHandle, PromptLevel, PromptResponse, Refineable, Render, + RenderablePromptHandle, Styled, TextStyleRefinement, View, ViewContext, VisualContext, + WindowContext, }; +use markdown::{Markdown, MarkdownStyle}; use settings::Settings; use theme::ThemeSettings; use ui::{ - h_flex, v_flex, ButtonCommon, ButtonStyle, Clickable, ElevationIndex, FluentBuilder, LabelSize, - TintColor, + h_flex, v_flex, ActiveTheme, ButtonCommon, ButtonStyle, Clickable, ElevationIndex, + FluentBuilder, LabelSize, TintColor, }; use workspace::ui::StyledExt; @@ -28,10 +30,27 @@ pub fn fallback_prompt_renderer( |cx| FallbackPromptRenderer { _level: level, message: message.to_string(), - detail: detail.map(ToString::to_string), actions: actions.iter().map(ToString::to_string).collect(), focus: cx.focus_handle(), active_action_id: 0, + detail: detail.filter(|text| !text.is_empty()).map(|text| { + cx.new_view(|cx| { + let settings = ThemeSettings::get_global(cx); + let mut base_text_style = cx.text_style(); + base_text_style.refine(&TextStyleRefinement { + font_family: Some(settings.ui_font.family.clone()), + font_size: Some(settings.ui_font_size.into()), + color: Some(ui::Color::Muted.color(cx)), + ..Default::default() + }); + let markdown_style = MarkdownStyle { + base_text_style, + selection_background_color: { cx.theme().players().local().selection }, + ..Default::default() + }; + Markdown::new(text.to_string(), markdown_style, None, None, cx) + }) + }), } }); @@ -42,10 +61,10 @@ pub fn fallback_prompt_renderer( pub struct FallbackPromptRenderer { _level: PromptLevel, message: String, - detail: Option, actions: Vec, focus: FocusHandle, active_action_id: usize, + detail: Option>, } impl FallbackPromptRenderer { @@ -111,13 +130,11 @@ impl Render for FallbackPromptRenderer { .child(self.message.clone()) .text_color(ui::Color::Default.color(cx)), ) - .children(self.detail.clone().map(|detail| { - div() - .w_full() - .text_xs() - .text_color(ui::Color::Muted.color(cx)) - .child(detail) - })) + .children( + self.detail + .clone() + .map(|detail| div().w_full().text_xs().child(detail)), + ) .child(h_flex().justify_end().gap_2().children( self.actions.iter().enumerate().rev().map(|(ix, action)| { ui::Button::new(ix, action.clone()) From b4f59284a94d3af414f62824331e2641e9b055e8 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Fri, 6 Dec 2024 18:31:58 +0100 Subject: [PATCH 319/886] markdown preview: Allow clicking on image to navigate to source location (#21630) Follow up to #21082 Similar to checkboxes, you can now click on the image to navigate to the source location, cmd-clicking opens the url in the browser. https://github.com/user-attachments/assets/edaaa580-9d8f-490b-a4b3-d6ffb21f197c Release Notes: - N/A --- Cargo.lock | 1 + crates/markdown_preview/Cargo.toml | 1 + .../markdown_preview/src/markdown_renderer.rs | 98 ++++++++++++++----- 3 files changed, 78 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b93ebce571..477a7b83e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7421,6 +7421,7 @@ dependencies = [ "settings", "theme", "ui", + "util", "workspace", ] diff --git a/crates/markdown_preview/Cargo.toml b/crates/markdown_preview/Cargo.toml index 46a33966f2..f1409c23a4 100644 --- a/crates/markdown_preview/Cargo.toml +++ b/crates/markdown_preview/Cargo.toml @@ -28,6 +28,7 @@ pulldown-cmark.workspace = true settings.workspace = true theme.workspace = true ui.workspace = true +util.workspace = true workspace.workspace = true [dev-dependencies] diff --git a/crates/markdown_preview/src/markdown_renderer.rs b/crates/markdown_preview/src/markdown_renderer.rs index 7a13077194..5183f361b6 100644 --- a/crates/markdown_preview/src/markdown_renderer.rs +++ b/crates/markdown_preview/src/markdown_renderer.rs @@ -7,8 +7,8 @@ use crate::markdown_elements::{ use gpui::{ div, img, px, rems, AbsoluteLength, AnyElement, ClipboardItem, DefiniteLength, Div, Element, ElementId, HighlightStyle, Hsla, ImageSource, InteractiveText, IntoElement, Keystroke, Length, - Modifiers, ParentElement, Resource, SharedString, Styled, StyledText, TextStyle, WeakView, - WindowContext, + Modifiers, ParentElement, Render, Resource, SharedString, Styled, StyledText, TextStyle, View, + WeakView, WindowContext, }; use settings::Settings; use std::{ @@ -18,9 +18,10 @@ use std::{ }; use theme::{ActiveTheme, SyntaxTheme, ThemeSettings}; use ui::{ - h_flex, relative, v_flex, Checkbox, Clickable, FluentBuilder, IconButton, IconName, IconSize, - InteractiveElement, LinkPreview, Selection, StatefulInteractiveElement, StyledExt, StyledImage, - Tooltip, VisibleOnHover, + h_flex, relative, tooltip_container, v_flex, Checkbox, Clickable, Color, FluentBuilder, + IconButton, IconName, IconSize, InteractiveElement, Label, LabelCommon, LabelSize, LinkPreview, + Selection, StatefulInteractiveElement, StyledExt, StyledImage, ViewContext, VisibleOnHover, + VisualContext as _, }; use workspace::Workspace; @@ -206,15 +207,7 @@ fn render_markdown_list_item( ) .hover(|s| s.cursor_pointer()) .tooltip(|cx| { - let secondary_modifier = Keystroke { - key: "".to_string(), - modifiers: Modifiers::secondary_key(), - key_char: None, - }; - Tooltip::text( - format!("{}-click to toggle the checkbox", secondary_modifier), - cx, - ) + InteractiveMarkdownElementTooltip::new(None, "toggle checkbox", cx).into() }) .into_any_element(), }; @@ -513,6 +506,7 @@ fn render_markdown_text(parsed_new: &MarkdownParagraph, cx: &mut RenderContext) let image_element = div() .id(element_id) + .cursor_pointer() .child(img(ImageSource::Resource(image_resource)).with_fallback({ let alt_text = image.alt_text.clone(); { @@ -521,18 +515,31 @@ fn render_markdown_text(parsed_new: &MarkdownParagraph, cx: &mut RenderContext) })) .tooltip({ let link = image.link.clone(); - move |cx| LinkPreview::new(&link.to_string(), cx) + move |cx| { + InteractiveMarkdownElementTooltip::new( + Some(link.to_string()), + "open image", + cx, + ) + .into() + } }) .on_click({ let workspace = workspace_clone.clone(); let link = image.link.clone(); - move |_event, window_cx| match &link { - Link::Web { url } => window_cx.open_url(url), - Link::Path { path, .. } => { - if let Some(workspace) = &workspace { - _ = workspace.update(window_cx, |workspace, cx| { - workspace.open_abs_path(path.clone(), false, cx).detach(); - }); + move |_, cx| { + if cx.modifiers().secondary() { + match &link { + Link::Web { url } => cx.open_url(url), + Link::Path { path, .. } => { + if let Some(workspace) = &workspace { + _ = workspace.update(cx, |workspace, cx| { + workspace + .open_abs_path(path.clone(), false, cx) + .detach(); + }); + } + } } } } @@ -550,3 +557,50 @@ fn render_markdown_rule(cx: &mut RenderContext) -> AnyElement { let rule = div().w_full().h(px(2.)).bg(cx.border_color); div().pt_3().pb_3().child(rule).into_any() } + +struct InteractiveMarkdownElementTooltip { + tooltip_text: Option, + action_text: String, +} + +impl InteractiveMarkdownElementTooltip { + pub fn new( + tooltip_text: Option, + action_text: &str, + cx: &mut WindowContext, + ) -> View { + let tooltip_text = tooltip_text.map(|t| util::truncate_and_trailoff(&t, 50).into()); + + cx.new_view(|_| Self { + tooltip_text, + action_text: action_text.to_string(), + }) + } +} + +impl Render for InteractiveMarkdownElementTooltip { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + tooltip_container(cx, |el, _| { + let secondary_modifier = Keystroke { + modifiers: Modifiers::secondary_key(), + ..Default::default() + }; + + el.child( + v_flex() + .gap_1() + .when_some(self.tooltip_text.clone(), |this, text| { + this.child(Label::new(text).size(LabelSize::Small)) + }) + .child( + Label::new(format!( + "{}-click to {}", + secondary_modifier, self.action_text + )) + .size(LabelSize::Small) + .color(Color::Muted), + ), + ) + }) + } +} From 8a6c2bb74936c6b28755576e2ad1484b2bb17bb1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 6 Dec 2024 12:32:45 -0500 Subject: [PATCH 320/886] Update Rust crate rsa to v0.9.7 (#21570) 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 | |---|---|---|---| | [rsa](https://redirect.github.com/RustCrypto/RSA) | workspace.dependencies | patch | `0.9.6` -> `0.9.7` | --- ### Release Notes
RustCrypto/RSA (rsa) ### [`v0.9.7`](https://redirect.github.com/RustCrypto/RSA/compare/v0.9.6...v0.9.7) [Compare Source](https://redirect.github.com/RustCrypto/RSA/compare/v0.9.6...v0.9.7)
--- ### 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 477a7b83e2..2c7f3d0498 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10694,9 +10694,9 @@ dependencies = [ [[package]] name = "rsa" -version = "0.9.6" +version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +checksum = "47c75d7c5c6b673e58bf54d8544a9f432e3a925b0e80f7cd3602ab5c50c55519" dependencies = [ "const-oid", "digest", From d6e11c58db0f894994d40e9bcd2cad4370a0120b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 6 Dec 2024 12:33:33 -0500 Subject: [PATCH 321/886] Update Rust crate pathdiff to v0.2.3 (#21568) 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 | |---|---|---|---| | [pathdiff](https://redirect.github.com/Manishearth/pathdiff) | workspace.dependencies | patch | `0.2.2` -> `0.2.3` | --- ### 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 2c7f3d0498..b17b55957d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8578,9 +8578,9 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pathdiff" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d61c5ce1153ab5b689d0c074c4e7fc613e942dfb7dd9eea5ab202d2ad91fe361" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" [[package]] name = "pathfinder_geometry" From feb2d85a135292435a76fcd45ac1085f29abfdf7 Mon Sep 17 00:00:00 2001 From: uncenter <47499684+uncenter@users.noreply.github.com> Date: Fri, 6 Dec 2024 12:34:15 -0500 Subject: [PATCH 322/886] Add YAML/TOML frontmatter injections for markdown (#21503) Closes #7938. Adds front-matter injections for TOML/YAML in markdown. - See: https://github.com/tree-sitter-grammars/tree-sitter-markdown/blob/split_parser/tree-sitter-markdown/queries/injections.scm. Co-authored-by: Peter Tripp --- crates/languages/src/markdown/injections.scm | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/languages/src/markdown/injections.scm b/crates/languages/src/markdown/injections.scm index 5972a43eb1..b2c35642e5 100644 --- a/crates/languages/src/markdown/injections.scm +++ b/crates/languages/src/markdown/injections.scm @@ -8,3 +8,7 @@ ((html_block) @content (#set! "language" "html")) + +((minus_metadata) @content (#set! "language" "yaml")) + +((plus_metadata) @content (#set! "language" "toml")) From 99c31816c9c86fe998641c20e2cebcff2a1b0411 Mon Sep 17 00:00:00 2001 From: Jax Young Date: Sat, 7 Dec 2024 01:47:05 +0800 Subject: [PATCH 323/886] docs: Correct default values (#20897) Some default values in the doc are outdated. Release Notes: - N/A --------- Co-authored-by: Marshall Bowers --- docs/src/vim.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/src/vim.md b/docs/src/vim.md index a350fb7773..4f87c649ef 100644 --- a/docs/src/vim.md +++ b/docs/src/vim.md @@ -473,15 +473,15 @@ Here's an example of these settings changed: Here are a few general Zed settings that can help you fine-tune your Vim experience: -| Property | Description | Default Value | -| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | -| cursor_blink | If `true`, the cursor blinks. | `true` | -| relative_line_numbers | If `true`, line numbers in the left gutter are relative to the cursor. | `false` | -| scrollbar | Object that controls the scrollbar display. Set to `{ "show": "never" }` to hide the scroll bar. | `{ "show": "always" }` | -| scroll_beyond_last_line | If set to `"one_page"`, allows scrolling up to one page beyond the last line. Set to `"off"` to prevent this behavior. | `"one_page"` | -| vertical_scroll_margin | The number of lines to keep above or below the cursor when scrolling. Set to `0` to allow the cursor to go up to the edges of the screen vertically. | `3` | -| gutter.line_numbers | Controls the display of line numbers in the gutter. Set the `"line_numbers"` property to `false` to hide line numbers. | `true` | -| command_aliases | Object that defines aliases for commands in the command palette. You can use it to define shortcut names for commands you use often. Read below for examples. | `{}` | +| Property | Description | Default Value | +| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | +| cursor_blink | If `true`, the cursor blinks. | `true` | +| relative_line_numbers | If `true`, line numbers in the left gutter are relative to the cursor. | `false` | +| scrollbar | Object that controls the scrollbar display. Set to `{ "show": "never" }` to hide the scroll bar. | `{ "show": "auto" }` | +| scroll_beyond_last_line | If set to `"one_page"`, allows scrolling up to one page beyond the last line. Set to `"off"` to prevent this behavior. | `"one_page"` | +| vertical_scroll_margin | The number of lines to keep above or below the cursor when scrolling. Set to `0` to allow the cursor to go up to the edges of the screen vertically. | `3` | +| gutter.line_numbers | Controls the display of line numbers in the gutter. Set the `"line_numbers"` property to `false` to hide line numbers. | `true` | +| command_aliases | Object that defines aliases for commands in the command palette. You can use it to define shortcut names for commands you use often. Read below for examples. | `{}` | Here's an example of these settings changed: From 0368fff030117fc99edc6cc4b78478d62a8623d1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 6 Dec 2024 13:16:53 -0500 Subject: [PATCH 324/886] Update cloudflare/wrangler-action digest to 6d58852 (#21551) 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 | |---|---|---|---| | [cloudflare/wrangler-action](https://redirect.github.com/cloudflare/wrangler-action) | action | digest | `05f17c4` -> `6d58852` | --- ### 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> --- .github/workflows/deploy_cloudflare.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy_cloudflare.yml b/.github/workflows/deploy_cloudflare.yml index d6daada6e3..6cc4ea0a33 100644 --- a/.github/workflows/deploy_cloudflare.yml +++ b/.github/workflows/deploy_cloudflare.yml @@ -37,28 +37,28 @@ jobs: mdbook build ./docs --dest-dir=../target/deploy/docs/ - name: Deploy Docs - uses: cloudflare/wrangler-action@05f17c4a695b4d94b57b59997562c6a4624c64e4 # v3 + uses: cloudflare/wrangler-action@6d58852c35a27e6034745c5d0bc373d739014f7f # v3 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} command: pages deploy target/deploy --project-name=docs - name: Deploy Install - uses: cloudflare/wrangler-action@05f17c4a695b4d94b57b59997562c6a4624c64e4 # v3 + uses: cloudflare/wrangler-action@6d58852c35a27e6034745c5d0bc373d739014f7f # v3 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} command: r2 object put -f script/install.sh zed-open-source-website-assets/install.sh - name: Deploy Docs Workers - uses: cloudflare/wrangler-action@05f17c4a695b4d94b57b59997562c6a4624c64e4 # v3 + uses: cloudflare/wrangler-action@6d58852c35a27e6034745c5d0bc373d739014f7f # v3 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} command: deploy .cloudflare/docs-proxy/src/worker.js - name: Deploy Install Workers - uses: cloudflare/wrangler-action@05f17c4a695b4d94b57b59997562c6a4624c64e4 # v3 + uses: cloudflare/wrangler-action@6d58852c35a27e6034745c5d0bc373d739014f7f # v3 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} From 7a1a7929bd245ebcd206edf575299cf8c94ad41f Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Fri, 6 Dec 2024 18:59:40 +0000 Subject: [PATCH 325/886] docs: Add x.ai Grok example (#21655) - Closes https://github.com/zed-industries/zed/issues/21635 Screenshot 2024-12-06 at 13 57 42 Release Notes: - Document support for x.ai Grok --- docs/src/assistant/configuration.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/src/assistant/configuration.md b/docs/src/assistant/configuration.md index 2145bd9504..8e558007bf 100644 --- a/docs/src/assistant/configuration.md +++ b/docs/src/assistant/configuration.md @@ -192,6 +192,30 @@ The Zed Assistant comes pre-configured to use the latest version for common mode You must provide the model's Context Window in the `max_tokens` parameter, this can be found [OpenAI Model Docs](https://platform.openai.com/docs/models). OpenAI `o1` models should set `max_completion_tokens` as well to avoid incurring high reasoning token costs. Custom models will be listed in the model dropdown in the assistant panel. +### OpenAI API Compatible + +Zed supports using OpenAI compatible APIs by specifying a custom `endpoint` and `available_models` for the OpenAI provider. + +#### X.ai Grok + +Example configuration for using X.ai Grok with Zed: + +```json + "language_models": { + "openai": { + "api_url": "https://api.x.ai/v1", + "available_models": [ + { + "name": "grok-beta", + "display_name": "X.ai Grok (Beta)", + "max_tokens": 131072 + } + ], + "version": "1" + }, + } +``` + ### Advanced configuration {#advanced-configuration} #### Example Configuration From 5142e38d2ba546da1be7c90de630cdd8978f46cb Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 6 Dec 2024 14:32:09 -0500 Subject: [PATCH 326/886] editor: Add actions for inserting UUIDs (#21656) This PR adds two new actions for generating and inserting UUIDs into the buffer: https://github.com/user-attachments/assets/a3445a98-07e2-40b8-9773-fd750706cbcc Release Notes: - Added `editor: insert uuid v4` and `editor: insert uuid v7` actions for inserting generated UUIDs into the editor. --- Cargo.lock | 1 + Cargo.toml | 2 +- crates/editor/Cargo.toml | 1 + crates/editor/src/actions.rs | 10 ++++++++++ crates/editor/src/editor.rs | 27 +++++++++++++++++++++++++++ crates/editor/src/element.rs | 2 ++ 6 files changed, 42 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index b17b55957d..0993089333 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3957,6 +3957,7 @@ dependencies = [ "unindent", "url", "util", + "uuid", "workspace", ] diff --git a/Cargo.toml b/Cargo.toml index a21a65c8fe..7ff0ad6ce3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -506,7 +506,7 @@ unindent = "0.1.7" unicode-segmentation = "1.10" unicode-script = "0.5.7" url = "2.2" -uuid = { version = "1.1.2", features = ["v4", "v5", "serde"] } +uuid = { version = "1.1.2", features = ["v4", "v5", "v7", "serde"] } wasmparser = "0.215" wasm-encoder = "0.215" wasmtime = { version = "24", default-features = false, features = [ diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index 166e7383fc..a728ea86a2 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -85,6 +85,7 @@ unindent = { workspace = true, optional = true } ui.workspace = true url.workspace = true util.workspace = true +uuid.workspace = true workspace.workspace = true [dev-dependencies] diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 99e7c6cd0b..eb0fcaa1e5 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -105,6 +105,7 @@ pub struct MoveDownByLines { #[serde(default)] pub(super) lines: u32, } + #[derive(PartialEq, Clone, Deserialize, Default)] pub struct SelectUpByLines { #[serde(default)] @@ -166,6 +167,13 @@ pub struct SpawnNearestTask { pub reveal: task::RevealStrategy, } +#[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize, Default)] +pub enum UuidVersion { + #[default] + V4, + V7, +} + impl_actions!( editor, [ @@ -271,6 +279,8 @@ gpui::actions!( HalfPageUp, Hover, Indent, + InsertUuidV4, + InsertUuidV7, JoinLines, KillRingCut, KillRingYank, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 132a5e04fb..0bd30465d9 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -12004,6 +12004,33 @@ impl Editor { .detach(); } + pub fn insert_uuid_v4(&mut self, _: &InsertUuidV4, cx: &mut ViewContext) { + self.insert_uuid(UuidVersion::V4, cx); + } + + pub fn insert_uuid_v7(&mut self, _: &InsertUuidV7, cx: &mut ViewContext) { + self.insert_uuid(UuidVersion::V7, cx); + } + + fn insert_uuid(&mut self, version: UuidVersion, cx: &mut ViewContext) { + self.transact(cx, |this, cx| { + let edits = this + .selections + .all::(cx) + .into_iter() + .map(|selection| { + let uuid = match version { + UuidVersion::V4 => uuid::Uuid::new_v4(), + UuidVersion::V7 => uuid::Uuid::now_v7(), + }; + + (selection.range(), uuid.to_string()) + }); + this.edit(edits, cx); + this.refresh_inline_completion(true, false, cx); + }); + } + /// Adds a row highlight for the given range. If a row has multiple highlights, the /// last highlight added will be used. /// diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 198ecf6826..2df6d66b6a 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -456,6 +456,8 @@ impl EditorElement { register_action(view, cx, Editor::open_active_item_in_terminal); register_action(view, cx, Editor::reload_file); register_action(view, cx, Editor::spawn_nearest_task); + register_action(view, cx, Editor::insert_uuid_v4); + register_action(view, cx, Editor::insert_uuid_v7); } fn register_key_listeners(&self, cx: &mut WindowContext, layout: &EditorLayout) { From e730a9d029ea6eebfe40741b8f9131c2cdc3768f Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 6 Dec 2024 13:06:55 -0700 Subject: [PATCH 327/886] Bump to livekit 1.1.6 (#21660) Co-Authored-By: Mikayla This bumps to the latest v1 version of swift SDK. We could bump to 2, but it sounds like this will already have some race condition fixes (and a click around locally seems less prone to deadlocking so far...) Release Notes: - N/A --- .../livekit_client_macos/LiveKitBridge/Package.resolved | 8 ++++---- crates/livekit_client_macos/LiveKitBridge/Package.swift | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/livekit_client_macos/LiveKitBridge/Package.resolved b/crates/livekit_client_macos/LiveKitBridge/Package.resolved index b925bc8f0d..c84933e5c1 100644 --- a/crates/livekit_client_macos/LiveKitBridge/Package.resolved +++ b/crates/livekit_client_macos/LiveKitBridge/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/livekit/client-sdk-swift.git", "state": { "branch": null, - "revision": "7331b813a5ab8a95cfb81fb2b4ed10519428b9ff", - "version": "1.0.12" + "revision": "8cde9e66ce9b470c3a743f5c72784f57c5a6d0c3", + "version": "1.1.6" } }, { @@ -24,8 +24,8 @@ "repositoryURL": "https://github.com/webrtc-sdk/Specs.git", "state": { "branch": null, - "revision": "2f6bab30c8df0fe59ab3e58bc99097f757f85f65", - "version": "104.5112.17" + "revision": "4fa8d6d647fc759cdd0265fd413d2f28ea2e0e08", + "version": "114.5735.8" } }, { diff --git a/crates/livekit_client_macos/LiveKitBridge/Package.swift b/crates/livekit_client_macos/LiveKitBridge/Package.swift index d7b5c271b9..a2a5b3eb75 100644 --- a/crates/livekit_client_macos/LiveKitBridge/Package.swift +++ b/crates/livekit_client_macos/LiveKitBridge/Package.swift @@ -12,16 +12,16 @@ let package = Package( .library( name: "LiveKitBridge", type: .static, - targets: ["LiveKitBridge"]), + targets: ["LiveKitBridge"]) ], dependencies: [ - .package(url: "https://github.com/livekit/client-sdk-swift.git", .exact("1.0.12")), + .package(url: "https://github.com/livekit/client-sdk-swift.git", .exact("1.1.6")) ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. // Targets can depend on other targets in this package, and on products in packages this package depends on. .target( name: "LiveKitBridge", - dependencies: [.product(name: "LiveKit", package: "client-sdk-swift")]), + dependencies: [.product(name: "LiveKit", package: "client-sdk-swift")]) ] ) From 17448f23a68862763c7be3f4a95cff24e135ce69 Mon Sep 17 00:00:00 2001 From: The Bearodactyl Date: Fri, 6 Dec 2024 14:19:36 -0600 Subject: [PATCH 328/886] docs: Add clarification in Windows build instructions (#21659) --- docs/src/development/windows.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/src/development/windows.md b/docs/src/development/windows.md index 9cb539366d..4d1e565a57 100644 --- a/docs/src/development/windows.md +++ b/docs/src/development/windows.md @@ -139,3 +139,5 @@ New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" -Name ``` For more information on this, please see [win32 docs](https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=powershell) + +(note that you will need to restart your system after enabling longpath support) From 78ca297282036b3dbaec52cf9eaed5a709a4871f Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 6 Dec 2024 14:05:03 -0700 Subject: [PATCH 329/886] Make use_key_equivalents opt-in (#21662) When revamping international keyboard shortcuts I wanted to make the default to use key equivalents; in hindsight, this is not what people expect. Release Notes: - (Breaking) In keymap.json `"use_layout_keys": true` is now the default. If you want to opt-out of this behaviour, set `"use_key_equivalents": true` to have keys mapped for your keyboard. See [documentation](https://zed.dev/docs/key-bindings#non-qwerty-keyboards) --------- Co-authored-by: Peter Tripp --- assets/keymaps/default-macos.json | 51 ++++++++++++++++++++++++++++++ assets/keymaps/vim.json | 28 ---------------- crates/settings/src/keymap_file.rs | 10 +++--- docs/src/key-bindings.md | 9 ++---- 4 files changed, 58 insertions(+), 40 deletions(-) diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 65389230ac..33c32035d9 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -1,6 +1,7 @@ [ // Standard macOS bindings { + "use_key_equivalents": true, "bindings": { "up": "menu::SelectPrev", "shift-tab": "menu::SelectPrev", @@ -40,6 +41,7 @@ }, { "context": "Editor", + "use_key_equivalents": true, "bindings": { "escape": "editor::Cancel", "backspace": "editor::Backspace", @@ -131,6 +133,7 @@ }, { "context": "Editor && mode == full", + "use_key_equivalents": true, "bindings": { "enter": "editor::Newline", "shift-enter": "editor::Newline", @@ -148,6 +151,7 @@ }, { "context": "Editor && mode == full && inline_completion", + "use_key_equivalents": true, "bindings": { "alt-]": "editor::NextInlineCompletion", "alt-[": "editor::PreviousInlineCompletion", @@ -156,12 +160,14 @@ }, { "context": "Editor && !inline_completion", + "use_key_equivalents": true, "bindings": { "alt-\\": "editor::ShowInlineCompletion" } }, { "context": "Editor && mode == auto_height", + "use_key_equivalents": true, "bindings": { "ctrl-enter": "editor::Newline", "shift-enter": "editor::Newline", @@ -170,12 +176,14 @@ }, { "context": "Markdown", + "use_key_equivalents": true, "bindings": { "cmd-c": "markdown::Copy" } }, { "context": "Editor && jupyter && !ContextEditor", + "use_key_equivalents": true, "bindings": { "ctrl-shift-enter": "repl::Run", "ctrl-alt-enter": "repl::RunInPlace" @@ -183,6 +191,7 @@ }, { "context": "AssistantPanel", + "use_key_equivalents": true, "bindings": { "cmd-k c": "assistant::CopyCode", "cmd-g": "search::SelectNextMatch", @@ -195,6 +204,7 @@ }, { "context": "ContextEditor > Editor", + "use_key_equivalents": true, "bindings": { "cmd-enter": "assistant::Assist", "cmd-shift-enter": "assistant::Edit", @@ -209,6 +219,7 @@ }, { "context": "AssistantPanel2", + "use_key_equivalents": true, "bindings": { "cmd-n": "assistant2::NewThread", "cmd-shift-h": "assistant2::OpenHistory" @@ -216,12 +227,14 @@ }, { "context": "MessageEditor > Editor", + "use_key_equivalents": true, "bindings": { "cmd-enter": "assistant2::Chat" } }, { "context": "PromptLibrary", + "use_key_equivalents": true, "bindings": { "cmd-n": "prompt_library::NewPrompt", "cmd-shift-s": "prompt_library::ToggleDefaultPrompt", @@ -230,6 +243,7 @@ }, { "context": "BufferSearchBar", + "use_key_equivalents": true, "bindings": { "escape": "buffer_search::Dismiss", "tab": "buffer_search::FocusEditor", @@ -243,6 +257,7 @@ }, { "context": "BufferSearchBar && in_replace > Editor", + "use_key_equivalents": true, "bindings": { "enter": "search::ReplaceNext", "cmd-enter": "search::ReplaceAll" @@ -250,6 +265,7 @@ }, { "context": "BufferSearchBar && !in_replace > Editor", + "use_key_equivalents": true, "bindings": { "up": "search::PreviousHistoryQuery", "down": "search::NextHistoryQuery" @@ -257,6 +273,7 @@ }, { "context": "ProjectSearchBar", + "use_key_equivalents": true, "bindings": { "escape": "project_search::ToggleFocus", "cmd-shift-j": "project_search::ToggleFilters", @@ -268,6 +285,7 @@ }, { "context": "ProjectSearchBar > Editor", + "use_key_equivalents": true, "bindings": { "up": "search::PreviousHistoryQuery", "down": "search::NextHistoryQuery" @@ -275,6 +293,7 @@ }, { "context": "ProjectSearchBar && in_replace > Editor", + "use_key_equivalents": true, "bindings": { "enter": "search::ReplaceNext", "cmd-enter": "search::ReplaceAll" @@ -282,6 +301,7 @@ }, { "context": "ProjectSearchView", + "use_key_equivalents": true, "bindings": { "escape": "project_search::ToggleFocus", "cmd-shift-j": "project_search::ToggleFilters", @@ -292,6 +312,7 @@ }, { "context": "Pane", + "use_key_equivalents": true, "bindings": { "cmd-{": "pane::ActivatePrevItem", "cmd-}": "pane::ActivateNextItem", @@ -320,6 +341,7 @@ // Bindings from VS Code { "context": "Editor", + "use_key_equivalents": true, "bindings": { "cmd-[": "editor::Outdent", "cmd-]": "editor::Indent", @@ -383,6 +405,7 @@ }, { "context": "Editor && mode == full", + "use_key_equivalents": true, "bindings": { "cmd-shift-o": "outline::Toggle", "ctrl-g": "go_to_line::Toggle" @@ -390,6 +413,7 @@ }, { "context": "Pane", + "use_key_equivalents": true, "bindings": { "ctrl-1": ["pane::ActivateItem", 0], "ctrl-2": ["pane::ActivateItem", 1], @@ -409,6 +433,7 @@ }, { "context": "Workspace", + "use_key_equivalents": true, "bindings": { // Change the default action on `menu::Confirm` by setting the parameter // "alt-cmd-o": ["projects::OpenRecent", {"create_new_window": true }], @@ -464,6 +489,7 @@ }, { "context": "Workspace && !Terminal", + "use_key_equivalents": true, "bindings": { "cmd-shift-r": "task::Spawn", "cmd-alt-r": "task::Rerun", @@ -474,6 +500,7 @@ // Bindings from Sublime Text { "context": "Editor", + "use_key_equivalents": true, "bindings": { "ctrl-j": "editor::JoinLines", "ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart", @@ -493,6 +520,7 @@ // Bindings from Atom { "context": "Pane", + "use_key_equivalents": true, "bindings": { "cmd-k up": "pane::SplitUp", "cmd-k down": "pane::SplitDown", @@ -503,12 +531,14 @@ // Bindings that should be unified with bindings for more general actions { "context": "Editor && renaming", + "use_key_equivalents": true, "bindings": { "enter": "editor::ConfirmRename" } }, { "context": "Editor && showing_completions", + "use_key_equivalents": true, "bindings": { "enter": "editor::ConfirmCompletion", "tab": "editor::ComposeCompletion" @@ -516,18 +546,21 @@ }, { "context": "Editor && inline_completion && !showing_completions", + "use_key_equivalents": true, "bindings": { "tab": "editor::AcceptInlineCompletion" } }, { "context": "Editor && showing_code_actions", + "use_key_equivalents": true, "bindings": { "enter": "editor::ConfirmCodeAction" } }, { "context": "Editor && (showing_code_actions || showing_completions)", + "use_key_equivalents": true, "bindings": { "up": "editor::ContextMenuPrev", "ctrl-p": "editor::ContextMenuPrev", @@ -539,6 +572,7 @@ }, // Custom bindings { + "use_key_equivalents": true, "bindings": { "ctrl-alt-cmd-f": "workspace::FollowNextCollaborator", // TODO: Move this to a dock open action @@ -549,6 +583,7 @@ }, { "context": "Editor && mode == full", + "use_key_equivalents": true, "bindings": { "alt-enter": "editor::OpenExcerpts", "shift-enter": "editor::ExpandExcerpts", @@ -560,6 +595,7 @@ }, { "context": "ProposedChangesEditor", + "use_key_equivalents": true, "bindings": { "cmd-shift-y": "editor::ApplyDiffHunk", "cmd-shift-a": "editor::ApplyAllDiffHunks" @@ -567,6 +603,7 @@ }, { "context": "PromptEditor", + "use_key_equivalents": true, "bindings": { "ctrl-[": "assistant::CyclePreviousInlineAssist", "ctrl-]": "assistant::CycleNextInlineAssist" @@ -574,12 +611,14 @@ }, { "context": "ProjectSearchBar && !in_replace", + "use_key_equivalents": true, "bindings": { "cmd-enter": "project_search::SearchInNew" } }, { "context": "OutlinePanel && not_editing", + "use_key_equivalents": true, "bindings": { "escape": "menu::Cancel", "left": "outline_panel::CollapseSelectedEntry", @@ -596,6 +635,7 @@ }, { "context": "ProjectPanel", + "use_key_equivalents": true, "bindings": { "left": "project_panel::CollapseSelectedEntry", "right": "project_panel::ExpandSelectedEntry", @@ -625,12 +665,14 @@ }, { "context": "ProjectPanel && not_editing", + "use_key_equivalents": true, "bindings": { "space": "project_panel::Open" } }, { "context": "CollabPanel && not_editing", + "use_key_equivalents": true, "bindings": { "ctrl-backspace": "collab_panel::Remove", "space": "menu::Confirm" @@ -638,18 +680,21 @@ }, { "context": "(CollabPanel && editing) > Editor", + "use_key_equivalents": true, "bindings": { "space": "collab_panel::InsertSpace" } }, { "context": "ChannelModal", + "use_key_equivalents": true, "bindings": { "tab": "channel_modal::ToggleMode" } }, { "context": "Picker > Editor", + "use_key_equivalents": true, "bindings": { "tab": "picker::ConfirmCompletion", "alt-enter": ["picker::ConfirmInput", { "secondary": false }], @@ -658,18 +703,21 @@ }, { "context": "ChannelModal > Picker > Editor", + "use_key_equivalents": true, "bindings": { "tab": "channel_modal::ToggleMode" } }, { "context": "FileFinder", + "use_key_equivalents": true, "bindings": { "cmd": "file_finder::ToggleMenu" } }, { "context": "FileFinder && !menu_open", + "use_key_equivalents": true, "bindings": { "cmd-shift-p": "file_finder::SelectPrev", "cmd-j": "pane::SplitDown", @@ -680,6 +728,7 @@ }, { "context": "FileFinder && menu_open", + "use_key_equivalents": true, "bindings": { "j": "pane::SplitDown", "k": "pane::SplitUp", @@ -689,6 +738,7 @@ }, { "context": "TabSwitcher", + "use_key_equivalents": true, "bindings": { "ctrl-up": "menu::SelectPrev", "ctrl-down": "menu::SelectNext", @@ -698,6 +748,7 @@ }, { "context": "Terminal", + "use_key_equivalents": true, "bindings": { "ctrl-cmd-space": "terminal::ShowCharacterPalette", "cmd-c": "terminal::Copy", diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 8931ad0dca..9328b0325f 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -1,7 +1,6 @@ [ { "context": "VimControl && !menu", - "use_layout_keys": true, "bindings": { "i": ["vim::PushOperator", { "Object": { "around": false } }], "a": ["vim::PushOperator", { "Object": { "around": true } }], @@ -188,7 +187,6 @@ }, { "context": "vim_mode == normal", - "use_layout_keys": true, "bindings": { "escape": "editor::Cancel", "ctrl-[": "editor::Cancel", @@ -243,7 +241,6 @@ }, { "context": "VimControl && VimCount", - "use_layout_keys": true, "bindings": { "0": ["vim::Number", 0], ":": "vim::CountCommand" @@ -251,7 +248,6 @@ }, { "context": "vim_mode == visual", - "use_layout_keys": true, "bindings": { ":": "vim::VisualCommand", "u": "vim::ConvertToLowerCase", @@ -301,7 +297,6 @@ }, { "context": "vim_mode == insert", - "use_layout_keys": true, "bindings": { "escape": "vim::NormalBefore", "ctrl-c": "vim::NormalBefore", @@ -344,7 +339,6 @@ { "context": "vim_mode == insert && !(showing_code_actions || showing_completions)", - "use_layout_keys": true, "bindings": { "ctrl-p": "editor::ShowCompletions", "ctrl-n": "editor::ShowCompletions" @@ -352,7 +346,6 @@ }, { "context": "vim_mode == replace", - "use_layout_keys": true, "bindings": { "escape": "vim::NormalBefore", "ctrl-c": "vim::NormalBefore", @@ -370,7 +363,6 @@ }, { "context": "vim_mode == waiting", - "use_layout_keys": true, "bindings": { "tab": "vim::Tab", "enter": "vim::Enter", @@ -384,7 +376,6 @@ }, { "context": "vim_mode == operator", - "use_layout_keys": true, "bindings": { "escape": "vim::ClearOperators", "ctrl-c": "vim::ClearOperators", @@ -394,7 +385,6 @@ }, { "context": "vim_operator == a || vim_operator == i || vim_operator == cs", - "use_layout_keys": true, "bindings": { "w": "vim::Word", "shift-w": ["vim::Word", { "ignorePunctuation": true }], @@ -425,7 +415,6 @@ }, { "context": "vim_operator == c", - "use_layout_keys": true, "bindings": { "c": "vim::CurrentLine", "d": "editor::Rename", // zed specific @@ -434,7 +423,6 @@ }, { "context": "vim_operator == d", - "use_layout_keys": true, "bindings": { "d": "vim::CurrentLine", "s": ["vim::PushOperator", "DeleteSurrounds"], @@ -444,7 +432,6 @@ }, { "context": "vim_operator == gu", - "use_layout_keys": true, "bindings": { "g u": "vim::CurrentLine", "u": "vim::CurrentLine" @@ -452,7 +439,6 @@ }, { "context": "vim_operator == gU", - "use_layout_keys": true, "bindings": { "g shift-u": "vim::CurrentLine", "shift-u": "vim::CurrentLine" @@ -460,7 +446,6 @@ }, { "context": "vim_operator == g~", - "use_layout_keys": true, "bindings": { "g ~": "vim::CurrentLine", "~": "vim::CurrentLine" @@ -468,7 +453,6 @@ }, { "context": "vim_operator == gq", - "use_layout_keys": true, "bindings": { "g q": "vim::CurrentLine", "q": "vim::CurrentLine", @@ -478,7 +462,6 @@ }, { "context": "vim_operator == y", - "use_layout_keys": true, "bindings": { "y": "vim::CurrentLine", "s": ["vim::PushOperator", { "AddSurrounds": {} }] @@ -486,42 +469,36 @@ }, { "context": "vim_operator == ys", - "use_layout_keys": true, "bindings": { "s": "vim::CurrentLine" } }, { "context": "vim_operator == >", - "use_layout_keys": true, "bindings": { ">": "vim::CurrentLine" } }, { "context": "vim_operator == <", - "use_layout_keys": true, "bindings": { "<": "vim::CurrentLine" } }, { "context": "vim_operator == eq", - "use_layout_keys": true, "bindings": { "=": "vim::CurrentLine" } }, { "context": "vim_operator == gc", - "use_layout_keys": true, "bindings": { "c": "vim::CurrentLine" } }, { "context": "vim_mode == literal", - "use_layout_keys": true, "bindings": { "ctrl-@": ["vim::Literal", ["ctrl-@", "\u0000"]], "ctrl-a": ["vim::Literal", ["ctrl-a", "\u0001"]], @@ -565,7 +542,6 @@ }, { "context": "BufferSearchBar && !in_replace", - "use_layout_keys": true, "bindings": { "enter": "vim::SearchSubmit", "escape": "buffer_search::Dismiss" @@ -573,7 +549,6 @@ }, { "context": "ProjectPanel || CollabPanel || OutlinePanel || ChatPanel || VimControl || EmptyPane || SharedScreen || MarkdownPreview || KeyContextView", - "use_layout_keys": true, "bindings": { // window related commands (ctrl-w X) "ctrl-w": null, @@ -630,7 +605,6 @@ }, { "context": "EmptyPane || SharedScreen || MarkdownPreview || KeyContextView || Welcome", - "use_layout_keys": true, "bindings": { ":": "command_palette::Toggle", "g /": "pane::DeploySearch" @@ -639,7 +613,6 @@ { // netrw compatibility "context": "ProjectPanel && not_editing", - "use_layout_keys": true, "bindings": { ":": "command_palette::Toggle", "%": "project_panel::NewFile", @@ -673,7 +646,6 @@ }, { "context": "OutlinePanel && not_editing", - "use_layout_keys": true, "bindings": { "j": "menu::SelectNext", "k": "menu::SelectPrev", diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index b34806405c..82329337c6 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -20,7 +20,7 @@ pub struct KeymapBlock { #[serde(default)] context: Option, #[serde(default)] - use_layout_keys: Option, + use_key_equivalents: Option, bindings: BTreeMap, } @@ -80,7 +80,7 @@ impl KeymapFile { for KeymapBlock { context, - use_layout_keys, + use_key_equivalents, bindings, } in self.0 { @@ -124,10 +124,10 @@ impl KeymapFile { &keystroke, action, context.as_deref(), - if use_layout_keys.unwrap_or_default() { - None - } else { + if use_key_equivalents.unwrap_or_default() { key_equivalents.as_ref() + } else { + None }, ) }) diff --git a/docs/src/key-bindings.md b/docs/src/key-bindings.md index 660a80ebd4..4d0a33ce55 100644 --- a/docs/src/key-bindings.md +++ b/docs/src/key-bindings.md @@ -146,20 +146,15 @@ Finally keyboards that support extended Latin alphabets (usually ISO keyboards) For example on a German QWERTZ keyboard, the `cmd->` shortcut is moved to `cmd-:` because `cmd->` is the system window switcher and this is where that shortcut is typed on a QWERTY keyboard. `cmd-+` stays the same because + is still typable without option, and as a result, `cmd-[` and `cmd-]` become `cmd-ö` and `cmd-ä`, moving out of the way of the `+` key. -If you are defining shortcuts in your personal keymap, you can opt-out of the key equivalent mapping by setting `use_layout_keys` to `true` in your keymap: +If you are defining shortcuts in your personal keymap, you can opt into the key equivalent mapping by setting `use_key_equivalents` to `true` in your keymap: ```json [ { + "use_key_equivalents": true, "bindings": { "ctrl->": "editor::Indent" // parsed as ctrl-: when a German QWERTZ keyboard is active } - }, - { - "use_layout_keys": true, - "bindings": { - "ctrl->": "editor::Indent" // remains ctrl-> when a German QWERTZ keyboard is active - } } ] ``` From 7d80d1208cb2f14707e330ed97d5c453a9a65057 Mon Sep 17 00:00:00 2001 From: geemili Date: Fri, 6 Dec 2024 14:05:41 -0700 Subject: [PATCH 330/886] vim: Add delete action to HelixNormal mode (#21544) Related issue: https://github.com/zed-industries/zed/issues/4642 Release-Notes: * N/A --- assets/keymaps/vim.json | 1 + crates/vim/src/helix.rs | 104 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 104 insertions(+), 1 deletion(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 9328b0325f..597388368d 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -326,6 +326,7 @@ "bindings": { "i": "vim::InsertBefore", "a": "vim::InsertAfter", + "d": "vim::HelixDelete", "w": "vim::NextWordStart", "e": "vim::NextWordEnd", "b": "vim::PreviousWordStart", diff --git a/crates/vim/src/helix.rs b/crates/vim/src/helix.rs index 21abb5cbaa..3358538991 100644 --- a/crates/vim/src/helix.rs +++ b/crates/vim/src/helix.rs @@ -5,10 +5,11 @@ use ui::ViewContext; use crate::{motion::Motion, state::Mode, Vim}; -actions!(vim, [HelixNormalAfter]); +actions!(vim, [HelixNormalAfter, HelixDelete]); pub fn register(editor: &mut Editor, cx: &mut ViewContext) { Vim::action(editor, cx, Vim::helix_normal_after); + Vim::action(editor, cx, Vim::helix_delete); } impl Vim { @@ -226,6 +227,27 @@ impl Vim { _ => self.helix_move_and_collapse(motion, times, cx), } } + + pub fn helix_delete(&mut self, _: &HelixDelete, cx: &mut ViewContext) { + self.store_visual_marks(cx); + self.update_editor(cx, |vim, editor, cx| { + // Fixup selections so they have helix's semantics. + // Specifically: + // - Make sure that each cursor acts as a 1 character wide selection + editor.transact(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.move_with(|map, selection| { + if selection.is_empty() && !selection.reversed { + selection.end = movement::right(map, selection.end); + } + }); + }); + }); + + vim.copy_selections_content(editor, false, cx); + editor.insert("", cx); + }); + } } #[cfg(test)] @@ -268,4 +290,84 @@ mod test { Mode::HelixNormal, ); } + + #[gpui::test] + async fn test_delete(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + // test delete a selection + cx.set_state( + indoc! {" + The qu«ick ˇ»brown + fox jumps over + the lazy dog."}, + Mode::HelixNormal, + ); + + cx.simulate_keystrokes("d"); + + cx.assert_state( + indoc! {" + The quˇbrown + fox jumps over + the lazy dog."}, + Mode::HelixNormal, + ); + + // test deleting a single character + cx.simulate_keystrokes("d"); + + cx.assert_state( + indoc! {" + The quˇrown + fox jumps over + the lazy dog."}, + Mode::HelixNormal, + ); + } + + #[gpui::test] + async fn test_delete_character_end_of_line(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + cx.set_state( + indoc! {" + The quick brownˇ + fox jumps over + the lazy dog."}, + Mode::HelixNormal, + ); + + cx.simulate_keystrokes("d"); + + cx.assert_state( + indoc! {" + The quick brownˇfox jumps over + the lazy dog."}, + Mode::HelixNormal, + ); + } + + #[gpui::test] + async fn test_delete_character_end_of_buffer(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + cx.set_state( + indoc! {" + The quick brown + fox jumps over + the lazy dog.ˇ"}, + Mode::HelixNormal, + ); + + cx.simulate_keystrokes("d"); + + cx.assert_state( + indoc! {" + The quick brown + fox jumps over + the lazy dog.ˇ"}, + Mode::HelixNormal, + ); + } } From de939e718a7f295b19a4a2b0315e710cc55dda38 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 6 Dec 2024 13:50:59 -0800 Subject: [PATCH 331/886] Simplify livekit config so that cargo check Just Works (#21661) Supersedes https://github.com/zed-industries/zed/pull/21653 This enables us to use `cargo test -p workspace` on macOS and Linux. Note that the line diffs in `shared_screen.rs` are spurious, I just re-ordered the `macos` and `cross-platform` modules to match the order in the call crate. Release Notes: - N/A --- .github/workflows/ci.yml | 9 +- crates/call/Cargo.toml | 10 +- crates/call/src/call.rs | 36 +-- crates/workspace/Cargo.toml | 2 - crates/workspace/src/shared_screen.rs | 289 +----------------- .../src/shared_screen/cross_platform.rs | 114 +++++++ crates/workspace/src/shared_screen/macos.rs | 126 ++++++++ crates/zed/Cargo.toml | 6 - script/check-rust-livekit-macos | 19 ++ .../patches/use-cross-platform-livekit.patch | 59 ++++ typos.toml | 2 + 11 files changed, 345 insertions(+), 327 deletions(-) create mode 100644 crates/workspace/src/shared_screen/cross_platform.rs create mode 100644 crates/workspace/src/shared_screen/macos.rs create mode 100755 script/check-rust-livekit-macos create mode 100644 script/patches/use-cross-platform-livekit.patch diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 46e7ab7d51..8a19130324 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -129,8 +129,9 @@ jobs: run: | cargo build --workspace --bins --all-features cargo check -p gpui --features "macos-blade" - cargo check -p workspace --features "livekit-cross-platform" + cargo check -p workspace cargo build -p remote_server + script/check-rust-livekit-macos linux_tests: timeout-minutes: 60 @@ -162,8 +163,10 @@ jobs: - name: Run tests uses: ./.github/actions/run_tests - - name: Build Zed - run: cargo build -p zed + - name: Build other binaries and features + run: | + cargo build -p zed + cargo check -p workspace build_remote_server: timeout-minutes: 60 diff --git a/crates/call/Cargo.toml b/crates/call/Cargo.toml index e7bc8b44a3..9ba10e56ba 100644 --- a/crates/call/Cargo.toml +++ b/crates/call/Cargo.toml @@ -21,8 +21,6 @@ test-support = [ "project/test-support", "util/test-support" ] -livekit-macos = ["livekit_client_macos"] -livekit-cross-platform = ["livekit_client"] [dependencies] anyhow.workspace = true @@ -42,8 +40,12 @@ serde.workspace = true serde_derive.workspace = true settings.workspace = true util.workspace = true -livekit_client_macos = { workspace = true, optional = true } -livekit_client = { workspace = true, optional = true } + +[target.'cfg(target_os = "macos")'.dependencies] +livekit_client_macos = { workspace = true } + +[target.'cfg(not(target_os = "macos"))'.dependencies] +livekit_client = { workspace = true } [dev-dependencies] client = { workspace = true, features = ["test-support"] } diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 9fdce4b8ba..5e212d35b7 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -1,41 +1,13 @@ pub mod call_settings; -#[cfg(any( - all(target_os = "macos", feature = "livekit-macos"), - all( - not(target_os = "macos"), - feature = "livekit-macos", - not(feature = "livekit-cross-platform") - ) -))] +#[cfg(target_os = "macos")] mod macos; -#[cfg(any( - all(target_os = "macos", feature = "livekit-macos"), - all( - not(target_os = "macos"), - feature = "livekit-macos", - not(feature = "livekit-cross-platform") - ) -))] +#[cfg(target_os = "macos")] pub use macos::*; -#[cfg(any( - all( - target_os = "macos", - feature = "livekit-cross-platform", - not(feature = "livekit-macos"), - ), - all(not(target_os = "macos"), feature = "livekit-cross-platform"), -))] +#[cfg(not(target_os = "macos"))] mod cross_platform; -#[cfg(any( - all( - target_os = "macos", - feature = "livekit-cross-platform", - not(feature = "livekit-macos"), - ), - all(not(target_os = "macos"), feature = "livekit-cross-platform"), -))] +#[cfg(not(target_os = "macos"))] pub use cross_platform::*; diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index be2dfb06bd..3b17ed8dab 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -24,8 +24,6 @@ test-support = [ "gpui/test-support", "fs/test-support", ] -livekit-macos = ["call/livekit-macos"] -livekit-cross-platform = ["call/livekit-cross-platform"] [dependencies] anyhow.workspace = true diff --git a/crates/workspace/src/shared_screen.rs b/crates/workspace/src/shared_screen.rs index f7a1ccf760..1d17cfa145 100644 --- a/crates/workspace/src/shared_screen.rs +++ b/crates/workspace/src/shared_screen.rs @@ -1,282 +1,11 @@ -#[cfg(any( - all( - target_os = "macos", - feature = "livekit-cross-platform", - not(feature = "livekit-macos"), - ), - all(not(target_os = "macos"), feature = "livekit-cross-platform"), -))] -mod cross_platform { - use crate::{ - item::{Item, ItemEvent}, - ItemNavHistory, WorkspaceId, - }; - use call::{RemoteVideoTrack, RemoteVideoTrackView}; - use client::{proto::PeerId, User}; - use gpui::{ - div, AppContext, EventEmitter, FocusHandle, FocusableView, InteractiveElement, - ParentElement, Render, SharedString, Styled, View, ViewContext, VisualContext, - WindowContext, - }; - use std::sync::Arc; - use ui::{prelude::*, Icon, IconName}; +#[cfg(target_os = "macos")] +mod macos; - pub enum Event { - Close, - } - - pub struct SharedScreen { - pub peer_id: PeerId, - user: Arc, - nav_history: Option, - view: View, - focus: FocusHandle, - } - - impl SharedScreen { - pub fn new( - track: RemoteVideoTrack, - peer_id: PeerId, - user: Arc, - cx: &mut ViewContext, - ) -> Self { - let view = cx.new_view(|cx| RemoteVideoTrackView::new(track.clone(), cx)); - cx.subscribe(&view, |_, _, ev, cx| match ev { - call::RemoteVideoTrackViewEvent::Close => cx.emit(Event::Close), - }) - .detach(); - Self { - view, - peer_id, - user, - nav_history: Default::default(), - focus: cx.focus_handle(), - } - } - } - - impl EventEmitter for SharedScreen {} - - impl FocusableView for SharedScreen { - fn focus_handle(&self, _: &AppContext) -> FocusHandle { - self.focus.clone() - } - } - impl Render for SharedScreen { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - div() - .bg(cx.theme().colors().editor_background) - .track_focus(&self.focus) - .key_context("SharedScreen") - .size_full() - .child(self.view.clone()) - } - } - - impl Item for SharedScreen { - type Event = Event; - - fn tab_tooltip_text(&self, _: &AppContext) -> Option { - Some(format!("{}'s screen", self.user.github_login).into()) - } - - fn deactivated(&mut self, cx: &mut ViewContext) { - if let Some(nav_history) = self.nav_history.as_mut() { - nav_history.push::<()>(None, cx); - } - } - - fn tab_icon(&self, _cx: &WindowContext) -> Option { - Some(Icon::new(IconName::Screen)) - } - - fn tab_content_text(&self, _cx: &WindowContext) -> Option { - Some(format!("{}'s screen", self.user.github_login).into()) - } - - fn telemetry_event_text(&self) -> Option<&'static str> { - None - } - - fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext) { - self.nav_history = Some(history); - } - - fn clone_on_split( - &self, - _workspace_id: Option, - cx: &mut ViewContext, - ) -> Option> { - Some(cx.new_view(|cx| Self { - view: self.view.update(cx, |view, cx| view.clone(cx)), - peer_id: self.peer_id, - user: self.user.clone(), - nav_history: Default::default(), - focus: cx.focus_handle(), - })) - } - - fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) { - match event { - Event::Close => f(ItemEvent::CloseItem), - } - } - } -} - -#[cfg(any( - all( - target_os = "macos", - feature = "livekit-cross-platform", - not(feature = "livekit-macos"), - ), - all(not(target_os = "macos"), feature = "livekit-cross-platform"), -))] -pub use cross_platform::*; - -#[cfg(any( - all(target_os = "macos", feature = "livekit-macos"), - all( - not(target_os = "macos"), - feature = "livekit-macos", - not(feature = "livekit-cross-platform") - ) -))] -mod macos { - use crate::{ - item::{Item, ItemEvent}, - ItemNavHistory, WorkspaceId, - }; - use anyhow::Result; - use call::participant::{Frame, RemoteVideoTrack}; - use client::{proto::PeerId, User}; - use futures::StreamExt; - use gpui::{ - div, surface, AppContext, EventEmitter, FocusHandle, FocusableView, InteractiveElement, - ParentElement, Render, SharedString, Styled, Task, View, ViewContext, VisualContext, - WindowContext, - }; - use std::sync::{Arc, Weak}; - use ui::{prelude::*, Icon, IconName}; - - pub enum Event { - Close, - } - - pub struct SharedScreen { - track: Weak, - frame: Option, - pub peer_id: PeerId, - user: Arc, - nav_history: Option, - _maintain_frame: Task>, - focus: FocusHandle, - } - - impl SharedScreen { - pub fn new( - track: Arc, - peer_id: PeerId, - user: Arc, - cx: &mut ViewContext, - ) -> Self { - cx.focus_handle(); - let mut frames = track.frames(); - Self { - track: Arc::downgrade(&track), - frame: None, - peer_id, - user, - nav_history: Default::default(), - _maintain_frame: cx.spawn(|this, mut cx| async move { - while let Some(frame) = frames.next().await { - this.update(&mut cx, |this, cx| { - this.frame = Some(frame); - cx.notify(); - })?; - } - this.update(&mut cx, |_, cx| cx.emit(Event::Close))?; - Ok(()) - }), - focus: cx.focus_handle(), - } - } - } - - impl EventEmitter for SharedScreen {} - - impl FocusableView for SharedScreen { - fn focus_handle(&self, _: &AppContext) -> FocusHandle { - self.focus.clone() - } - } - impl Render for SharedScreen { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - div() - .bg(cx.theme().colors().editor_background) - .track_focus(&self.focus) - .key_context("SharedScreen") - .size_full() - .children( - self.frame - .as_ref() - .map(|frame| surface(frame.image()).size_full()), - ) - } - } - - impl Item for SharedScreen { - type Event = Event; - - fn tab_tooltip_text(&self, _: &AppContext) -> Option { - Some(format!("{}'s screen", self.user.github_login).into()) - } - - fn deactivated(&mut self, cx: &mut ViewContext) { - if let Some(nav_history) = self.nav_history.as_mut() { - nav_history.push::<()>(None, cx); - } - } - - fn tab_icon(&self, _cx: &WindowContext) -> Option { - Some(Icon::new(IconName::Screen)) - } - - fn tab_content_text(&self, _cx: &WindowContext) -> Option { - Some(format!("{}'s screen", self.user.github_login).into()) - } - - fn telemetry_event_text(&self) -> Option<&'static str> { - None - } - - fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext) { - self.nav_history = Some(history); - } - - fn clone_on_split( - &self, - _workspace_id: Option, - cx: &mut ViewContext, - ) -> Option> { - let track = self.track.upgrade()?; - Some(cx.new_view(|cx| Self::new(track, self.peer_id, self.user.clone(), cx))) - } - - fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) { - match event { - Event::Close => f(ItemEvent::CloseItem), - } - } - } -} - -#[cfg(any( - all(target_os = "macos", feature = "livekit-macos"), - all( - not(target_os = "macos"), - feature = "livekit-macos", - not(feature = "livekit-cross-platform") - ) -))] +#[cfg(target_os = "macos")] pub use macos::*; + +#[cfg(not(target_os = "macos"))] +mod cross_platform; + +#[cfg(not(target_os = "macos"))] +pub use cross_platform::*; diff --git a/crates/workspace/src/shared_screen/cross_platform.rs b/crates/workspace/src/shared_screen/cross_platform.rs new file mode 100644 index 0000000000..285946cce0 --- /dev/null +++ b/crates/workspace/src/shared_screen/cross_platform.rs @@ -0,0 +1,114 @@ +use crate::{ + item::{Item, ItemEvent}, + ItemNavHistory, WorkspaceId, +}; +use call::{RemoteVideoTrack, RemoteVideoTrackView}; +use client::{proto::PeerId, User}; +use gpui::{ + div, AppContext, EventEmitter, FocusHandle, FocusableView, InteractiveElement, ParentElement, + Render, SharedString, Styled, View, ViewContext, VisualContext, WindowContext, +}; +use std::sync::Arc; +use ui::{prelude::*, Icon, IconName}; + +pub enum Event { + Close, +} + +pub struct SharedScreen { + pub peer_id: PeerId, + user: Arc, + nav_history: Option, + view: View, + focus: FocusHandle, +} + +impl SharedScreen { + pub fn new( + track: RemoteVideoTrack, + peer_id: PeerId, + user: Arc, + cx: &mut ViewContext, + ) -> Self { + let view = cx.new_view(|cx| RemoteVideoTrackView::new(track.clone(), cx)); + cx.subscribe(&view, |_, _, ev, cx| match ev { + call::RemoteVideoTrackViewEvent::Close => cx.emit(Event::Close), + }) + .detach(); + Self { + view, + peer_id, + user, + nav_history: Default::default(), + focus: cx.focus_handle(), + } + } +} + +impl EventEmitter for SharedScreen {} + +impl FocusableView for SharedScreen { + fn focus_handle(&self, _: &AppContext) -> FocusHandle { + self.focus.clone() + } +} +impl Render for SharedScreen { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + div() + .bg(cx.theme().colors().editor_background) + .track_focus(&self.focus) + .key_context("SharedScreen") + .size_full() + .child(self.view.clone()) + } +} + +impl Item for SharedScreen { + type Event = Event; + + fn tab_tooltip_text(&self, _: &AppContext) -> Option { + Some(format!("{}'s screen", self.user.github_login).into()) + } + + fn deactivated(&mut self, cx: &mut ViewContext) { + if let Some(nav_history) = self.nav_history.as_mut() { + nav_history.push::<()>(None, cx); + } + } + + fn tab_icon(&self, _cx: &WindowContext) -> Option { + Some(Icon::new(IconName::Screen)) + } + + fn tab_content_text(&self, _cx: &WindowContext) -> Option { + Some(format!("{}'s screen", self.user.github_login).into()) + } + + fn telemetry_event_text(&self) -> Option<&'static str> { + None + } + + fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext) { + self.nav_history = Some(history); + } + + fn clone_on_split( + &self, + _workspace_id: Option, + cx: &mut ViewContext, + ) -> Option> { + Some(cx.new_view(|cx| Self { + view: self.view.update(cx, |view, cx| view.clone(cx)), + peer_id: self.peer_id, + user: self.user.clone(), + nav_history: Default::default(), + focus: cx.focus_handle(), + })) + } + + fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) { + match event { + Event::Close => f(ItemEvent::CloseItem), + } + } +} diff --git a/crates/workspace/src/shared_screen/macos.rs b/crates/workspace/src/shared_screen/macos.rs new file mode 100644 index 0000000000..ad0b4c4275 --- /dev/null +++ b/crates/workspace/src/shared_screen/macos.rs @@ -0,0 +1,126 @@ +use crate::{ + item::{Item, ItemEvent}, + ItemNavHistory, WorkspaceId, +}; +use anyhow::Result; +use call::participant::{Frame, RemoteVideoTrack}; +use client::{proto::PeerId, User}; +use futures::StreamExt; +use gpui::{ + div, surface, AppContext, EventEmitter, FocusHandle, FocusableView, InteractiveElement, + ParentElement, Render, SharedString, Styled, Task, View, ViewContext, VisualContext, + WindowContext, +}; +use std::sync::{Arc, Weak}; +use ui::{prelude::*, Icon, IconName}; + +pub enum Event { + Close, +} + +pub struct SharedScreen { + track: Weak, + frame: Option, + pub peer_id: PeerId, + user: Arc, + nav_history: Option, + _maintain_frame: Task>, + focus: FocusHandle, +} + +impl SharedScreen { + pub fn new( + track: Arc, + peer_id: PeerId, + user: Arc, + cx: &mut ViewContext, + ) -> Self { + cx.focus_handle(); + let mut frames = track.frames(); + Self { + track: Arc::downgrade(&track), + frame: None, + peer_id, + user, + nav_history: Default::default(), + _maintain_frame: cx.spawn(|this, mut cx| async move { + while let Some(frame) = frames.next().await { + this.update(&mut cx, |this, cx| { + this.frame = Some(frame); + cx.notify(); + })?; + } + this.update(&mut cx, |_, cx| cx.emit(Event::Close))?; + Ok(()) + }), + focus: cx.focus_handle(), + } + } +} + +impl EventEmitter for SharedScreen {} + +impl FocusableView for SharedScreen { + fn focus_handle(&self, _: &AppContext) -> FocusHandle { + self.focus.clone() + } +} +impl Render for SharedScreen { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + div() + .bg(cx.theme().colors().editor_background) + .track_focus(&self.focus) + .key_context("SharedScreen") + .size_full() + .children( + self.frame + .as_ref() + .map(|frame| surface(frame.image()).size_full()), + ) + } +} + +impl Item for SharedScreen { + type Event = Event; + + fn tab_tooltip_text(&self, _: &AppContext) -> Option { + Some(format!("{}'s screen", self.user.github_login).into()) + } + + fn deactivated(&mut self, cx: &mut ViewContext) { + if let Some(nav_history) = self.nav_history.as_mut() { + nav_history.push::<()>(None, cx); + } + } + + fn tab_icon(&self, _cx: &WindowContext) -> Option { + Some(Icon::new(IconName::Screen)) + } + + fn tab_content_text(&self, _cx: &WindowContext) -> Option { + Some(format!("{}'s screen", self.user.github_login).into()) + } + + fn telemetry_event_text(&self) -> Option<&'static str> { + None + } + + fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext) { + self.nav_history = Some(history); + } + + fn clone_on_split( + &self, + _workspace_id: Option, + cx: &mut ViewContext, + ) -> Option> { + let track = self.track.upgrade()?; + Some(cx.new_view(|cx| Self::new(track, self.peer_id, self.user.clone(), cx))) + } + + fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) { + match event { + Event::Close => f(ItemEvent::CloseItem), + } + } +} diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 9a672757a6..2220cc7be0 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -127,12 +127,6 @@ welcome.workspace = true workspace.workspace = true zed_actions.workspace = true -[target.'cfg(target_os = "macos")'.dependencies] -workspace = { workspace = true, features = ["livekit-macos"] } - -[target.'cfg(not(target_os = "macos"))'.dependencies] -workspace = { workspace = true, features = ["livekit-cross-platform"] } - [target.'cfg(target_os = "windows")'.dependencies] windows.workspace = true diff --git a/script/check-rust-livekit-macos b/script/check-rust-livekit-macos new file mode 100755 index 0000000000..e2d0f9cf62 --- /dev/null +++ b/script/check-rust-livekit-macos @@ -0,0 +1,19 @@ +#!/bin/bash + + +set -exuo pipefail + +git apply script/patches/use-cross-platform-livekit.patch + +# Re-enable error skipping for this check, so that we can unapply the patch +set +e + +cargo check -p workspace +exit_code=$? + +# Disable error skipping again +set -e + +git apply -R script/patches/use-cross-platform-livekit.patch + +exit "$exit_code" diff --git a/script/patches/use-cross-platform-livekit.patch b/script/patches/use-cross-platform-livekit.patch new file mode 100644 index 0000000000..81dcca80f6 --- /dev/null +++ b/script/patches/use-cross-platform-livekit.patch @@ -0,0 +1,59 @@ +diff --git a/crates/call/Cargo.toml b/crates/call/Cargo.toml +index 9ba10e56ba..bb69440691 100644 +--- a/crates/call/Cargo.toml ++++ b/crates/call/Cargo.toml +@@ -41,10 +41,10 @@ serde_derive.workspace = true + settings.workspace = true + util.workspace = true + +-[target.'cfg(target_os = "macos")'.dependencies] ++[target.'cfg(any())'.dependencies] + livekit_client_macos = { workspace = true } + +-[target.'cfg(not(target_os = "macos"))'.dependencies] ++[target.'cfg(all())'.dependencies] + livekit_client = { workspace = true } + + [dev-dependencies] +diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs +index 5e212d35b7..a8f9e8f43e 100644 +--- a/crates/call/src/call.rs ++++ b/crates/call/src/call.rs +@@ -1,13 +1,13 @@ + pub mod call_settings; + +-#[cfg(target_os = "macos")] ++#[cfg(any())] + mod macos; + +-#[cfg(target_os = "macos")] ++#[cfg(any())] + pub use macos::*; + +-#[cfg(not(target_os = "macos"))] ++#[cfg(all())] + mod cross_platform; + +-#[cfg(not(target_os = "macos"))] ++#[cfg(all())] + pub use cross_platform::*; +diff --git a/crates/workspace/src/shared_screen.rs b/crates/workspace/src/shared_screen.rs +index 1d17cfa145..f845234987 100644 +--- a/crates/workspace/src/shared_screen.rs ++++ b/crates/workspace/src/shared_screen.rs +@@ -1,11 +1,11 @@ +-#[cfg(target_os = "macos")] ++#[cfg(any())] + mod macos; + +-#[cfg(target_os = "macos")] ++#[cfg(any())] + pub use macos::*; + +-#[cfg(not(target_os = "macos"))] ++#[cfg(all())] + mod cross_platform; + +-#[cfg(not(target_os = "macos"))] ++#[cfg(all())] + pub use cross_platform::*; diff --git a/typos.toml b/typos.toml index dc724dd50d..50f3aadd0a 100644 --- a/typos.toml +++ b/typos.toml @@ -43,6 +43,8 @@ extend-exclude = [ "docs/theme/css/", # Spellcheck triggers on `|Fixe[sd]|` regex part. "script/danger/dangerfile.ts", + # Hashes are not typos + "script/patches/use-cross-platform-livekit.patch" ] [default] From e5374f5d7dc70023f9b8b504f2cf001ac6f18d5e Mon Sep 17 00:00:00 2001 From: feeiyu <158308373+feeiyu@users.noreply.github.com> Date: Sat, 7 Dec 2024 06:15:04 +0800 Subject: [PATCH 332/886] windows: Ignore WM_SIZE event when minimizing window (#21533) Closes #21364 Release Notes: - Fixed minimize window and then reopen cause the layout changed ![layout1204](https://github.com/user-attachments/assets/e823da90-0cc6-4fc9-8b8e-82680357c6fe) --- crates/gpui/src/platform/windows/events.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/gpui/src/platform/windows/events.rs b/crates/gpui/src/platform/windows/events.rs index 025fbba4ac..27235d5d40 100644 --- a/crates/gpui/src/platform/windows/events.rs +++ b/crates/gpui/src/platform/windows/events.rs @@ -33,7 +33,7 @@ pub(crate) fn handle_msg( WM_ACTIVATE => handle_activate_msg(handle, wparam, state_ptr), WM_CREATE => handle_create_msg(handle, state_ptr), WM_MOVE => handle_move_msg(handle, lparam, state_ptr), - WM_SIZE => handle_size_msg(lparam, state_ptr), + WM_SIZE => handle_size_msg(wparam, lparam, state_ptr), WM_ENTERSIZEMOVE | WM_ENTERMENULOOP => handle_size_move_loop(handle), WM_EXITSIZEMOVE | WM_EXITMENULOOP => handle_size_move_loop_exit(handle), WM_TIMER => handle_timer_msg(handle, wparam, state_ptr), @@ -136,7 +136,15 @@ fn handle_move_msg( Some(0) } -fn handle_size_msg(lparam: LPARAM, state_ptr: Rc) -> Option { +fn handle_size_msg( + wparam: WPARAM, + lparam: LPARAM, + state_ptr: Rc, +) -> Option { + if wparam.0 == SIZE_MINIMIZED as usize { + return Some(0); + } + let width = lparam.loword().max(1) as i32; let height = lparam.hiword().max(1) as i32; let mut lock = state_ptr.state.borrow_mut(); From e019d1405a597d379a2fe54ef09d97eb788bf16d Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Fri, 6 Dec 2024 17:35:00 -0500 Subject: [PATCH 333/886] Send an event when user changes their max monthly spend limit (#21664) Release Notes: - N/A --------- Co-authored-by: Marshall Bowers --- crates/collab/src/api/billing.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/crates/collab/src/api/billing.rs b/crates/collab/src/api/billing.rs index d431e4c043..88201bb5cc 100644 --- a/crates/collab/src/api/billing.rs +++ b/crates/collab/src/api/billing.rs @@ -9,6 +9,7 @@ use collections::HashSet; use reqwest::StatusCode; use sea_orm::ActiveValue; use serde::{Deserialize, Serialize}; +use serde_json::json; use std::{str::FromStr, sync::Arc, time::Duration}; use stripe::{ BillingPortalSession, CreateBillingPortalSession, CreateBillingPortalSessionFlowData, @@ -19,6 +20,7 @@ use stripe::{ }; use util::ResultExt; +use crate::api::events::SnowflakeRow; use crate::llm::{DEFAULT_MAX_MONTHLY_SPEND, FREE_TIER_MONTHLY_SPENDING_LIMIT}; use crate::rpc::{ResultExt as _, Server}; use crate::{ @@ -124,6 +126,20 @@ async fn update_billing_preferences( .await? }; + SnowflakeRow::new( + "Spend Limit Updated", + Some(user.metrics_id), + user.admin, + None, + json!({ + "user_id": user.id, + "max_monthly_llm_usage_spending_in_cents": billing_preferences.max_monthly_llm_usage_spending_in_cents, + }), + ) + .write(&app.kinesis_client, &app.config.kinesis_stream) + .await + .log_err(); + rpc_server.refresh_llm_tokens_for_user(user.id).await; Ok(Json(BillingPreferencesResponse { From 21a6664cf8f54c6a8395c40a3eec9f7fdb796f55 Mon Sep 17 00:00:00 2001 From: Matin Aniss <76515905+MatinAniss@users.noreply.github.com> Date: Sat, 7 Dec 2024 09:53:27 +1100 Subject: [PATCH 334/886] gpui: Support animated WebP image (#20778) Add support for decoding animated WebP images into their individual frames. Release Notes: - N/A --- crates/gpui/src/elements/img.rs | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/crates/gpui/src/elements/img.rs b/crates/gpui/src/elements/img.rs index 895904c801..3a1b1d92fb 100644 --- a/crates/gpui/src/elements/img.rs +++ b/crates/gpui/src/elements/img.rs @@ -8,7 +8,8 @@ use anyhow::{anyhow, Result}; use futures::{AsyncReadExt, Future}; use image::{ - codecs::gif::GifDecoder, AnimationDecoder, Frame, ImageBuffer, ImageError, ImageFormat, + codecs::{gif::GifDecoder, webp::WebPDecoder}, + AnimationDecoder, DynamicImage, Frame, ImageBuffer, ImageError, ImageFormat, Rgba, }; use smallvec::SmallVec; use std::{ @@ -542,6 +543,34 @@ impl Asset for ImageAssetLoader { frames } + ImageFormat::WebP => { + let mut decoder = WebPDecoder::new(Cursor::new(&bytes))?; + + if decoder.has_animation() { + let _ = decoder.set_background_color(Rgba([0, 0, 0, 0])); + let mut frames = SmallVec::new(); + + for frame in decoder.into_frames() { + let mut frame = frame?; + // Convert from RGBA to BGRA. + for pixel in frame.buffer_mut().chunks_exact_mut(4) { + pixel.swap(0, 2); + } + frames.push(frame); + } + + frames + } else { + let mut data = DynamicImage::from_decoder(decoder)?.into_rgba8(); + + // Convert from RGBA to BGRA. + for pixel in data.chunks_exact_mut(4) { + pixel.swap(0, 2); + } + + SmallVec::from_elem(Frame::new(data), 1) + } + } _ => { let mut data = image::load_from_memory_with_format(&bytes, format)?.into_rgba8(); From 9d44ed089471c71c96c599c80dc60090b15f8696 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 6 Dec 2024 16:42:50 -0700 Subject: [PATCH 335/886] Stop overriding cancelOperation (#21667) This was added before we were handling key equivalents, and is no longer needed. Furthermore in the gpui2 re-write we stopped sending the correct modifiers so this hasn't worked for the last year. Fixes #21520 Release Notes: - Fixed a bug where cmd-escape could act like . --- crates/gpui/src/platform/mac/window.rs | 27 -------------------------- 1 file changed, 27 deletions(-) diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 8ea7ebd4d5..1779767dca 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -152,10 +152,6 @@ unsafe fn build_classes() { sel!(flagsChanged:), handle_view_event as extern "C" fn(&Object, Sel, id), ); - decl.add_method( - sel!(cancelOperation:), - cancel_operation as extern "C" fn(&Object, Sel, id), - ); decl.add_method( sel!(makeBackingLayer), @@ -1455,29 +1451,6 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) { } } -// Allows us to receive `cmd-.` (the shortcut for closing a dialog) -// https://bugs.eclipse.org/bugs/show_bug.cgi?id=300620#c6 -extern "C" fn cancel_operation(this: &Object, _sel: Sel, _sender: id) { - let window_state = unsafe { get_window_state(this) }; - let mut lock = window_state.as_ref().lock(); - - let keystroke = Keystroke { - modifiers: Default::default(), - key: ".".into(), - key_char: None, - }; - let event = PlatformInput::KeyDown(KeyDownEvent { - keystroke: keystroke.clone(), - is_held: false, - }); - - if let Some(mut callback) = lock.event_callback.take() { - drop(lock); - callback(event); - window_state.lock().event_callback = Some(callback); - } -} - extern "C" fn window_did_change_occlusion_state(this: &Object, _: Sel, _: id) { let window_state = unsafe { get_window_state(this) }; let lock = &mut *window_state.lock(); From 9e287b33e58a4d4753a90d31a318f4c3de1d4690 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 6 Dec 2024 16:42:58 -0700 Subject: [PATCH 336/886] Update NorwegianExtended equivalents (#21665) Release Notes: - Impoved key equivalents for Norwegian Extended layout --- crates/settings/src/key_equivalents.rs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/crates/settings/src/key_equivalents.rs b/crates/settings/src/key_equivalents.rs index 4c5ae9e065..a0029aabbe 100644 --- a/crates/settings/src/key_equivalents.rs +++ b/crates/settings/src/key_equivalents.rs @@ -881,7 +881,26 @@ pub fn get_key_equivalents(layout: &str) -> Option> { ('}', 'Æ'), ('~', '>'), ], - "com.apple.keylayout.NorwegianExtended" => &[('^', 'ˆ'), ('~', '˜')], + "com.apple.keylayout.NorwegianExtended" => &[ + ('"', 'ˆ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ø'), + ('\\', '@'), + (']', 'æ'), + ('`', '<'), + ('}', 'Æ'), + ('~', '>'), + ], "com.apple.keylayout.NorwegianSami-PC" => &[ ('"', 'ˆ'), ('&', '/'), From 4d22a07a1e90214a254f4d9d94c15d7428a96f92 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 6 Dec 2024 16:43:12 -0700 Subject: [PATCH 337/886] Remove last few alt- bindings (#21669) Although I hoped we could keep the non-ascii alt characters, it turns out this is not the case for all keyboards. Fixes #21175 Release Notes: - (breaking change) editor::ShowInlineCompetion is now `option-tab` on macOS (not `option-/`). editor::{Next,Previous}Completion are `option-tab` and `option-shift-tab` (not `option-[` and `option-]`). This fixes typing characters generated by option-{/,[,]} on keyboards like Croatian. --- assets/keymaps/default-macos.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 33c32035d9..f54216712e 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -153,8 +153,8 @@ "context": "Editor && mode == full && inline_completion", "use_key_equivalents": true, "bindings": { - "alt-]": "editor::NextInlineCompletion", - "alt-[": "editor::PreviousInlineCompletion", + "alt-tab": "editor::NextInlineCompletion", + "alt-shift-tab": "editor::PreviousInlineCompletion", "ctrl-right": "editor::AcceptPartialInlineCompletion" } }, @@ -162,7 +162,7 @@ "context": "Editor && !inline_completion", "use_key_equivalents": true, "bindings": { - "alt-\\": "editor::ShowInlineCompletion" + "alt-tab": "editor::ShowInlineCompletion" } }, { From fa7dddd6b56b44711c2cf4100131b735435acbfe Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Fri, 6 Dec 2024 22:11:40 -0500 Subject: [PATCH 338/886] gpui: Don't panic when failing to exec system opener (#21674) --- crates/gpui/src/platform/linux/platform.rs | 8 ++++---- crates/gpui/src/platform/mac/platform.rs | 10 ++++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index d8bdcf1052..d0c0f1768e 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -18,7 +18,7 @@ use std::{ time::Duration, }; -use anyhow::anyhow; +use anyhow::{anyhow, Context as _}; use async_task::Runnable; use calloop::channel::Channel; use calloop::{EventLoop, LoopHandle, LoopSignal}; @@ -382,14 +382,14 @@ impl Platform for P { } fn open_with_system(&self, path: &Path) { - let executor = self.background_executor().clone(); let path = path.to_owned(); - executor + self.background_executor() .spawn(async move { let _ = std::process::Command::new("xdg-open") .arg(path) .spawn() - .expect("Failed to open file with xdg-open"); + .context("invoking xdg-open") + .log_err(); }) .detach(); } diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index f0fe560ca4..096bf860a6 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -10,7 +10,7 @@ use crate::{ PlatformTextSystem, PlatformWindow, Result, ScreenCaptureSource, SemanticVersion, Task, WindowAppearance, WindowParams, }; -use anyhow::anyhow; +use anyhow::{anyhow, Context as _}; use block::ConcreteBlock; use cocoa::{ appkit::{ @@ -57,6 +57,7 @@ use std::{ sync::Arc, }; use strum::IntoEnumIterator; +use util::ResultExt; #[allow(non_upper_case_globals)] const NSUTF8StringEncoding: NSUInteger = 4; @@ -779,15 +780,16 @@ impl Platform for MacPlatform { } fn open_with_system(&self, path: &Path) { - let path = path.to_path_buf(); + let path = path.to_owned(); self.0 .lock() .background_executor .spawn(async move { - std::process::Command::new("open") + let _ = std::process::Command::new("open") .arg(path) .spawn() - .expect("Failed to open file"); + .context("invoking open command") + .log_err(); }) .detach(); } From 14ba4a9c944f75e26a4ce3f148844a3eb83b97b9 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 7 Dec 2024 10:39:01 +0200 Subject: [PATCH 339/886] Fix zoomed terminal pane issues on split (#21668) Closes https://github.com/zed-industries/zed/issues/21652 * prevents zooming out the panel when any terminal pane is closed * forces focus on new terminal panes, to prevent the workspace from getting odd pane events in the background Release Notes: - (Preview only) Fixed zoomed terminal pane issues on split --- crates/terminal_view/src/persistence.rs | 9 +- crates/terminal_view/src/terminal_panel.rs | 98 ++++++++++++++++------ crates/workspace/src/pane.rs | 8 +- 3 files changed, 87 insertions(+), 28 deletions(-) diff --git a/crates/terminal_view/src/persistence.rs b/crates/terminal_view/src/persistence.rs index d410ef6d72..f4653014a1 100644 --- a/crates/terminal_view/src/persistence.rs +++ b/crates/terminal_view/src/persistence.rs @@ -214,8 +214,13 @@ async fn deserialize_pane_group( .await; let pane = panel - .update(cx, |_, cx| { - new_terminal_pane(workspace.clone(), project.clone(), cx) + .update(cx, |terminal_panel, cx| { + new_terminal_pane( + workspace.clone(), + project.clone(), + terminal_panel.active_pane.read(cx).is_zoomed(), + cx, + ) }) .log_err()?; let active_item = serialized_pane.active_item; diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index bbe25b8a92..7a68fdd6ba 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -84,9 +84,10 @@ pub struct TerminalPanel { impl TerminalPanel { pub fn new(workspace: &Workspace, cx: &mut ViewContext) -> Self { let project = workspace.project(); - let pane = new_terminal_pane(workspace.weak_handle(), project.clone(), cx); + let pane = new_terminal_pane(workspace.weak_handle(), project.clone(), false, cx); let center = PaneGroup::new(pane.clone()); let enabled = project.read(cx).supports_terminal(cx); + cx.focus_view(&pane); let terminal_panel = Self { center, active_pane: pane, @@ -299,6 +300,9 @@ impl TerminalPanel { let pane_count_before_removal = self.center.panes().len(); let _removal_result = self.center.remove(&pane); if pane_count_before_removal == 1 { + self.center.first_pane().update(cx, |pane, cx| { + pane.set_zoomed(false, cx); + }); cx.emit(PanelEvent::Close); } else { if let Some(focus_on_pane) = @@ -308,27 +312,49 @@ impl TerminalPanel { } } } - pane::Event::ZoomIn => cx.emit(PanelEvent::ZoomIn), - pane::Event::ZoomOut => cx.emit(PanelEvent::ZoomOut), + pane::Event::ZoomIn => { + for pane in self.center.panes() { + pane.update(cx, |pane, cx| { + pane.set_zoomed(true, cx); + }) + } + cx.emit(PanelEvent::ZoomIn); + cx.notify(); + } + pane::Event::ZoomOut => { + for pane in self.center.panes() { + pane.update(cx, |pane, cx| { + pane.set_zoomed(false, cx); + }) + } + cx.emit(PanelEvent::ZoomOut); + cx.notify(); + } pane::Event::AddItem { item } => { if let Some(workspace) = self.workspace.upgrade() { workspace.update(cx, |workspace, cx| { item.added_to_pane(workspace, pane.clone(), cx) }) } + self.serialize(cx); } pane::Event::Split(direction) => { let new_pane = self.new_pane_with_cloned_active_terminal(cx); let pane = pane.clone(); let direction = *direction; - cx.spawn(move |this, mut cx| async move { + cx.spawn(move |terminal_panel, mut cx| async move { let Some(new_pane) = new_pane.await else { return; }; - this.update(&mut cx, |this, _| { - this.center.split(&pane, &new_pane, direction).log_err(); - }) - .ok(); + terminal_panel + .update(&mut cx, |terminal_panel, cx| { + terminal_panel + .center + .split(&pane, &new_pane, direction) + .log_err(); + cx.focus_view(&new_pane); + }) + .ok(); }) .detach(); } @@ -365,7 +391,7 @@ impl TerminalPanel { .or_else(|| default_working_directory(workspace.read(cx), cx)); let kind = TerminalKind::Shell(working_directory); let window = cx.window_handle(); - cx.spawn(move |this, mut cx| async move { + cx.spawn(move |terminal_panel, mut cx| async move { let terminal = project .update(&mut cx, |project, cx| { project.create_terminal(kind, window, cx) @@ -380,10 +406,15 @@ impl TerminalPanel { }) .ok()?, ); - let pane = this - .update(&mut cx, |this, cx| { - let pane = new_terminal_pane(weak_workspace, project, cx); - this.apply_tab_bar_buttons(&pane, cx); + let pane = terminal_panel + .update(&mut cx, |terminal_panel, cx| { + let pane = new_terminal_pane( + weak_workspace, + project, + terminal_panel.active_pane.read(cx).is_zoomed(), + cx, + ); + terminal_panel.apply_tab_bar_buttons(&pane, cx); pane }) .ok()?; @@ -392,7 +423,6 @@ impl TerminalPanel { pane.add_item(terminal_view, true, true, None, cx); }) .ok()?; - cx.focus_view(&pane).ok()?; Some(pane) }) @@ -814,6 +844,7 @@ impl TerminalPanel { pub fn new_terminal_pane( workspace: WeakView, project: Model, + zoomed: bool, cx: &mut ViewContext, ) -> View { let is_local = project.read(cx).is_local(); @@ -827,9 +858,11 @@ pub fn new_terminal_pane( NewTerminal.boxed_clone(), cx, ); + pane.set_zoomed(zoomed, cx); pane.set_can_navigate(false, cx); pane.display_nav_history_buttons(None); pane.set_should_display_tab_bar(|_| true); + pane.set_zoom_out_on_close(false); let terminal_panel_for_split_check = terminal_panel.clone(); pane.set_can_split(Some(Arc::new(move |pane, dragged_item, cx| { @@ -879,8 +912,12 @@ pub fn new_terminal_pane( let new_pane = pane.drag_split_direction().and_then(|split_direction| { terminal_panel.update(cx, |terminal_panel, cx| { - let new_pane = - new_terminal_pane(workspace.clone(), project.clone(), cx); + let new_pane = new_terminal_pane( + workspace.clone(), + project.clone(), + terminal_panel.active_pane.read(cx).is_zoomed(), + cx, + ); terminal_panel.apply_tab_bar_buttons(&new_pane, cx); terminal_panel .center @@ -1062,14 +1099,21 @@ impl Render for TerminalPanel { cx.focus_view(&pane); } else { let new_pane = terminal_panel.new_pane_with_cloned_active_terminal(cx); - cx.spawn(|this, mut cx| async move { + cx.spawn(|terminal_panel, mut cx| async move { if let Some(new_pane) = new_pane.await { - this.update(&mut cx, |this, _| { - this.center - .split(&this.active_pane, &new_pane, SplitDirection::Right) - .log_err(); - }) - .ok(); + terminal_panel + .update(&mut cx, |terminal_panel, cx| { + terminal_panel + .center + .split( + &terminal_panel.active_pane, + &new_pane, + SplitDirection::Right, + ) + .log_err(); + cx.focus_view(&new_pane); + }) + .ok(); } }) .detach(); @@ -1152,8 +1196,12 @@ impl Panel for TerminalPanel { } fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext) { - self.active_pane - .update(cx, |pane, cx| pane.set_zoomed(zoomed, cx)); + for pane in self.center.panes() { + pane.update(cx, |pane, cx| { + pane.set_zoomed(zoomed, cx); + }) + } + cx.notify(); } fn set_active(&mut self, active: bool, cx: &mut ViewContext) { diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 8264cb2a4a..d213ab630b 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -306,6 +306,7 @@ pub struct Pane { pub split_item_context_menu_handle: PopoverMenuHandle, pinned_tab_count: usize, diagnostics: HashMap, + zoom_out_on_close: bool, } pub struct ActivationHistoryEntry { @@ -507,6 +508,7 @@ impl Pane { new_item_context_menu_handle: Default::default(), pinned_tab_count: 0, diagnostics: Default::default(), + zoom_out_on_close: true, } } @@ -1586,7 +1588,7 @@ impl Pane { .remove(&item.item_id()); } - if self.items.is_empty() && close_pane_if_empty && self.zoomed { + if self.zoom_out_on_close && self.items.is_empty() && close_pane_if_empty && self.zoomed { cx.emit(Event::ZoomOut); } @@ -2787,6 +2789,10 @@ impl Pane { pub fn drag_split_direction(&self) -> Option { self.drag_split_direction } + + pub fn set_zoom_out_on_close(&mut self, zoom_out_on_close: bool) { + self.zoom_out_on_close = zoom_out_on_close; + } } impl FocusableView for Pane { From f561a91daf9a8d62bb3533cac2d0bf7842338d28 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Sat, 7 Dec 2024 13:08:18 +0100 Subject: [PATCH 340/886] lsp: Add support for didRename/willRename LSP messages (#21651) Closes #21564 Notably, RA will now rename module references if you change the source file name via our project panel. This PR is a tad bigger than necessary as I torn out the Model<> from didSave watchers (I tried to reuse that code for the same purpose). Release Notes: - Added support for language server actions being executed on file rename. --- crates/lsp/src/lsp.rs | 6 + crates/project/src/lsp_store.rs | 392 ++++++++++++++++++++------- crates/project/src/project.rs | 42 ++- crates/project/src/project_tests.rs | 135 ++++++++- crates/project/src/worktree_store.rs | 68 ++++- 5 files changed, 537 insertions(+), 106 deletions(-) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 8789f5f252..4f714cccc9 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -638,6 +638,12 @@ impl LanguageServer { snippet_edit_support: Some(true), ..WorkspaceEditClientCapabilities::default() }), + file_operations: Some(WorkspaceFileOperationsClientCapabilities { + dynamic_registration: Some(false), + did_rename: Some(true), + will_rename: Some(true), + ..Default::default() + }), ..Default::default() }), text_document: Some(TextDocumentClientCapabilities { diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index ff2a3d47e7..6a9acd3048 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -23,10 +23,10 @@ use futures::{ stream::FuturesUnordered, AsyncWriteExt, Future, FutureExt, StreamExt, }; -use globset::{Glob, GlobSet, GlobSetBuilder}; +use globset::{Glob, GlobBuilder, GlobMatcher, GlobSet, GlobSetBuilder}; use gpui::{ - AppContext, AsyncAppContext, Context, Entity, EventEmitter, Model, ModelContext, PromptLevel, - Task, WeakModel, + AppContext, AsyncAppContext, Entity, EventEmitter, Model, ModelContext, PromptLevel, Task, + WeakModel, }; use http_client::HttpClient; use itertools::Itertools as _; @@ -43,12 +43,13 @@ use language::{ Unclipped, }; use lsp::{ - CodeActionKind, CompletionContext, DiagnosticSeverity, DiagnosticTag, - DidChangeWatchedFilesRegistrationOptions, Edit, FileSystemWatcher, InsertTextFormat, - LanguageServer, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerId, - LanguageServerName, LspRequestFuture, MessageActionItem, MessageType, OneOf, - ServerHealthStatus, ServerStatus, SymbolKind, TextEdit, Url, WorkDoneProgressCancelParams, - WorkspaceFolder, + notification::DidRenameFiles, CodeActionKind, CompletionContext, DiagnosticSeverity, + DiagnosticTag, DidChangeWatchedFilesRegistrationOptions, Edit, FileOperationFilter, + FileOperationPatternKind, FileOperationRegistrationOptions, FileRename, FileSystemWatcher, + InsertTextFormat, LanguageServer, LanguageServerBinary, LanguageServerBinaryOptions, + LanguageServerId, LanguageServerName, LspRequestFuture, MessageActionItem, MessageType, OneOf, + RenameFilesParams, ServerHealthStatus, ServerStatus, SymbolKind, TextEdit, Url, + WillRenameFiles, WorkDoneProgressCancelParams, WorkspaceFolder, }; use node_runtime::read_package_installed_version; use parking_lot::{Mutex, RwLock}; @@ -139,7 +140,9 @@ pub struct LocalLspStore { pub language_servers: HashMap, buffers_being_formatted: HashSet, last_workspace_edits_by_language_server: HashMap, - language_server_watched_paths: HashMap>, + language_server_watched_paths: HashMap, + language_server_paths_watched_for_rename: + HashMap, language_server_watcher_registrations: HashMap>>, supplementary_language_servers: @@ -899,6 +902,7 @@ impl LspStore { language_servers: Default::default(), last_workspace_edits_by_language_server: Default::default(), language_server_watched_paths: Default::default(), + language_server_paths_watched_for_rename: Default::default(), language_server_watcher_registrations: Default::default(), current_lsp_settings: ProjectSettings::get_global(cx).lsp.clone(), buffers_being_formatted: Default::default(), @@ -4332,6 +4336,112 @@ impl LspStore { .map(|(key, value)| (*key, value)) } + pub(super) fn did_rename_entry( + &self, + worktree_id: WorktreeId, + old_path: &Path, + new_path: &Path, + is_dir: bool, + ) { + maybe!({ + let local_store = self.as_local()?; + + let old_uri = lsp::Url::from_file_path(old_path).ok().map(String::from)?; + let new_uri = lsp::Url::from_file_path(new_path).ok().map(String::from)?; + + for language_server in self.language_servers_for_worktree(worktree_id) { + let Some(filter) = local_store + .language_server_paths_watched_for_rename + .get(&language_server.server_id()) + else { + continue; + }; + + if filter.should_send_did_rename(&old_uri, is_dir) { + language_server + .notify::(RenameFilesParams { + files: vec![FileRename { + old_uri: old_uri.clone(), + new_uri: new_uri.clone(), + }], + }) + .log_err(); + } + } + Some(()) + }); + } + + pub(super) fn will_rename_entry( + this: WeakModel, + worktree_id: WorktreeId, + old_path: &Path, + new_path: &Path, + is_dir: bool, + cx: AsyncAppContext, + ) -> Task<()> { + let old_uri = lsp::Url::from_file_path(old_path).ok().map(String::from); + let new_uri = lsp::Url::from_file_path(new_path).ok().map(String::from); + cx.spawn(move |mut cx| async move { + let mut tasks = vec![]; + this.update(&mut cx, |this, cx| { + let local_store = this.as_local()?; + let old_uri = old_uri?; + let new_uri = new_uri?; + for language_server in this.language_servers_for_worktree(worktree_id) { + let Some(filter) = local_store + .language_server_paths_watched_for_rename + .get(&language_server.server_id()) + else { + continue; + }; + let Some(adapter) = + this.language_server_adapter_for_id(language_server.server_id()) + else { + continue; + }; + if filter.should_send_will_rename(&old_uri, is_dir) { + let apply_edit = cx.spawn({ + let old_uri = old_uri.clone(); + let new_uri = new_uri.clone(); + let language_server = language_server.clone(); + |this, mut cx| async move { + let edit = language_server + .request::(RenameFilesParams { + files: vec![FileRename { old_uri, new_uri }], + }) + .log_err() + .await + .flatten()?; + + Self::deserialize_workspace_edit( + this.upgrade()?, + edit, + false, + adapter.clone(), + language_server.clone(), + &mut cx, + ) + .await + .ok(); + Some(()) + } + }); + tasks.push(apply_edit); + } + } + Some(()) + }) + .ok() + .flatten(); + for task in tasks { + // Await on tasks sequentially so that the order of application of edits is deterministic + // (at least with regards to the order of registration of language servers) + task.await; + } + }) + } + fn lsp_notify_abs_paths_changed( &mut self, server_id: LanguageServerId, @@ -4369,6 +4479,32 @@ impl LspStore { language_server_id: LanguageServerId, cx: &mut ModelContext, ) { + let Some(watchers) = self.as_local().and_then(|local| { + local + .language_server_watcher_registrations + .get(&language_server_id) + }) else { + return; + }; + + let watch_builder = + self.rebuild_watched_paths_inner(language_server_id, watchers.values().flatten(), cx); + let Some(local_lsp_store) = self.as_local_mut() else { + return; + }; + let watcher = watch_builder.build(local_lsp_store.fs.clone(), language_server_id, cx); + local_lsp_store + .language_server_watched_paths + .insert(language_server_id, watcher); + + cx.notify(); + } + fn rebuild_watched_paths_inner<'a>( + &'a self, + language_server_id: LanguageServerId, + watchers: impl Iterator, + cx: &mut ModelContext, + ) -> LanguageServerWatchedPathsBuilder { let worktrees = self .worktree_store .read(cx) @@ -4380,15 +4516,6 @@ impl LspStore { }) .collect::>(); - let local_lsp_store = self.as_local_mut().unwrap(); - - let Some(watchers) = local_lsp_store - .language_server_watcher_registrations - .get(&language_server_id) - else { - return; - }; - let mut worktree_globs = HashMap::default(); let mut abs_globs = HashMap::default(); log::trace!( @@ -4406,7 +4533,7 @@ impl LspStore { pattern: String, }, } - for watcher in watchers.values().flatten() { + for watcher in watchers { let mut found_host = false; for worktree in &worktrees { let glob_is_inside_worktree = worktree.update(cx, |tree, _| { @@ -4545,12 +4672,7 @@ impl LspStore { watch_builder.watch_abs_path(abs_path, globset); } } - let watcher = watch_builder.build(local_lsp_store.fs.clone(), language_server_id, cx); - local_lsp_store - .language_server_watched_paths - .insert(language_server_id, watcher); - - cx.notify(); + watch_builder } pub fn language_server_for_id(&self, id: LanguageServerId) -> Option> { @@ -6650,6 +6772,23 @@ impl LspStore { simulate_disk_based_diagnostics_completion: None, }, ); + if let Some(file_ops_caps) = language_server + .capabilities() + .workspace + .as_ref() + .and_then(|ws| ws.file_operations.as_ref()) + { + let did_rename_caps = file_ops_caps.did_rename.as_ref(); + let will_rename_caps = file_ops_caps.will_rename.as_ref(); + if did_rename_caps.or(will_rename_caps).is_some() { + let watcher = RenamePathsWatchedForServer::default() + .with_did_rename_patterns(did_rename_caps) + .with_will_rename_patterns(will_rename_caps); + local + .language_server_paths_watched_for_rename + .insert(server_id, watcher); + } + } } self.language_server_statuses.insert( @@ -7010,7 +7149,7 @@ impl LspStore { if let Some(watched_paths) = local .language_server_watched_paths .get(server_id) - .and_then(|paths| paths.read(cx).worktree_paths.get(&worktree_id)) + .and_then(|paths| paths.worktree_paths.get(&worktree_id)) { let params = lsp::DidChangeWatchedFilesParams { changes: changes @@ -7115,7 +7254,7 @@ impl LspStore { Ok(transaction) } - pub async fn deserialize_workspace_edit( + pub(crate) async fn deserialize_workspace_edit( this: Model, edit: lsp::WorkspaceEdit, push_to_history: bool, @@ -7515,6 +7654,84 @@ pub enum LanguageServerToQuery { Other(LanguageServerId), } +#[derive(Default)] +struct RenamePathsWatchedForServer { + did_rename: Vec, + will_rename: Vec, +} + +impl RenamePathsWatchedForServer { + fn with_did_rename_patterns( + mut self, + did_rename: Option<&FileOperationRegistrationOptions>, + ) -> Self { + if let Some(did_rename) = did_rename { + self.did_rename = did_rename + .filters + .iter() + .filter_map(|filter| filter.try_into().log_err()) + .collect(); + } + self + } + fn with_will_rename_patterns( + mut self, + will_rename: Option<&FileOperationRegistrationOptions>, + ) -> Self { + if let Some(will_rename) = will_rename { + self.will_rename = will_rename + .filters + .iter() + .filter_map(|filter| filter.try_into().log_err()) + .collect(); + } + self + } + + fn should_send_did_rename(&self, path: &str, is_dir: bool) -> bool { + self.did_rename.iter().any(|pred| pred.eval(path, is_dir)) + } + fn should_send_will_rename(&self, path: &str, is_dir: bool) -> bool { + self.will_rename.iter().any(|pred| pred.eval(path, is_dir)) + } +} + +impl TryFrom<&FileOperationFilter> for RenameActionPredicate { + type Error = globset::Error; + fn try_from(ops: &FileOperationFilter) -> Result { + Ok(Self { + kind: ops.pattern.matches.clone(), + glob: GlobBuilder::new(&ops.pattern.glob) + .case_insensitive( + ops.pattern + .options + .as_ref() + .map_or(false, |ops| ops.ignore_case.unwrap_or(false)), + ) + .build()? + .compile_matcher(), + }) + } +} +struct RenameActionPredicate { + glob: GlobMatcher, + kind: Option, +} + +impl RenameActionPredicate { + // Returns true if language server should be notified + fn eval(&self, path: &str, is_dir: bool) -> bool { + self.kind.as_ref().map_or(true, |kind| { + let expected_kind = if is_dir { + FileOperationPatternKind::Folder + } else { + FileOperationPatternKind::File + }; + kind == &expected_kind + }) && self.glob.is_match(path) + } +} + #[derive(Default)] struct LanguageServerWatchedPaths { worktree_paths: HashMap, @@ -7539,78 +7756,65 @@ impl LanguageServerWatchedPathsBuilder { fs: Arc, language_server_id: LanguageServerId, cx: &mut ModelContext, - ) -> Model { + ) -> LanguageServerWatchedPaths { let project = cx.weak_model(); - cx.new_model(|cx| { - let this_id = cx.entity_id(); - const LSP_ABS_PATH_OBSERVE: Duration = Duration::from_millis(100); - let abs_paths = self - .abs_paths - .into_iter() - .map(|(abs_path, globset)| { - let task = cx.spawn({ - let abs_path = abs_path.clone(); - let fs = fs.clone(); + const LSP_ABS_PATH_OBSERVE: Duration = Duration::from_millis(100); + let abs_paths = self + .abs_paths + .into_iter() + .map(|(abs_path, globset)| { + let task = cx.spawn({ + let abs_path = abs_path.clone(); + let fs = fs.clone(); - let lsp_store = project.clone(); - |_, mut cx| async move { - maybe!(async move { - let mut push_updates = - fs.watch(&abs_path, LSP_ABS_PATH_OBSERVE).await; - while let Some(update) = push_updates.0.next().await { - let action = lsp_store - .update(&mut cx, |this, cx| { - let Some(local) = this.as_local() else { - return ControlFlow::Break(()); - }; - let Some(watcher) = local - .language_server_watched_paths - .get(&language_server_id) - else { - return ControlFlow::Break(()); - }; - if watcher.entity_id() != this_id { - // This watcher is no longer registered on the project, which means that we should - // cease operations. - return ControlFlow::Break(()); - } - let (globs, _) = watcher - .read(cx) - .abs_paths - .get(&abs_path) - .expect( - "Watched abs path is not registered with a watcher", - ); - let matching_entries = update - .into_iter() - .filter(|event| globs.is_match(&event.path)) - .collect::>(); - this.lsp_notify_abs_paths_changed( - language_server_id, - matching_entries, - ); - ControlFlow::Continue(()) - }) - .ok()?; + let lsp_store = project.clone(); + |_, mut cx| async move { + maybe!(async move { + let mut push_updates = fs.watch(&abs_path, LSP_ABS_PATH_OBSERVE).await; + while let Some(update) = push_updates.0.next().await { + let action = lsp_store + .update(&mut cx, |this, _| { + let Some(local) = this.as_local() else { + return ControlFlow::Break(()); + }; + let Some(watcher) = local + .language_server_watched_paths + .get(&language_server_id) + else { + return ControlFlow::Break(()); + }; + let (globs, _) = watcher.abs_paths.get(&abs_path).expect( + "Watched abs path is not registered with a watcher", + ); + let matching_entries = update + .into_iter() + .filter(|event| globs.is_match(&event.path)) + .collect::>(); + this.lsp_notify_abs_paths_changed( + language_server_id, + matching_entries, + ); + ControlFlow::Continue(()) + }) + .ok()?; - if action.is_break() { - break; - } - } - Some(()) - }) - .await; + if action.is_break() { + break; + } } - }); - (abs_path, (globset, task)) - }) - .collect(); - LanguageServerWatchedPaths { - worktree_paths: self.worktree_paths, - abs_paths, - } + Some(()) + }) + .await; + } + }); + (abs_path, (globset, task)) }) + .collect(); + LanguageServerWatchedPaths { + worktree_paths: self.worktree_paths, + abs_paths, + } } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 84aedab92b..6ab800460e 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -583,6 +583,8 @@ impl Project { client.add_model_request_handler(Self::handle_open_new_buffer); client.add_model_message_handler(Self::handle_create_buffer_for_peer); + client.add_model_request_handler(WorktreeStore::handle_rename_project_entry); + WorktreeStore::init(&client); BufferStore::init(&client); LspStore::init(&client); @@ -1489,11 +1491,45 @@ impl Project { new_path: impl Into>, cx: &mut ModelContext, ) -> Task> { - let Some(worktree) = self.worktree_for_entry(entry_id, cx) else { + let worktree_store = self.worktree_store.read(cx); + let new_path = new_path.into(); + let Some((worktree, old_path, is_dir)) = worktree_store + .worktree_and_entry_for_id(entry_id, cx) + .map(|(worktree, entry)| (worktree, entry.path.clone(), entry.is_dir())) + else { return Task::ready(Err(anyhow!(format!("No worktree for entry {entry_id:?}")))); }; - worktree.update(cx, |worktree, cx| { - worktree.rename_entry(entry_id, new_path, cx) + + let worktree_id = worktree.read(cx).id(); + + let lsp_store = self.lsp_store().downgrade(); + cx.spawn(|_, mut cx| async move { + let (old_abs_path, new_abs_path) = { + let root_path = worktree.update(&mut cx, |this, _| this.abs_path())?; + (root_path.join(&old_path), root_path.join(&new_path)) + }; + LspStore::will_rename_entry( + lsp_store.clone(), + worktree_id, + &old_abs_path, + &new_abs_path, + is_dir, + cx.clone(), + ) + .await; + + let entry = worktree + .update(&mut cx, |worktree, cx| { + worktree.rename_entry(entry_id, new_path.clone(), cx) + })? + .await?; + + lsp_store + .update(&mut cx, |this, _| { + this.did_rename_entry(worktree_id, &old_abs_path, &new_abs_path, is_dir); + }) + .ok(); + Ok(entry) }) } diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 26537503dc..0bd681a588 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -9,12 +9,16 @@ use language::{ tree_sitter_rust, tree_sitter_typescript, Diagnostic, DiagnosticSet, DiskState, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageName, LineEnding, OffsetRangeExt, Point, ToPoint, }; -use lsp::{DiagnosticSeverity, NumberOrString}; +use lsp::{ + notification::DidRenameFiles, DiagnosticSeverity, DocumentChanges, FileOperationFilter, + NumberOrString, TextDocumentEdit, WillRenameFiles, +}; use parking_lot::Mutex; use pretty_assertions::{assert_eq, assert_matches}; use serde_json::json; #[cfg(not(windows))] use std::os; +use std::{str::FromStr, sync::OnceLock}; use std::{mem, num::NonZeroU32, ops::Range, task::Poll}; use task::{ResolvedTask, TaskContext}; @@ -3915,6 +3919,135 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) { ); } +#[gpui::test] +async fn test_lsp_rename_notifications(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + "/dir", + json!({ + "one.rs": "const ONE: usize = 1;", + "two": { + "two.rs": "const TWO: usize = one::ONE + one::ONE;" + } + + }), + ) + .await; + let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + language_registry.add(rust_lang()); + let watched_paths = lsp::FileOperationRegistrationOptions { + filters: vec![ + FileOperationFilter { + scheme: Some("file".to_owned()), + pattern: lsp::FileOperationPattern { + glob: "**/*.rs".to_owned(), + matches: Some(lsp::FileOperationPatternKind::File), + options: None, + }, + }, + FileOperationFilter { + scheme: Some("file".to_owned()), + pattern: lsp::FileOperationPattern { + glob: "**/**".to_owned(), + matches: Some(lsp::FileOperationPatternKind::Folder), + options: None, + }, + }, + ], + }; + let mut fake_servers = language_registry.register_fake_lsp( + "Rust", + FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + workspace: Some(lsp::WorkspaceServerCapabilities { + workspace_folders: None, + file_operations: Some(lsp::WorkspaceFileOperationsServerCapabilities { + did_rename: Some(watched_paths.clone()), + will_rename: Some(watched_paths), + ..Default::default() + }), + }), + ..Default::default() + }, + ..Default::default() + }, + ); + + let _ = project + .update(cx, |project, cx| { + project.open_local_buffer("/dir/one.rs", cx) + }) + .await + .unwrap(); + + let fake_server = fake_servers.next().await.unwrap(); + let response = project.update(cx, |project, cx| { + let worktree = project.worktrees(cx).next().unwrap(); + let entry = worktree.read(cx).entry_for_path("one.rs").unwrap(); + project.rename_entry(entry.id, "three.rs".as_ref(), cx) + }); + let expected_edit = lsp::WorkspaceEdit { + changes: None, + document_changes: Some(DocumentChanges::Edits({ + vec![TextDocumentEdit { + edits: vec![lsp::Edit::Plain(lsp::TextEdit { + range: lsp::Range { + start: lsp::Position { + line: 0, + character: 1, + }, + end: lsp::Position { + line: 0, + character: 3, + }, + }, + new_text: "This is not a drill".to_owned(), + })], + text_document: lsp::OptionalVersionedTextDocumentIdentifier { + uri: Url::from_str("file:///dir/two/two.rs").unwrap(), + version: Some(1337), + }, + }] + })), + change_annotations: None, + }; + let resolved_workspace_edit = Arc::new(OnceLock::new()); + fake_server + .handle_request::({ + let resolved_workspace_edit = resolved_workspace_edit.clone(); + let expected_edit = expected_edit.clone(); + move |params, _| { + let resolved_workspace_edit = resolved_workspace_edit.clone(); + let expected_edit = expected_edit.clone(); + async move { + assert_eq!(params.files.len(), 1); + assert_eq!(params.files[0].old_uri, "file:///dir/one.rs"); + assert_eq!(params.files[0].new_uri, "file:///dir/three.rs"); + resolved_workspace_edit.set(expected_edit.clone()).unwrap(); + Ok(Some(expected_edit)) + } + } + }) + .next() + .await + .unwrap(); + let _ = response.await.unwrap(); + fake_server + .handle_notification::(|params, _| { + assert_eq!(params.files.len(), 1); + assert_eq!(params.files[0].old_uri, "file:///dir/one.rs"); + assert_eq!(params.files[0].new_uri, "file:///dir/three.rs"); + }) + .next() + .await + .unwrap(); + assert_eq!(resolved_workspace_edit.get(), Some(&expected_edit)); +} + #[gpui::test] async fn test_rename(cx: &mut gpui::TestAppContext) { // hi diff --git a/crates/project/src/worktree_store.rs b/crates/project/src/worktree_store.rs index 1e48cc052e..c39b88cd40 100644 --- a/crates/project/src/worktree_store.rs +++ b/crates/project/src/worktree_store.rs @@ -26,7 +26,7 @@ use text::ReplicaId; use util::{paths::SanitizedPath, ResultExt}; use worktree::{Entry, ProjectEntryId, Worktree, WorktreeId, WorktreeSettings}; -use crate::{search::SearchQuery, ProjectPath}; +use crate::{search::SearchQuery, LspStore, ProjectPath}; struct MatchingEntry { worktree_path: Arc, @@ -69,7 +69,6 @@ impl EventEmitter for WorktreeStore {} impl WorktreeStore { pub fn init(client: &AnyProtoClient) { client.add_model_request_handler(Self::handle_create_project_entry); - client.add_model_request_handler(Self::handle_rename_project_entry); client.add_model_request_handler(Self::handle_copy_project_entry); client.add_model_request_handler(Self::handle_delete_project_entry); client.add_model_request_handler(Self::handle_expand_project_entry); @@ -184,6 +183,19 @@ impl WorktreeStore { .find_map(|worktree| worktree.read(cx).entry_for_id(entry_id)) } + pub fn worktree_and_entry_for_id<'a>( + &'a self, + entry_id: ProjectEntryId, + cx: &'a AppContext, + ) -> Option<(Model, &'a Entry)> { + self.worktrees().find_map(|worktree| { + worktree + .read(cx) + .entry_for_id(entry_id) + .map(|e| (worktree.clone(), e)) + }) + } + pub fn entry_for_path(&self, path: &ProjectPath, cx: &AppContext) -> Option { self.worktree_for_id(path.worktree_id, cx)? .read(cx) @@ -1004,16 +1016,56 @@ impl WorktreeStore { } pub async fn handle_rename_project_entry( - this: Model, + this: Model, envelope: TypedEnvelope, mut cx: AsyncAppContext, ) -> Result { let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id); - let worktree = this.update(&mut cx, |this, cx| { - this.worktree_for_entry(entry_id, cx) - .ok_or_else(|| anyhow!("worktree not found")) - })??; - Worktree::handle_rename_entry(worktree, envelope.payload, cx).await + let (worktree_id, worktree, old_path, is_dir) = this + .update(&mut cx, |this, cx| { + this.worktree_store + .read(cx) + .worktree_and_entry_for_id(entry_id, cx) + .map(|(worktree, entry)| { + ( + worktree.read(cx).id(), + worktree, + entry.path.clone(), + entry.is_dir(), + ) + }) + })? + .ok_or_else(|| anyhow!("worktree not found"))?; + let (old_abs_path, new_abs_path) = { + let root_path = worktree.update(&mut cx, |this, _| this.abs_path())?; + ( + root_path.join(&old_path), + root_path.join(&envelope.payload.new_path), + ) + }; + let lsp_store = this + .update(&mut cx, |this, _| this.lsp_store())? + .downgrade(); + LspStore::will_rename_entry( + lsp_store, + worktree_id, + &old_abs_path, + &new_abs_path, + is_dir, + cx.clone(), + ) + .await; + let response = Worktree::handle_rename_entry(worktree, envelope.payload, cx.clone()).await; + this.update(&mut cx, |this, cx| { + this.lsp_store().read(cx).did_rename_entry( + worktree_id, + &old_abs_path, + &new_abs_path, + is_dir, + ); + }) + .ok(); + response } pub async fn handle_copy_project_entry( From fdc7751457aa899344f8436ac67cffcda65a3f6d Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Sat, 7 Dec 2024 14:52:55 +0100 Subject: [PATCH 341/886] toolchains: Do not use as_json representation for PartialEq (#21682) Closes #21679 Release Notes: - N/A --- crates/language/src/toolchain.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/crates/language/src/toolchain.rs b/crates/language/src/toolchain.rs index 13703d81a7..5b48157f0f 100644 --- a/crates/language/src/toolchain.rs +++ b/crates/language/src/toolchain.rs @@ -14,7 +14,7 @@ use settings::WorktreeId; use crate::LanguageName; /// Represents a single toolchain. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] pub struct Toolchain { /// User-facing label pub name: SharedString, @@ -24,6 +24,18 @@ pub struct Toolchain { pub as_json: serde_json::Value, } +impl PartialEq for Toolchain { + fn eq(&self, other: &Self) -> bool { + // Do not use as_json for comparisons; it shouldn't impact equality, as it's not user-surfaced. + // Thus, there could be multiple entries that look the same in the UI. + (&self.name, &self.path, &self.language_name).eq(&( + &other.name, + &other.path, + &other.language_name, + )) + } +} + #[async_trait] pub trait ToolchainLister: Send + Sync { async fn list( From eb3d3eaebfdc559105e5f09cec9e162200e3ad1c Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Sat, 7 Dec 2024 11:00:31 -0300 Subject: [PATCH 342/886] Adjust diagnostic in tabs behavior (#21671) Follow up to https://github.com/zed-industries/zed/pull/21637 After discussing about this feature with the team, we've decided that diagnostic display in tabs should be: 1) turned off by default, and 2) only shown when there are file icons. The main reason here being to keep Zed's UI uncluttered. This means that you can technically have this setting: ``` "tabs": { "show_diagnostics": "all" }, ``` ...and still don't see any diagnostics because you're missing `file_icons": true`. | Error with file icons | Error with no file icons | |--------|--------| | Screenshot 2024-12-06 at 21 05 13 | Screenshot 2024-12-06 at 21 05 24 | Release Notes: - N/A --- assets/settings/default.json | 5 +++-- crates/workspace/src/item.rs | 4 ++-- crates/workspace/src/pane.rs | 26 +++++++++++++------------- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index dd9098e0c0..20819529ff 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -569,7 +569,8 @@ // "Neighbour" "activate_on_close": "history", /// Which files containing diagnostic errors/warnings to mark in the tabs. - /// This setting can take the following three values: + /// Diagnostics are only shown when file icons are also active. + /// This setting only works when can take the following three values: /// /// 1. Do not mark any files: /// "off" @@ -577,7 +578,7 @@ /// "errors" /// 3. Mark files with errors and warnings: /// "all" - "show_diagnostics": "all" + "show_diagnostics": "off" }, // Settings related to preview tabs. "preview_tabs": { diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 97c27b52a1..7b9478a9a7 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -64,9 +64,9 @@ pub enum ClosePosition { #[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum ShowDiagnostics { + #[default] Off, Errors, - #[default] All, } @@ -99,7 +99,7 @@ pub struct ItemSettingsContent { /// Which files containing diagnostic errors/warnings to mark in the tabs. /// This setting can take the following three values: /// - /// Default: all + /// Default: off show_diagnostics: Option, /// Whether to always show the close button on tabs. /// diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index d213ab630b..a4ca58c11c 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -2002,12 +2002,8 @@ impl Pane { let icon = if decorated_icon.is_none() { match item_diagnostic { - Some(&DiagnosticSeverity::ERROR) => { - Some(Icon::new(IconName::X).color(Color::Error)) - } - Some(&DiagnosticSeverity::WARNING) => { - Some(Icon::new(IconName::Triangle).color(Color::Warning)) - } + Some(&DiagnosticSeverity::ERROR) => None, + Some(&DiagnosticSeverity::WARNING) => None, _ => item.tab_icon(cx).map(|icon| icon.color(Color::Muted)), } .map(|icon| icon.size(IconSize::Small)) @@ -2144,13 +2140,17 @@ impl Pane { .child( h_flex() .gap_1() - .child(if let Some(decorated_icon) = decorated_icon { - div().child(decorated_icon.into_any_element()) - } else if let Some(icon) = icon { - div().mt(px(2.5)).child(icon.into_any_element()) - } else { - div() - }) + .items_center() + .children( + std::iter::once(if let Some(decorated_icon) = decorated_icon { + Some(div().child(decorated_icon.into_any_element())) + } else if let Some(icon) = icon { + Some(div().child(icon.into_any_element())) + } else { + None + }) + .flatten(), + ) .child(label), ); From c5b6d78d5b01e0fcd243499bf3fb09cb1c758902 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Sat, 7 Dec 2024 12:56:52 -0500 Subject: [PATCH 343/886] project_diff: Keep going after failing to rescan a buffer (#21673) I ran into a case locally where the project diff view was unexpectedly empty because the first file to be scanned wasn't valid UTF-8, and the inmost loop in `schedule_worktree_rescan` currently breaks when any loading task fails. It seems like it might make more sense to continue with the rest of the buffers in this case and also when `Project::open_unstaged_changes` fails. I've left the error handling for `update` as-is. Release Notes: - Fix project diff view missing files --- crates/editor/src/git/project_diff.rs | 34 +++++++++++++++++---------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/crates/editor/src/git/project_diff.rs b/crates/editor/src/git/project_diff.rs index 8ececa9bb8..8fb600c52c 100644 --- a/crates/editor/src/git/project_diff.rs +++ b/crates/editor/src/git/project_diff.rs @@ -6,7 +6,7 @@ use std::{ time::Duration, }; -use anyhow::Context as _; +use anyhow::{anyhow, Context as _}; use collections::{BTreeMap, HashMap}; use feature_flags::FeatureFlagAppExt; use git::{ @@ -235,22 +235,30 @@ impl ProjectDiffEditor { >::default(); let mut change_sets = Vec::new(); for (status, entry_id, entry_path, open_task) in open_tasks { - let (_, opened_model) = open_task.await.with_context(|| { - format!("loading buffer {:?} for git diff", entry_path.path) - })?; - let buffer = match opened_model.downcast::() { - Ok(buffer) => buffer, - Err(_model) => anyhow::bail!( - "Could not load {:?} as a buffer for git diff", - entry_path.path - ), + let Some(buffer) = open_task + .await + .and_then(|(_, opened_model)| { + opened_model + .downcast::() + .map_err(|_| anyhow!("Unexpected non-buffer")) + }) + .with_context(|| { + format!("loading {} for git diff", entry_path.path.display()) + }) + .log_err() + else { + continue; }; - let change_set = project + let Some(change_set) = project .update(&mut cx, |project, cx| { project.open_unstaged_changes(buffer.clone(), cx) })? - .await?; + .await + .log_err() + else { + continue; + }; cx.update(|cx| { buffers.insert( @@ -267,7 +275,7 @@ impl ProjectDiffEditor { new_entries.push((entry_path, entry_id)); } - Ok((buffers, new_entries, change_sets)) + anyhow::Ok((buffers, new_entries, change_sets)) }) .await .log_err() From 4b93a5ca4469ef6c3926b186fc2c4f7476ad69dd Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Sun, 8 Dec 2024 09:44:48 -0700 Subject: [PATCH 344/886] Make completions selector continue to show docs aside if ever shown (#21704) In #21286, documentation fetch was made more efficient by only fetching the current completion. This has a side effect of causing the aside to disappear and reappear when navigating the list. This is particularly jarring when there isn't enough space for the aside, causing the completions list to jump to the left. The solution here is to continue to show the aside even if the current selection does not yet have docs fetched. Release Notes: - N/A --- crates/editor/src/editor.rs | 50 ++++++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 0bd30465d9..b2abe8db80 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -141,7 +141,7 @@ use snippet::Snippet; use std::{ any::TypeId, borrow::Cow, - cell::RefCell, + cell::{Cell, RefCell}, cmp::{self, Ordering, Reverse}, mem, num::NonZeroU32, @@ -1008,6 +1008,7 @@ struct CompletionsMenu { selected_item: usize, scroll_handle: UniformListScrollHandle, selected_completion_resolve_debounce: Option>>, + aside_was_displayed: Cell, } impl CompletionsMenu { @@ -1040,6 +1041,7 @@ impl CompletionsMenu { selected_item: 0, scroll_handle: UniformListScrollHandle::new(), selected_completion_resolve_debounce: Some(Arc::new(Mutex::new(DebouncedDelay::new()))), + aside_was_displayed: Cell::new(false), } } @@ -1093,6 +1095,7 @@ impl CompletionsMenu { selected_item: 0, scroll_handle: UniformListScrollHandle::new(), selected_completion_resolve_debounce: Some(Arc::new(Mutex::new(DebouncedDelay::new()))), + aside_was_displayed: Cell::new(false), } } @@ -1231,7 +1234,7 @@ impl CompletionsMenu { let multiline_docs = if show_completion_documentation { let mat = &self.matches[selected_item]; - let multiline_docs = match &self.completions.read()[mat.candidate_id].documentation { + match &self.completions.read()[mat.candidate_id].documentation { Some(Documentation::MultiLinePlainText(text)) => { Some(div().child(SharedString::from(text.clone()))) } @@ -1244,24 +1247,37 @@ impl CompletionsMenu { cx, ))) } + Some(Documentation::Undocumented) if self.aside_was_displayed.get() => { + Some(div().child("No documentation")) + } _ => None, - }; - multiline_docs.map(|div| { - div.id("multiline_docs") - .max_h(max_height) - .flex_1() - .px_1p5() - .py_1() - .min_w(px(260.)) - .max_w(px(640.)) - .w(px(500.)) - .overflow_y_scroll() - .occlude() - }) + } } else { None }; + let aside_contents = if let Some(multiline_docs) = multiline_docs { + Some(multiline_docs) + } else if self.aside_was_displayed.get() { + Some(div().child("Fetching documentation...")) + } else { + None + }; + self.aside_was_displayed.set(aside_contents.is_some()); + + let aside_contents = aside_contents.map(|div| { + div.id("multiline_docs") + .max_h(max_height) + .flex_1() + .px_1p5() + .py_1() + .min_w(px(260.)) + .max_w(px(640.)) + .w(px(500.)) + .overflow_y_scroll() + .occlude() + }); + let list = uniform_list( cx.view().clone(), "completions", @@ -1357,8 +1373,8 @@ impl CompletionsMenu { Popover::new() .child(list) - .when_some(multiline_docs, |popover, multiline_docs| { - popover.aside(multiline_docs) + .when_some(aside_contents, |popover, aside_contents| { + popover.aside(aside_contents) }) .into_any_element() } From ac07b9197a5a2814dad605148fe032f6777ce5fe Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Sun, 8 Dec 2024 13:30:23 -0500 Subject: [PATCH 345/886] gpui: Don't panic on failing to set X11 cursor style (#21689) One more panic (well, two) that should be a `log_err`. Release Notes: - N/A --- crates/gpui/src/platform/linux/x11/client.rs | 8 +++++--- crates/util/src/util.rs | 10 ++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index 1fd0e9aa66..a0c9ab4794 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -9,6 +9,7 @@ use std::time::{Duration, Instant}; use calloop::generic::{FdWrapper, Generic}; use calloop::{EventLoop, LoopHandle, RegistrationToken}; +use anyhow::Context as _; use collections::HashMap; use http_client::Url; use smallvec::SmallVec; @@ -1417,9 +1418,10 @@ impl LinuxClient for X11Client { ..Default::default() }, ) - .expect("failed to change window cursor") - .check() - .unwrap(); + .anyhow() + .and_then(|cookie| cookie.check().anyhow()) + .context("setting cursor style") + .log_err(); } fn open_uri(&self, uri: &str) { diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index fe3f7ef9a0..777b8b60dc 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -206,6 +206,9 @@ pub trait ResultExt { /// Assert that this result should never be an error in development or tests. fn debug_assert_ok(self, reason: &str) -> Self; fn warn_on_err(self) -> Option; + fn anyhow(self) -> anyhow::Result + where + E: Into; } impl ResultExt for Result @@ -243,6 +246,13 @@ where } } } + + fn anyhow(self) -> anyhow::Result + where + E: Into, + { + self.map_err(Into::into) + } } fn log_error_with_caller(caller: core::panic::Location<'_>, error: E, level: log::Level) From d0e99f649655e5d3697185fb52b73409a41294ee Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Sun, 8 Dec 2024 18:42:44 -0700 Subject: [PATCH 346/886] Bump x11rb version to v0.13.1 (#21723) From diff looks like no material differences. With a local checkout of `v0.13.0` I get build errors due to warning checking when I use a `path = ...` dependency, but it is fixed with `v0.13.1`. I see mention of this in the [renovate configuration PR](https://github.com/zed-industries/zed/pull/15132) but doesn't seem like that initial batch of renovation happened. Release Notes: - N/A --- crates/gpui/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index ed523c769a..347d70853a 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -181,7 +181,7 @@ wayland-protocols-plasma = { version = "0.2.0", features = [ # X11 as-raw-xcb-connection = { version = "1", optional = true } -x11rb = { version = "0.13.0", features = [ +x11rb = { version = "0.13.1", features = [ "allow-unsafe-code", "xkb", "randr", @@ -198,7 +198,7 @@ xim = { git = "https://github.com/XDeme1/xim-rs", rev = "d50d461764c2213655cd9cf "x11rb-xcb", "x11rb-client", ], optional = true } -x11-clipboard = { version = "0.9.2", optional = true } +x11-clipboard = { version = "0.9.3", optional = true } [target.'cfg(windows)'.dependencies] blade-util.workspace = true From bf1525588dfba78491b0647a2102a54cd3212c62 Mon Sep 17 00:00:00 2001 From: Hendrik <7716993+DD5HT@users.noreply.github.com> Date: Mon, 9 Dec 2024 02:44:46 +0100 Subject: [PATCH 347/886] Add .jj to default file exclusion (#21708) Relates to #21538 Release Notes: - Added `**/.jj` to the default file exclusion list. --- assets/settings/default.json | 1 + crates/worktree/src/worktree_settings.rs | 1 + docs/src/configuring-zed.md | 1 + 3 files changed, 3 insertions(+) diff --git a/assets/settings/default.json b/assets/settings/default.json index 20819529ff..3b78580610 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -685,6 +685,7 @@ "**/.git", "**/.svn", "**/.hg", + "**/.jj", "**/CVS", "**/.DS_Store", "**/Thumbs.db", diff --git a/crates/worktree/src/worktree_settings.rs b/crates/worktree/src/worktree_settings.rs index f26dc4af0f..9535264c92 100644 --- a/crates/worktree/src/worktree_settings.rs +++ b/crates/worktree/src/worktree_settings.rs @@ -40,6 +40,7 @@ pub struct WorktreeSettingsContent { /// "**/.git", /// "**/.svn", /// "**/.hg", + /// "**/.jj", /// "**/CVS", /// "**/.DS_Store", /// "**/Thumbs.db", diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index d4f8c40dbd..b51b01a1e7 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -994,6 +994,7 @@ The result is still `)))` and not `))))))`, which is what it would be by default "**/.git", "**/.svn", "**/.hg", + "**/.jj" "**/CVS", "**/.DS_Store", "**/Thumbs.db", From 2ce01ead93f9d7b5f307ae0c016cd9495084e28c Mon Sep 17 00:00:00 2001 From: tims <0xtimsb@gmail.com> Date: Mon, 9 Dec 2024 07:43:12 +0530 Subject: [PATCH 348/886] Fix right click selection behavior in project panel (#21707) Closes #21605 Consider you have set of entries selected or even a single entry selected, and you right click some other entry which is **not** part of your selected set. This doesn't not clear existing entries selection (which it should clear, as how file manager right-click logic works, see more below). This issue might lead unexpected operation like deletion applied on those existing selected entries. This PR fixes it. Release Notes: - Fix right click selection behavior in project panel --- crates/project_panel/src/project_panel.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 12c90e2195..7433d0599f 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -3496,6 +3496,9 @@ impl ProjectPanel { // Stop propagation to prevent the catch-all context menu for the project // panel from being deployed. cx.stop_propagation(); + if !this.marked_entries.contains(&selection) { + this.marked_entries.clear(); + } this.deploy_context_menu(event.position, entry_id, cx); }, )) From 55ee72d84ab90c6f4b91aa25a3c9cf7f5dc799d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Marcos?= Date: Mon, 9 Dec 2024 00:27:54 -0300 Subject: [PATCH 349/886] Simplify TextHighlights map (#21724) Remove unnecessary `Option` wrapping. --- crates/editor/src/display_map.rs | 10 +++++----- crates/editor/src/display_map/inlay_map.rs | 14 +++++++------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index a75c2ce9fa..2c62295a29 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -82,7 +82,7 @@ pub trait ToDisplayPoint { fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint; } -type TextHighlights = TreeMap, Arc<(HighlightStyle, Vec>)>>; +type TextHighlights = TreeMap>)>>; type InlayHighlights = TreeMap>; /// Decides how text in a [`MultiBuffer`] should be displayed in a buffer, handling inlay hints, @@ -434,7 +434,7 @@ impl DisplayMap { style: HighlightStyle, ) { self.text_highlights - .insert(Some(type_id), Arc::new((style, ranges))); + .insert(type_id, Arc::new((style, ranges))); } pub(crate) fn highlight_inlays( @@ -457,11 +457,11 @@ impl DisplayMap { } pub fn text_highlights(&self, type_id: TypeId) -> Option<(HighlightStyle, &[Range])> { - let highlights = self.text_highlights.get(&Some(type_id))?; + let highlights = self.text_highlights.get(&type_id)?; Some((highlights.0, &highlights.1)) } pub fn clear_highlights(&mut self, type_id: TypeId) -> bool { - let mut cleared = self.text_highlights.remove(&Some(type_id)).is_some(); + let mut cleared = self.text_highlights.remove(&type_id).is_some(); cleared |= self.inlay_highlights.remove(&type_id).is_some(); cleared } @@ -1239,7 +1239,7 @@ impl DisplaySnapshot { &self, ) -> Option>)>> { let type_id = TypeId::of::(); - self.text_highlights.get(&Some(type_id)).cloned() + self.text_highlights.get(&type_id).cloned() } #[allow(unused)] diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 4598a5c015..e4884d3c43 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -211,7 +211,7 @@ pub struct InlayBufferRows<'a> { struct HighlightEndpoint { offset: InlayOffset, is_start: bool, - tag: Option, + tag: TypeId, style: HighlightStyle, } @@ -239,7 +239,7 @@ pub struct InlayChunks<'a> { max_output_offset: InlayOffset, highlight_styles: HighlightStyles, highlight_endpoints: Peekable>, - active_highlights: BTreeMap, HighlightStyle>, + active_highlights: BTreeMap, highlights: Highlights<'a>, snapshot: &'a InlaySnapshot, } @@ -1096,7 +1096,7 @@ impl InlaySnapshot { &self, cursor: &mut Cursor<'_, Transform, (InlayOffset, usize)>, range: &Range, - text_highlights: &TreeMap, Arc<(HighlightStyle, Vec>)>>, + text_highlights: &TreeMap>)>>, highlight_endpoints: &mut Vec, ) { while cursor.start().0 < range.end { @@ -1112,7 +1112,7 @@ impl InlaySnapshot { ))) }; - for (tag, text_highlights) in text_highlights.iter() { + for (&tag, text_highlights) in text_highlights.iter() { let style = text_highlights.0; let ranges = &text_highlights.1; @@ -1134,13 +1134,13 @@ impl InlaySnapshot { highlight_endpoints.push(HighlightEndpoint { offset: self.to_inlay_offset(range.start.to_offset(&self.buffer)), is_start: true, - tag: *tag, + tag, style, }); highlight_endpoints.push(HighlightEndpoint { offset: self.to_inlay_offset(range.end.to_offset(&self.buffer)), is_start: false, - tag: *tag, + tag, style, }); } @@ -1708,7 +1708,7 @@ mod tests { text_highlight_ranges.sort_by_key(|range| (range.start, Reverse(range.end))); log::info!("highlighting text ranges {text_highlight_ranges:?}"); text_highlights.insert( - Some(TypeId::of::<()>()), + TypeId::of::<()>(), Arc::new(( HighlightStyle::default(), text_highlight_ranges From 4564273322dfbc34f20232474fe40c9988d84aaa Mon Sep 17 00:00:00 2001 From: "mgsloan@gmail.com" Date: Sun, 8 Dec 2024 21:21:16 -0700 Subject: [PATCH 350/886] Add comment explaining project panel behavior on right-click outside selection --- crates/project_panel/src/project_panel.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 7433d0599f..f30c4b91e2 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -3496,6 +3496,10 @@ impl ProjectPanel { // Stop propagation to prevent the catch-all context menu for the project // panel from being deployed. cx.stop_propagation(); + // Some context menu actions apply to all marked entries. If the user + // right-clicks on an entry that is not marked, they may not realize the + // action applies to multiple entries. To avoid inadvertent changes, all + // entries are unmarked. if !this.marked_entries.contains(&selection) { this.marked_entries.clear(); } From e58cdca044a45e2df296fc3bf831ad1ec043405d Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Mon, 9 Dec 2024 12:17:51 +0100 Subject: [PATCH 351/886] Added JavaScript runnable detection for `context` and `suite` methods (#21719) Fixes https://github.com/zed-industries/zed/pull/21246#issuecomment-2525578141 Screenshot 2024-12-08 at 22 58 33 Screenshot 2024-12-08 at 22 58 44 Release Notes: - Added JavaScript runnable detection for `context` and `suite` methods for mochajs framework --- crates/languages/src/javascript/outline.scm | 2 +- crates/languages/src/javascript/runnables.scm | 2 +- crates/languages/src/tsx/outline.scm | 2 +- crates/languages/src/tsx/runnables.scm | 2 +- crates/languages/src/typescript/outline.scm | 2 +- crates/languages/src/typescript/runnables.scm | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/languages/src/javascript/outline.scm b/crates/languages/src/javascript/outline.scm index 0159d452cc..d70d8bb597 100644 --- a/crates/languages/src/javascript/outline.scm +++ b/crates/languages/src/javascript/outline.scm @@ -73,7 +73,7 @@ ] ) ] @context - (#any-of? @_name "it" "test" "describe") + (#any-of? @_name "it" "test" "describe" "context" "suite") arguments: ( arguments . (string (string_fragment) @name) ) diff --git a/crates/languages/src/javascript/runnables.scm b/crates/languages/src/javascript/runnables.scm index af619dacb7..1b68b9a41e 100644 --- a/crates/languages/src/javascript/runnables.scm +++ b/crates/languages/src/javascript/runnables.scm @@ -11,7 +11,7 @@ ] ) ] - (#any-of? @_name "it" "test" "describe") + (#any-of? @_name "it" "test" "describe" "context" "suite") arguments: ( arguments . (string (string_fragment) @run) ) diff --git a/crates/languages/src/tsx/outline.scm b/crates/languages/src/tsx/outline.scm index 34b80b733b..c0c5c735e2 100644 --- a/crates/languages/src/tsx/outline.scm +++ b/crates/languages/src/tsx/outline.scm @@ -81,7 +81,7 @@ ] ) ] @context - (#any-of? @_name "it" "test" "describe") + (#any-of? @_name "it" "test" "describe" "context" "suite") arguments: ( arguments . (string (string_fragment) @name) ) diff --git a/crates/languages/src/tsx/runnables.scm b/crates/languages/src/tsx/runnables.scm index af619dacb7..1b68b9a41e 100644 --- a/crates/languages/src/tsx/runnables.scm +++ b/crates/languages/src/tsx/runnables.scm @@ -11,7 +11,7 @@ ] ) ] - (#any-of? @_name "it" "test" "describe") + (#any-of? @_name "it" "test" "describe" "context" "suite") arguments: ( arguments . (string (string_fragment) @run) ) diff --git a/crates/languages/src/typescript/outline.scm b/crates/languages/src/typescript/outline.scm index 34b80b733b..c0c5c735e2 100644 --- a/crates/languages/src/typescript/outline.scm +++ b/crates/languages/src/typescript/outline.scm @@ -81,7 +81,7 @@ ] ) ] @context - (#any-of? @_name "it" "test" "describe") + (#any-of? @_name "it" "test" "describe" "context" "suite") arguments: ( arguments . (string (string_fragment) @name) ) diff --git a/crates/languages/src/typescript/runnables.scm b/crates/languages/src/typescript/runnables.scm index af619dacb7..1b68b9a41e 100644 --- a/crates/languages/src/typescript/runnables.scm +++ b/crates/languages/src/typescript/runnables.scm @@ -11,7 +11,7 @@ ] ) ] - (#any-of? @_name "it" "test" "describe") + (#any-of? @_name "it" "test" "describe" "context" "suite") arguments: ( arguments . (string (string_fragment) @run) ) From ce9e4629becc759c1e94248a6d180df1676fc5db Mon Sep 17 00:00:00 2001 From: Nils Koch Date: Mon, 9 Dec 2024 11:56:01 +0000 Subject: [PATCH 352/886] Add go version to gopls cache key (#20922) Closes #8071 Release Notes: - Changed the Go integration to check whether an existing `gopls` was compiled for the current `go` version. Previously we cached gopls (the go language server) as a file called `gopls_{GOPLS_VERSION}`. The go version that gopls was built with is crucial, so we need to cache the go version as well. It's actually super interesting and very clever; gopls uses go to parse the AST and do all the analyzation etc. Go exposes its internals in its standard lib (`go/parser`, `go/types`, ...), which gopls uses to analyze the user code. So if there is a new go release that contains new syntax/features/etc. (the libraries `go/parser`, `go/types`, ... change), we can rebuild the same version of `gopls` with the new version of go (with the updated `go/xxx` libraries) to support the new language features. We had some issues around that (e.g., range over integers introduced in go1.22, or custom iterators in go1.23) where we never updated gopls, because we were on the latest gopls version, but built with an old go version. After this PR gopls will be cached under the name `gopls_{GOPLS_VERSION}_go_{GO_VERSION}`. Most users do not see this issue anymore, because after https://github.com/zed-industries/zed/pull/8188 we first check if we can find gopls in the PATH before downloading and caching gopls, but the issue still exists. --- crates/languages/src/go.rs | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/crates/languages/src/go.rs b/crates/languages/src/go.rs index 6e2b5d464e..46bd12a268 100644 --- a/crates/languages/src/go.rs +++ b/crates/languages/src/go.rs @@ -15,6 +15,7 @@ use std::{ ffi::{OsStr, OsString}, ops::Range, path::PathBuf, + process::Output, str, sync::{ atomic::{AtomicBool, Ordering::SeqCst}, @@ -35,8 +36,8 @@ impl GoLspAdapter { const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("gopls"); } -static GOPLS_VERSION_REGEX: LazyLock = - LazyLock::new(|| Regex::new(r"\d+\.\d+\.\d+").expect("Failed to create GOPLS_VERSION_REGEX")); +static VERSION_REGEX: LazyLock = + LazyLock::new(|| Regex::new(r"\d+\.\d+\.\d+").expect("Failed to create VERSION_REGEX")); static GO_ESCAPE_SUBTEST_NAME_REGEX: LazyLock = LazyLock::new(|| { Regex::new(r#"[.*+?^${}()|\[\]\\]"#).expect("Failed to create GO_ESCAPE_SUBTEST_NAME_REGEX") @@ -111,11 +112,18 @@ impl super::LspAdapter for GoLspAdapter { container_dir: PathBuf, delegate: &dyn LspAdapterDelegate, ) -> Result { + let go = delegate.which("go".as_ref()).await.unwrap_or("go".into()); + let go_version_output = util::command::new_smol_command(&go) + .args(["version"]) + .output() + .await + .context("failed to get go version via `go version` command`")?; + let go_version = parse_version_output(&go_version_output)?; let version = version.downcast::>().unwrap(); let this = *self; if let Some(version) = *version { - let binary_path = container_dir.join(format!("gopls_{version}")); + let binary_path = container_dir.join(format!("gopls_{version}_go_{go_version}")); if let Ok(metadata) = fs::metadata(&binary_path).await { if metadata.is_file() { remove_matching(&container_dir, |entry| { @@ -139,8 +147,6 @@ 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 = util::command::new_smol_command(go) .env("GO111MODULE", "on") .env("GOBIN", &gobin_dir) @@ -164,13 +170,8 @@ impl super::LspAdapter for GoLspAdapter { .output() .await .context("failed to run installed gopls binary")?; - let version_stdout = str::from_utf8(&version_output.stdout) - .context("gopls version produced invalid utf8 output")?; - let version = GOPLS_VERSION_REGEX - .find(version_stdout) - .with_context(|| format!("failed to parse golps version output '{version_stdout}'"))? - .as_str(); - let binary_path = container_dir.join(format!("gopls_{version}")); + let gopls_version = parse_version_output(&version_output)?; + let binary_path = container_dir.join(format!("gopls_{gopls_version}_go_{go_version}")); fs::rename(&installed_binary_path, &binary_path).await?; Ok(LanguageServerBinary { @@ -366,6 +367,18 @@ impl super::LspAdapter for GoLspAdapter { } } +fn parse_version_output(output: &Output) -> Result<&str> { + let version_stdout = + str::from_utf8(&output.stdout).context("version command produced invalid utf8 output")?; + + let version = VERSION_REGEX + .find(version_stdout) + .with_context(|| format!("failed to parse version output '{version_stdout}'"))? + .as_str(); + + Ok(version) +} + async fn get_cached_server_binary(container_dir: PathBuf) -> Option { maybe!(async { let mut last_binary_path = None; From a7d12eea390ac10d20e8daf18013b970305863d0 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 9 Dec 2024 09:28:40 -0300 Subject: [PATCH 353/886] Enhance the Vim Mode toggle discoverability (#21589) Closes https://github.com/zed-industries/zed/issues/21522 This PR adds an info tooltip on the Welcome screen, informing users how Vim Mode can be toggled on and off. It also adds the Vim Mode toggle in the Editor Controls menu. This is all so that folks who accidentally turn it on better know how to turn it off. We're of course already able to toggle this setting via the command palette, but that may be harder to reach for beginners. So, maybe that's enough to close the linked issue? Open to feedback. (Note: I also added a max-width to the tooltip's label in this PR. I'm confident that this won't make any tooltip look weird/broken, but if it does, it may be because of this new property). | Welcome Page | Editor Controls | |--------|--------| | Screenshot 2024-12-05 at 11 20 04 | Screenshot 2024-12-05 at 11 12 15 | Release Notes: - N/A --------- Co-authored-by: Thorsten Ball --- assets/icons/info.svg | 12 ++ crates/ui/src/components/icon.rs | 1 + crates/ui/src/components/tooltip.rs | 2 +- crates/welcome/src/welcome.rs | 48 +++--- crates/zed/src/zed.rs | 1 + crates/zed/src/zed/quick_action_bar.rs | 226 ++++++++++++++----------- 6 files changed, 169 insertions(+), 121 deletions(-) create mode 100644 assets/icons/info.svg diff --git a/assets/icons/info.svg b/assets/icons/info.svg new file mode 100644 index 0000000000..7016cfe4d1 --- /dev/null +++ b/assets/icons/info.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index 03000f0638..ed7c294db9 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -202,6 +202,7 @@ pub enum IconName { HistoryRerun, Indicator, IndicatorX, + Info, InlayHint, Keyboard, Library, diff --git a/crates/ui/src/components/tooltip.rs b/crates/ui/src/components/tooltip.rs index 89b89786b0..5460966189 100644 --- a/crates/ui/src/components/tooltip.rs +++ b/crates/ui/src/components/tooltip.rs @@ -88,7 +88,7 @@ impl Render for Tooltip { el.child( h_flex() .gap_4() - .child(self.title.clone()) + .child(div().max_w_72().child(self.title.clone())) .when_some(self.key_binding.clone(), |this, key_binding| { this.justify_between().child(key_binding) }), diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index 8dcb26bcc1..f6953b944c 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -11,7 +11,7 @@ use gpui::{ }; use settings::{Settings, SettingsStore}; use std::sync::Arc; -use ui::{prelude::*, CheckboxWithLabel}; +use ui::{prelude::*, CheckboxWithLabel, Tooltip}; use vim_mode_setting::VimModeSetting; use workspace::{ dock::DockPosition, @@ -266,24 +266,34 @@ impl Render for WelcomePage { .child( v_group() .gap_2() - .child(CheckboxWithLabel::new( - "enable-vim", - Label::new("Enable Vim Mode"), - if VimModeSetting::get_global(cx).0 { - ui::Selection::Selected - } else { - ui::Selection::Unselected - }, - cx.listener(move |this, selection, cx| { - this.telemetry - .report_app_event("welcome page: toggle vim".to_string()); - this.update_settings::( - selection, - cx, - |setting, value| *setting = Some(value), - ); - }), - )) + .child( + h_flex() + .justify_between() + .child(CheckboxWithLabel::new( + "enable-vim", + Label::new("Enable Vim Mode"), + if VimModeSetting::get_global(cx).0 { + ui::Selection::Selected + } else { + ui::Selection::Unselected + }, + cx.listener(move |this, selection, cx| { + this.telemetry + .report_app_event("welcome page: toggle vim".to_string()); + this.update_settings::( + selection, + cx, + |setting, value| *setting = Some(value), + ); + }), + )) + .child( + IconButton::new("vim-mode", IconName::Info) + .icon_size(IconSize::XSmall) + .icon_color(Color::Muted) + .tooltip(|cx| Tooltip::text("You can also toggle Vim Mode via the command palette or Editor Controls menu.", cx)), + ) + ) .child(CheckboxWithLabel::new( "enable-crash", Label::new("Send Crash Reports"), diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 2adb287b4d..a52c8ec405 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -3447,6 +3447,7 @@ mod tests { app_state.languages.add(markdown_language()); + vim_mode_setting::init(cx); theme::init(theme::LoadThemes::JustBase, cx); audio::init((), cx); channel::init(&app_state.client, app_state.user_store.clone(), cx); diff --git a/crates/zed/src/zed/quick_action_bar.rs b/crates/zed/src/zed/quick_action_bar.rs index bfcd3fa391..be38502566 100644 --- a/crates/zed/src/zed/quick_action_bar.rs +++ b/crates/zed/src/zed/quick_action_bar.rs @@ -19,6 +19,7 @@ use ui::{ prelude::*, ButtonStyle, ContextMenu, IconButton, IconButtonShape, IconName, IconSize, PopoverMenu, PopoverMenuHandle, Tooltip, }; +use vim_mode_setting::VimModeSetting; use workspace::{ item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, }; @@ -154,6 +155,7 @@ impl Render for QuickActionBar { let editor_selections_dropdown = selection_menu_enabled.then(|| { let focus = editor.focus_handle(cx); + PopoverMenu::new("editor-selections-dropdown") .trigger( IconButton::new("toggle_editor_selections_icon", IconName::CursorIBeam) @@ -201,34 +203,78 @@ impl Render for QuickActionBar { }); let editor = editor.downgrade(); - let editor_settings_dropdown = PopoverMenu::new("editor-settings") - .trigger( - IconButton::new("toggle_editor_settings_icon", IconName::Sliders) - .shape(IconButtonShape::Square) - .icon_size(IconSize::Small) - .style(ButtonStyle::Subtle) - .selected(self.toggle_settings_handle.is_deployed()) - .when(!self.toggle_settings_handle.is_deployed(), |this| { - this.tooltip(|cx| Tooltip::text("Editor Controls", cx)) - }), - ) - .anchor(AnchorCorner::TopRight) - .with_handle(self.toggle_settings_handle.clone()) - .menu(move |cx| { - let menu = ContextMenu::build(cx, |mut menu, _| { - if supports_inlay_hints { + let editor_settings_dropdown = { + let vim_mode_enabled = VimModeSetting::get_global(cx).0; + + PopoverMenu::new("editor-settings") + .trigger( + IconButton::new("toggle_editor_settings_icon", IconName::Sliders) + .shape(IconButtonShape::Square) + .icon_size(IconSize::Small) + .style(ButtonStyle::Subtle) + .selected(self.toggle_settings_handle.is_deployed()) + .when(!self.toggle_settings_handle.is_deployed(), |this| { + this.tooltip(|cx| Tooltip::text("Editor Controls", cx)) + }), + ) + .anchor(AnchorCorner::TopRight) + .with_handle(self.toggle_settings_handle.clone()) + .menu(move |cx| { + let menu = ContextMenu::build(cx, |mut menu, _| { + if supports_inlay_hints { + menu = menu.toggleable_entry( + "Inlay Hints", + inlay_hints_enabled, + IconPosition::Start, + Some(editor::actions::ToggleInlayHints.boxed_clone()), + { + let editor = editor.clone(); + move |cx| { + editor + .update(cx, |editor, cx| { + editor.toggle_inlay_hints( + &editor::actions::ToggleInlayHints, + cx, + ); + }) + .ok(); + } + }, + ); + } + menu = menu.toggleable_entry( - "Inlay Hints", - inlay_hints_enabled, + "Selection Menu", + selection_menu_enabled, IconPosition::Start, - Some(editor::actions::ToggleInlayHints.boxed_clone()), + Some(editor::actions::ToggleSelectionMenu.boxed_clone()), { let editor = editor.clone(); move |cx| { editor .update(cx, |editor, cx| { - editor.toggle_inlay_hints( - &editor::actions::ToggleInlayHints, + editor.toggle_selection_menu( + &editor::actions::ToggleSelectionMenu, + cx, + ) + }) + .ok(); + } + }, + ); + + menu = menu.toggleable_entry( + "Auto Signature Help", + auto_signature_help_enabled, + IconPosition::Start, + Some(editor::actions::ToggleAutoSignatureHelp.boxed_clone()), + { + let editor = editor.clone(); + move |cx| { + editor + .update(cx, |editor, cx| { + editor.toggle_auto_signature_help_menu( + &editor::actions::ToggleAutoSignatureHelp, cx, ); }) @@ -236,92 +282,70 @@ impl Render for QuickActionBar { } }, ); - } - menu = menu.toggleable_entry( - "Selection Menu", - selection_menu_enabled, - IconPosition::Start, - Some(editor::actions::ToggleSelectionMenu.boxed_clone()), - { - let editor = editor.clone(); - move |cx| { - editor - .update(cx, |editor, cx| { - editor.toggle_selection_menu( - &editor::actions::ToggleSelectionMenu, - cx, - ) - }) - .ok(); - } - }, - ); + menu = menu.separator(); - menu = menu.toggleable_entry( - "Auto Signature Help", - auto_signature_help_enabled, - IconPosition::Start, - Some(editor::actions::ToggleAutoSignatureHelp.boxed_clone()), - { - let editor = editor.clone(); - move |cx| { - editor - .update(cx, |editor, cx| { - editor.toggle_auto_signature_help_menu( - &editor::actions::ToggleAutoSignatureHelp, - cx, - ); - }) - .ok(); - } - }, - ); + menu = menu.toggleable_entry( + "Inline Git Blame", + git_blame_inline_enabled, + IconPosition::Start, + Some(editor::actions::ToggleGitBlameInline.boxed_clone()), + { + let editor = editor.clone(); + move |cx| { + editor + .update(cx, |editor, cx| { + editor.toggle_git_blame_inline( + &editor::actions::ToggleGitBlameInline, + cx, + ) + }) + .ok(); + } + }, + ); - menu = menu.separator(); + menu = menu.toggleable_entry( + "Column Git Blame", + show_git_blame_gutter, + IconPosition::Start, + Some(editor::actions::ToggleGitBlame.boxed_clone()), + { + let editor = editor.clone(); + move |cx| { + editor + .update(cx, |editor, cx| { + editor.toggle_git_blame( + &editor::actions::ToggleGitBlame, + cx, + ) + }) + .ok(); + } + }, + ); - menu = menu.toggleable_entry( - "Inline Git Blame", - git_blame_inline_enabled, - IconPosition::Start, - Some(editor::actions::ToggleGitBlameInline.boxed_clone()), - { - let editor = editor.clone(); - move |cx| { - editor - .update(cx, |editor, cx| { - editor.toggle_git_blame_inline( - &editor::actions::ToggleGitBlameInline, - cx, - ) - }) - .ok(); - } - }, - ); + menu = menu.separator(); - menu = menu.toggleable_entry( - "Column Git Blame", - show_git_blame_gutter, - IconPosition::Start, - Some(editor::actions::ToggleGitBlame.boxed_clone()), - { - let editor = editor.clone(); - move |cx| { - editor - .update(cx, |editor, cx| { - editor - .toggle_git_blame(&editor::actions::ToggleGitBlame, cx) - }) - .ok(); - } - }, - ); + menu = menu.toggleable_entry( + "Vim Mode", + vim_mode_enabled, + IconPosition::Start, + None, + { + move |cx| { + let new_value = !vim_mode_enabled; + VimModeSetting::override_global(VimModeSetting(new_value), cx); + cx.refresh(); + } + }, + ); - menu - }); - Some(menu) - }); + menu + }); + Some(menu) + }) + }; h_flex() .id("quick action bar") From 39e8944dcc3a1b1b3ce49075b5f36c90f4b3840d Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 9 Dec 2024 14:10:11 +0100 Subject: [PATCH 354/886] language_tools: Split LSP log view selector into two (#21742) This should make it easier to interact with LSP log view when there are multiple language servers. I often find the current UI clunky when I have over 5 servers running (which isn't uncommon with multiple projects open) https://github.com/user-attachments/assets/2ecaf17f-4b40-4c8f-aa6f-03b437a3d979 Closes #ISSUE Release Notes: - N/A --- crates/language_tools/src/lsp_log.rs | 388 +++++++++++++++------------ 1 file changed, 215 insertions(+), 173 deletions(-) diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index 2e2c0caf40..892745f4d3 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -1145,19 +1145,28 @@ impl Render for LspLogToolbarItemView { None } }); - + let available_language_servers: Vec<_> = menu_rows + .iter() + .map(|row| { + ( + row.server_id, + row.server_name.clone(), + row.worktree_root_name.clone(), + row.selected_entry, + ) + }) + .collect(); let log_toolbar_view = cx.view().clone(); let lsp_menu = PopoverMenu::new("LspLogView") .anchor(AnchorCorner::TopLeft) .trigger(Button::new( "language_server_menu_header", current_server + .as_ref() .map(|row| { Cow::Owned(format!( - "{} ({}) - {}", - row.server_name.0, - row.worktree_root_name, - row.selected_entry.label() + "{} ({})", + row.server_name.0, row.worktree_root_name, )) }) .unwrap_or_else(|| "No server selected".into()), @@ -1165,36 +1174,71 @@ impl Render for LspLogToolbarItemView { .menu({ let log_view = log_view.clone(); move |cx| { - let menu_rows = menu_rows.clone(); let log_view = log_view.clone(); - let log_toolbar_view = log_toolbar_view.clone(); - ContextMenu::build(cx, move |mut menu, cx| { - for (ix, row) in menu_rows.into_iter().enumerate() { - let server_selected = Some(row.server_id) == current_server_id; - menu = menu - .header(format!( - "{} ({})", - row.server_name.0, row.worktree_root_name - )) - .entry( - SERVER_LOGS, - None, - cx.handler_for(&log_view, move |view, cx| { - view.show_logs_for_server(row.server_id, cx); - }), - ); - // We do not support tracing for remote language servers right now - if row.server_kind.is_remote() { - continue; - } + ContextMenu::build(cx, |mut menu, cx| { + for (server_id, name, worktree_root, active_entry_kind) in + available_language_servers.iter() + { + let label = format!("{} ({})", name, worktree_root); + let server_id = *server_id; + let active_entry_kind = *active_entry_kind; menu = menu.entry( + label, + None, + cx.handler_for(&log_view, move |view, cx| { + view.current_server_id = Some(server_id); + view.active_entry_kind = active_entry_kind; + match view.active_entry_kind { + LogKind::Rpc => { + view.toggle_rpc_trace_for_server(server_id, true, cx); + view.show_rpc_trace_for_server(server_id, cx); + } + LogKind::Trace => view.show_trace_for_server(server_id, cx), + LogKind::Logs => view.show_logs_for_server(server_id, cx), + LogKind::Capabilities => { + view.show_capabilities_for_server(server_id, cx) + } + } + cx.notify(); + }), + ); + } + menu + }) + .into() + } + }); + let view_selector = current_server.map(|server| { + let server_id = server.server_id; + let is_remote = server.server_kind.is_remote(); + let rpc_trace_enabled = server.rpc_trace_enabled; + let log_view = log_view.clone(); + PopoverMenu::new("LspViewSelector") + .anchor(AnchorCorner::TopLeft) + .trigger(Button::new( + "language_server_menu_header", + server.selected_entry.label(), + )) + .menu(move |cx| { + let log_toolbar_view = log_toolbar_view.clone(); + let log_view = log_view.clone(); + Some(ContextMenu::build(cx, move |this, cx| { + this.entry( + SERVER_LOGS, + None, + cx.handler_for(&log_view, move |view, cx| { + view.show_logs_for_server(server_id, cx); + }), + ) + .when(!is_remote, |this| { + this.entry( SERVER_TRACE, None, cx.handler_for(&log_view, move |view, cx| { - view.show_trace_for_server(row.server_id, cx); + view.show_trace_for_server(server_id, cx); }), - ); - menu = menu.custom_entry( + ) + .custom_entry( { let log_toolbar_view = log_toolbar_view.clone(); move |cx| { @@ -1205,8 +1249,8 @@ impl Render for LspLogToolbarItemView { .child( div().child( Checkbox::new( - ix, - if row.rpc_trace_enabled { + "LspLogEnableRpcTrace", + if rpc_trace_enabled { Selection::Selected } else { Selection::Unselected @@ -1220,9 +1264,7 @@ impl Render for LspLogToolbarItemView { Selection::Selected ); view.toggle_rpc_logging_for_server( - row.server_id, - enabled, - cx, + server_id, enabled, cx, ); cx.stop_propagation(); }, @@ -1233,42 +1275,148 @@ impl Render for LspLogToolbarItemView { } }, cx.handler_for(&log_view, move |view, cx| { - view.show_rpc_trace_for_server(row.server_id, cx); + view.show_rpc_trace_for_server(server_id, cx); }), - ); - if server_selected && row.selected_entry == LogKind::Rpc { - let selected_ix = menu.select_last(); - // Each language server has: - // 1. A title. - // 2. Server logs. - // 3. Server trace. - // 4. RPC messages. - // 5. Server capabilities - // Thus, if nth server's RPC is selected, the index of selected entry should match this formula - let _expected_index = ix * 5 + 3; - debug_assert_eq!( - Some(_expected_index), - selected_ix, - "Could not scroll to a just added LSP menu item" - ); - } - menu = menu.entry( - SERVER_CAPABILITIES, - None, - cx.handler_for(&log_view, move |view, cx| { - view.show_capabilities_for_server(row.server_id, cx); - }), - ); - } - menu - }) - .into() - } - }); - + ) + }) + .entry( + SERVER_CAPABILITIES, + None, + cx.handler_for(&log_view, move |view, cx| { + view.show_capabilities_for_server(server_id, cx); + }), + ) + })) + }) + }); h_flex() .size_full() - .child(lsp_menu) + .justify_between() + .child( + h_flex() + .child(lsp_menu) + .children(view_selector) + .child(log_view.update(cx, |this, _| match this.active_entry_kind { + LogKind::Trace => { + let log_view = log_view.clone(); + div().child( + PopoverMenu::new("lsp-trace-level-menu") + .anchor(AnchorCorner::TopLeft) + .trigger(Button::new( + "language_server_trace_level_selector", + "Trace level", + )) + .menu({ + let log_view = log_view.clone(); + + move |cx| { + let id = log_view.read(cx).current_server_id?; + + let trace_level = log_view.update(cx, |this, cx| { + this.log_store.update(cx, |this, _| { + Some( + this.get_language_server_state(id)? + .trace_level, + ) + }) + })?; + + ContextMenu::build(cx, |mut menu, _| { + let log_view = log_view.clone(); + + for (option, label) in [ + (TraceValue::Off, "Off"), + (TraceValue::Messages, "Messages"), + (TraceValue::Verbose, "Verbose"), + ] { + menu = menu.entry(label, None, { + let log_view = log_view.clone(); + move |cx| { + log_view.update(cx, |this, cx| { + if let Some(id) = + this.current_server_id + { + this.update_trace_level( + id, option, cx, + ); + } + }); + } + }); + if option == trace_level { + menu.select_last(); + } + } + + menu + }) + .into() + } + }), + ) + } + LogKind::Logs => { + let log_view = log_view.clone(); + div().child( + PopoverMenu::new("lsp-log-level-menu") + .anchor(AnchorCorner::TopLeft) + .trigger(Button::new( + "language_server_log_level_selector", + "Log level", + )) + .menu({ + let log_view = log_view.clone(); + + move |cx| { + let id = log_view.read(cx).current_server_id?; + + let log_level = log_view.update(cx, |this, cx| { + this.log_store.update(cx, |this, _| { + Some( + this.get_language_server_state(id)? + .log_level, + ) + }) + })?; + + ContextMenu::build(cx, |mut menu, _| { + let log_view = log_view.clone(); + + for (option, label) in [ + (MessageType::LOG, "Log"), + (MessageType::INFO, "Info"), + (MessageType::WARNING, "Warning"), + (MessageType::ERROR, "Error"), + ] { + menu = menu.entry(label, None, { + let log_view = log_view.clone(); + move |cx| { + log_view.update(cx, |this, cx| { + if let Some(id) = + this.current_server_id + { + this.update_log_level( + id, option, cx, + ); + } + }); + } + }); + if option == log_level { + menu.select_last(); + } + } + + menu + }) + .into() + } + }), + ) + } + _ => div(), + })), + ) .child( div() .child( @@ -1288,112 +1436,6 @@ impl Render for LspLogToolbarItemView { ) .ml_2(), ) - .child(log_view.update(cx, |this, _| match this.active_entry_kind { - LogKind::Trace => { - let log_view = log_view.clone(); - div().child( - PopoverMenu::new("lsp-trace-level-menu") - .anchor(AnchorCorner::TopLeft) - .trigger(Button::new( - "language_server_trace_level_selector", - "Trace level", - )) - .menu({ - let log_view = log_view.clone(); - - move |cx| { - let id = log_view.read(cx).current_server_id?; - - let trace_level = log_view.update(cx, |this, cx| { - this.log_store.update(cx, |this, _| { - Some(this.get_language_server_state(id)?.trace_level) - }) - })?; - - ContextMenu::build(cx, |mut menu, _| { - let log_view = log_view.clone(); - - for (option, label) in [ - (TraceValue::Off, "Off"), - (TraceValue::Messages, "Messages"), - (TraceValue::Verbose, "Verbose"), - ] { - menu = menu.entry(label, None, { - let log_view = log_view.clone(); - move |cx| { - log_view.update(cx, |this, cx| { - if let Some(id) = this.current_server_id { - this.update_trace_level(id, option, cx); - } - }); - } - }); - if option == trace_level { - menu.select_last(); - } - } - - menu - }) - .into() - } - }), - ) - } - LogKind::Logs => { - let log_view = log_view.clone(); - div().child( - PopoverMenu::new("lsp-log-level-menu") - .anchor(AnchorCorner::TopLeft) - .trigger(Button::new( - "language_server_log_level_selector", - "Log level", - )) - .menu({ - let log_view = log_view.clone(); - - move |cx| { - let id = log_view.read(cx).current_server_id?; - - let log_level = log_view.update(cx, |this, cx| { - this.log_store.update(cx, |this, _| { - Some(this.get_language_server_state(id)?.log_level) - }) - })?; - - ContextMenu::build(cx, |mut menu, _| { - let log_view = log_view.clone(); - - for (option, label) in [ - (MessageType::LOG, "Log"), - (MessageType::INFO, "Info"), - (MessageType::WARNING, "Warning"), - (MessageType::ERROR, "Error"), - ] { - menu = menu.entry(label, None, { - let log_view = log_view.clone(); - move |cx| { - log_view.update(cx, |this, cx| { - if let Some(id) = this.current_server_id { - this.update_log_level(id, option, cx); - } - }); - } - }); - if option == log_level { - menu.select_last(); - } - } - - menu - }) - .into() - } - }), - ) - } - _ => div(), - })) } } From 77b8296fbbf2f68d2ae9bdcb04d222de3f69cd59 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 9 Dec 2024 14:26:36 +0100 Subject: [PATCH 355/886] Introduce staff-only inline completion provider (#21739) Release Notes: - N/A --------- Co-authored-by: Thorsten Ball Co-authored-by: Bennet Co-authored-by: Thorsten --- Cargo.lock | 42 +- Cargo.toml | 2 + crates/client/src/telemetry.rs | 21 +- crates/collab/k8s/collab.template.yml | 15 + crates/collab/src/api/events.rs | 6 +- crates/collab/src/lib.rs | 6 + crates/collab/src/llm.rs | 59 +- crates/collab/src/llm/prediction_prompt.md | 12 + crates/collab/src/tests/test_server.rs | 3 + crates/copilot/Cargo.toml | 7 +- .../src/copilot_completion_provider.rs | 136 +-- crates/editor/src/display_map.rs | 6 + crates/editor/src/display_map/block_map.rs | 85 ++ crates/editor/src/editor.rs | 394 ++++--- crates/editor/src/element.rs | 417 +++++++- crates/editor/src/inline_completion_tests.rs | 360 +++++++ crates/inline_completion/Cargo.toml | 2 - .../src/inline_completion.rs | 55 +- crates/inline_completion_button/Cargo.toml | 2 + .../src/inline_completion_button.rs | 30 +- crates/language/src/buffer.rs | 2 +- crates/language/src/language_settings.rs | 1 + crates/language_models/src/language_models.rs | 4 +- crates/language_models/src/provider/cloud.rs | 6 +- crates/open_ai/src/open_ai.rs | 83 ++ crates/rpc/src/llm.rs | 11 + .../src/supermaven_completion_provider.rs | 81 +- .../telemetry_events/src/telemetry_events.rs | 16 + crates/vim/src/normal/change.rs | 2 + crates/vim/src/normal/delete.rs | 2 + crates/vim/src/vim.rs | 14 +- crates/zed/Cargo.toml | 1 + crates/zed/src/main.rs | 2 +- crates/zed/src/zed.rs | 2 +- .../zed/src/zed/inline_completion_registry.rs | 37 +- crates/zeta/Cargo.toml | 60 ++ crates/zeta/LICENSE-GPL | 1 + crates/zeta/src/rate_completion_modal.rs | 301 ++++++ crates/zeta/src/zeta.rs | 960 ++++++++++++++++++ 39 files changed, 2890 insertions(+), 356 deletions(-) create mode 100644 crates/collab/src/llm/prediction_prompt.md create mode 100644 crates/editor/src/inline_completion_tests.rs create mode 100644 crates/zeta/Cargo.toml create mode 120000 crates/zeta/LICENSE-GPL create mode 100644 crates/zeta/src/rate_completion_modal.rs create mode 100644 crates/zeta/src/zeta.rs diff --git a/Cargo.lock b/Cargo.lock index 0993089333..3804dce05a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6271,8 +6271,6 @@ version = "0.1.0" dependencies = [ "gpui", "language", - "project", - "text", ] [[package]] @@ -6282,6 +6280,7 @@ dependencies = [ "anyhow", "copilot", "editor", + "feature_flags", "fs", "futures 0.3.31", "gpui", @@ -6297,6 +6296,7 @@ dependencies = [ "ui", "workspace", "zed_actions", + "zeta", ] [[package]] @@ -16146,6 +16146,7 @@ dependencies = [ "winresource", "workspace", "zed_actions", + "zeta", ] [[package]] @@ -16456,6 +16457,43 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "zeta" +version = "0.1.0" +dependencies = [ + "anyhow", + "call", + "client", + "clock", + "collections", + "ctor", + "editor", + "env_logger 0.11.5", + "futures 0.3.31", + "gpui", + "http_client", + "indoc", + "inline_completion", + "language", + "language_models", + "log", + "menu", + "reqwest_client", + "rpc", + "serde_json", + "settings", + "similar", + "telemetry_events", + "theme", + "tree-sitter-go", + "tree-sitter-rust", + "ui", + "util", + "uuid", + "workspace", + "worktree", +] + [[package]] name = "zip" version = "0.6.6" diff --git a/Cargo.toml b/Cargo.toml index 7ff0ad6ce3..7764255805 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -141,6 +141,7 @@ members = [ "crates/worktree", "crates/zed", "crates/zed_actions", + "crates/zeta", # # Extensions @@ -325,6 +326,7 @@ workspace = { path = "crates/workspace" } worktree = { path = "crates/worktree" } zed = { path = "crates/zed" } zed_actions = { path = "crates/zed_actions" } +zeta = { path = "crates/zeta" } # # External crates diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index eef2a8215f..3e97b0164b 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -18,7 +18,8 @@ use std::time::Instant; use std::{env, mem, path::PathBuf, sync::Arc, time::Duration}; use telemetry_events::{ ActionEvent, AppEvent, AssistantEvent, CallEvent, EditEvent, EditorEvent, Event, - EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent, ReplEvent, SettingEvent, + EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent, InlineCompletionRating, + InlineCompletionRatingEvent, ReplEvent, SettingEvent, }; use util::{ResultExt, TryFutureExt}; use worktree::{UpdatedEntriesSet, WorktreeId}; @@ -355,6 +356,24 @@ impl Telemetry { self.report_event(event) } + pub fn report_inline_completion_rating_event( + self: &Arc, + rating: InlineCompletionRating, + input_events: Arc, + input_excerpt: Arc, + output_excerpt: Arc, + feedback: String, + ) { + let event = Event::InlineCompletionRating(InlineCompletionRatingEvent { + rating, + input_events, + input_excerpt, + output_excerpt, + feedback, + }); + self.report_event(event); + } + pub fn report_assistant_event(self: &Arc, event: AssistantEvent) { self.report_event(Event::Assistant(event)); } diff --git a/crates/collab/k8s/collab.template.yml b/crates/collab/k8s/collab.template.yml index a2f89e5646..89921f2424 100644 --- a/crates/collab/k8s/collab.template.yml +++ b/crates/collab/k8s/collab.template.yml @@ -149,6 +149,21 @@ spec: secretKeyRef: name: google-ai key: api_key + - name: PREDICTION_API_URL + valueFrom: + secretKeyRef: + name: prediction + key: api_url + - name: PREDICTION_API_KEY + valueFrom: + secretKeyRef: + name: prediction + key: api_key + - name: PREDICTION_MODEL + valueFrom: + secretKeyRef: + name: prediction + key: model - name: BLOB_STORE_ACCESS_KEY valueFrom: secretKeyRef: diff --git a/crates/collab/src/api/events.rs b/crates/collab/src/api/events.rs index b5cd920fb3..1dc036ca86 100644 --- a/crates/collab/src/api/events.rs +++ b/crates/collab/src/api/events.rs @@ -483,7 +483,7 @@ pub async fn post_events( checksum_matched, )) } - Event::Cpu(_) | Event::Memory(_) => continue, + Event::Cpu(_) | Event::Memory(_) | Event::InlineCompletionRating(_) => continue, Event::App(event) => to_upload.app_events.push(AppEventRow::from_event( event.clone(), wrapper, @@ -1406,6 +1406,10 @@ fn for_snowflake( ), serde_json::to_value(e).unwrap(), ), + Event::InlineCompletionRating(e) => ( + "Inline Completion Feedback".to_string(), + serde_json::to_value(e).unwrap(), + ), Event::Call(e) => { let event_type = match e.operation.trim() { "unshare project" => "Project Unshared".to_string(), diff --git a/crates/collab/src/lib.rs b/crates/collab/src/lib.rs index cfa0e1631e..9c87b69826 100644 --- a/crates/collab/src/lib.rs +++ b/crates/collab/src/lib.rs @@ -180,6 +180,9 @@ pub struct Config { pub anthropic_api_key: Option>, pub anthropic_staff_api_key: Option>, pub llm_closed_beta_model_name: Option>, + pub prediction_api_url: Option>, + pub prediction_api_key: Option>, + pub prediction_model: Option>, pub zed_client_checksum_seed: Option, pub slack_panics_webhook: Option, pub auto_join_channel_id: Option, @@ -230,6 +233,9 @@ impl Config { anthropic_api_key: None, anthropic_staff_api_key: None, llm_closed_beta_model_name: None, + prediction_api_url: None, + prediction_api_key: None, + prediction_model: None, clickhouse_url: None, clickhouse_user: None, clickhouse_password: None, diff --git a/crates/collab/src/llm.rs b/crates/collab/src/llm.rs index 603b76db73..94329c0b6f 100644 --- a/crates/collab/src/llm.rs +++ b/crates/collab/src/llm.rs @@ -29,7 +29,10 @@ use reqwest_client::ReqwestClient; use rpc::{ proto::Plan, LanguageModelProvider, PerformCompletionParams, EXPIRED_LLM_TOKEN_HEADER_NAME, }; -use rpc::{ListModelsResponse, MAX_LLM_MONTHLY_SPEND_REACHED_HEADER_NAME}; +use rpc::{ + ListModelsResponse, PredictEditsParams, PredictEditsResponse, + MAX_LLM_MONTHLY_SPEND_REACHED_HEADER_NAME, +}; use serde_json::json; use std::{ pin::Pin, @@ -126,6 +129,7 @@ pub fn routes() -> Router<(), Body> { Router::new() .route("/models", get(list_models)) .route("/completion", post(perform_completion)) + .route("/predict_edits", post(predict_edits)) .layer(middleware::from_fn(validate_api_token)) } @@ -439,6 +443,59 @@ fn normalize_model_name(known_models: Vec, name: String) -> String { } } +async fn predict_edits( + Extension(state): Extension>, + Extension(claims): Extension, + _country_code_header: Option>, + Json(params): Json, +) -> Result { + if !claims.is_staff { + return Err(anyhow!("not found"))?; + } + + let api_url = state + .config + .prediction_api_url + .as_ref() + .context("no PREDICTION_API_URL configured on the server")?; + let api_key = state + .config + .prediction_api_key + .as_ref() + .context("no PREDICTION_API_KEY configured on the server")?; + let model = state + .config + .prediction_model + .as_ref() + .context("no PREDICTION_MODEL configured on the server")?; + let prompt = include_str!("./llm/prediction_prompt.md") + .replace("", ¶ms.input_events) + .replace("", ¶ms.input_excerpt); + let mut response = open_ai::complete_text( + &state.http_client, + api_url, + api_key, + open_ai::CompletionRequest { + model: model.to_string(), + prompt: prompt.clone(), + max_tokens: 1024, + temperature: 0., + prediction: Some(open_ai::Prediction::Content { + content: params.input_excerpt, + }), + rewrite_speculation: Some(true), + }, + ) + .await?; + let choice = response + .choices + .pop() + .context("no output from completion response")?; + Ok(Json(PredictEditsResponse { + output_excerpt: choice.text, + })) +} + /// The maximum monthly spending an individual user can reach on the free tier /// before they have to pay. pub const FREE_TIER_MONTHLY_SPENDING_LIMIT: Cents = Cents::from_dollars(10); diff --git a/crates/collab/src/llm/prediction_prompt.md b/crates/collab/src/llm/prediction_prompt.md new file mode 100644 index 0000000000..81de06d280 --- /dev/null +++ b/crates/collab/src/llm/prediction_prompt.md @@ -0,0 +1,12 @@ +Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request. + +### Instruction: +You are a code completion assistant and your task is to analyze user edits and then rewrite an excerpt that the user provides, suggesting the appropriate edits within the excerpt, taking into account the cursor location. + +### Events: + + +### Input: + + +### Response: diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index e66a828a77..91e103510c 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -546,6 +546,9 @@ impl TestServer { anthropic_api_key: None, anthropic_staff_api_key: None, llm_closed_beta_model_name: None, + prediction_api_url: None, + prediction_api_key: None, + prediction_model: None, clickhouse_url: None, clickhouse_user: None, clickhouse_password: None, diff --git a/crates/copilot/Cargo.toml b/crates/copilot/Cargo.toml index 2cbe76c16e..5905d2d46e 100644 --- a/crates/copilot/Cargo.toml +++ b/crates/copilot/Cargo.toml @@ -59,18 +59,21 @@ workspace.workspace = true async-std = { version = "1.12.0", features = ["unstable"] } [dev-dependencies] -clock.workspace = true indoc.workspace = true serde_json.workspace = true +clock = { workspace = true, features = ["test-support"] } +client = { workspace = true, features = ["test-support"] } collections = { workspace = true, features = ["test-support"] } editor = { workspace = true, features = ["test-support"] } fs = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] } +http_client = { workspace = true, features = ["test-support"] } language = { workspace = true, features = ["test-support"] } lsp = { workspace = true, features = ["test-support"] } +node_runtime = { workspace = true, features = ["test-support"] } project = { workspace = true, features = ["test-support"] } rpc = { workspace = true, features = ["test-support"] } settings = { workspace = true, features = ["test-support"] } theme = { workspace = true, features = ["test-support"] } util = { workspace = true, features = ["test-support"] } -http_client = { workspace = true, features = ["test-support"] } +workspace = { workspace = true, features = ["test-support"] } diff --git a/crates/copilot/src/copilot_completion_provider.rs b/crates/copilot/src/copilot_completion_provider.rs index 85fe20f1ae..8d664e2289 100644 --- a/crates/copilot/src/copilot_completion_provider.rs +++ b/crates/copilot/src/copilot_completion_provider.rs @@ -1,14 +1,13 @@ use crate::{Completion, Copilot}; use anyhow::Result; -use client::telemetry::Telemetry; use gpui::{AppContext, EntityId, Model, ModelContext, Task}; -use inline_completion::{CompletionProposal, Direction, InlayProposal, InlineCompletionProvider}; +use inline_completion::{Direction, InlineCompletion, InlineCompletionProvider}; use language::{ language_settings::{all_language_settings, AllLanguageSettings}, Buffer, OffsetRangeExt, ToOffset, }; use settings::Settings; -use std::{path::Path, sync::Arc, time::Duration}; +use std::{path::Path, time::Duration}; pub const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75); @@ -21,7 +20,6 @@ pub struct CopilotCompletionProvider { pending_refresh: Task>, pending_cycling_refresh: Task>, copilot: Model, - telemetry: Option>, } impl CopilotCompletionProvider { @@ -35,15 +33,9 @@ impl CopilotCompletionProvider { pending_refresh: Task::ready(Ok(())), pending_cycling_refresh: Task::ready(Ok(())), copilot, - telemetry: None, } } - pub fn with_telemetry(mut self, telemetry: Arc) -> Self { - self.telemetry = Some(telemetry); - self - } - fn active_completion(&self) -> Option<&Completion> { self.completions.get(self.active_completion_index) } @@ -190,23 +182,10 @@ impl InlineCompletionProvider for CopilotCompletionProvider { self.copilot .update(cx, |copilot, cx| copilot.accept_completion(completion, cx)) .detach_and_log_err(cx); - if self.active_completion().is_some() { - if let Some(telemetry) = self.telemetry.as_ref() { - telemetry.report_inline_completion_event( - Self::name().to_string(), - true, - self.file_extension.clone(), - ); - } - } } } - fn discard( - &mut self, - should_report_inline_completion_event: bool, - cx: &mut ModelContext, - ) { + fn discard(&mut self, cx: &mut ModelContext) { let settings = AllLanguageSettings::get_global(cx); let copilot_enabled = settings.inline_completions_enabled(None, None, cx); @@ -220,24 +199,14 @@ impl InlineCompletionProvider for CopilotCompletionProvider { copilot.discard_completions(&self.completions, cx) }) .detach_and_log_err(cx); - - if should_report_inline_completion_event && self.active_completion().is_some() { - if let Some(telemetry) = self.telemetry.as_ref() { - telemetry.report_inline_completion_event( - Self::name().to_string(), - false, - self.file_extension.clone(), - ); - } - } } - fn active_completion_text<'a>( - &'a self, + fn suggest( + &mut self, buffer: &Model, cursor_position: language::Anchor, - cx: &'a AppContext, - ) -> Option { + cx: &mut ModelContext, + ) -> Option { let buffer_id = buffer.entity_id(); let buffer = buffer.read(cx); let completion = self.active_completion()?; @@ -267,13 +236,9 @@ impl InlineCompletionProvider for CopilotCompletionProvider { if completion_text.trim().is_empty() { None } else { - Some(CompletionProposal { - inlays: vec![InlayProposal::Suggestion( - cursor_position.bias_right(buffer), - completion_text.into(), - )], - text: completion_text.into(), - delete_range: None, + let position = cursor_position.bias_right(buffer); + Some(InlineCompletion { + edits: vec![(position..position, completion_text.into())], }) } } else { @@ -359,7 +324,7 @@ mod tests { executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, cx| { assert!(editor.context_menu_visible()); - assert!(!editor.has_active_inline_completion(cx)); + assert!(!editor.has_active_inline_completion()); // Confirming a completion inserts it and hides the context menu, without showing // the copilot suggestion afterwards. @@ -368,7 +333,7 @@ mod tests { .unwrap() .detach(); assert!(!editor.context_menu_visible()); - assert!(!editor.has_active_inline_completion(cx)); + assert!(!editor.has_active_inline_completion()); assert_eq!(editor.text(cx), "one.completion_a\ntwo\nthree\n"); assert_eq!(editor.display_text(cx), "one.completion_a\ntwo\nthree\n"); }); @@ -401,7 +366,7 @@ mod tests { executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, cx| { assert!(!editor.context_menu_visible()); - assert!(editor.has_active_inline_completion(cx)); + assert!(editor.has_active_inline_completion()); assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.\ntwo\nthree\n"); }); @@ -434,12 +399,12 @@ mod tests { executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, cx| { assert!(editor.context_menu_visible()); - assert!(!editor.has_active_inline_completion(cx)); + assert!(!editor.has_active_inline_completion()); // When hiding the context menu, the Copilot suggestion becomes visible. editor.cancel(&Default::default(), cx); assert!(!editor.context_menu_visible()); - assert!(editor.has_active_inline_completion(cx)); + assert!(editor.has_active_inline_completion()); assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.\ntwo\nthree\n"); }); @@ -449,7 +414,7 @@ mod tests { executor.run_until_parked(); cx.update_editor(|editor, cx| { assert!(!editor.context_menu_visible()); - assert!(editor.has_active_inline_completion(cx)); + assert!(editor.has_active_inline_completion()); assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); }); @@ -467,25 +432,25 @@ mod tests { executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, cx| { assert!(!editor.context_menu_visible()); - assert!(editor.has_active_inline_completion(cx)); + assert!(editor.has_active_inline_completion()); assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); // Canceling should remove the active Copilot suggestion. editor.cancel(&Default::default(), cx); - assert!(!editor.has_active_inline_completion(cx)); + assert!(!editor.has_active_inline_completion()); assert_eq!(editor.display_text(cx), "one.c\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); // After canceling, tabbing shouldn't insert the previously shown suggestion. editor.tab(&Default::default(), cx); - assert!(!editor.has_active_inline_completion(cx)); + assert!(!editor.has_active_inline_completion()); assert_eq!(editor.display_text(cx), "one.c \ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.c \ntwo\nthree\n"); // When undoing the previously active suggestion is shown again. editor.undo(&Default::default(), cx); - assert!(editor.has_active_inline_completion(cx)); + assert!(editor.has_active_inline_completion()); assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); }); @@ -493,25 +458,25 @@ mod tests { // If an edit occurs outside of this editor, the suggestion is still correctly interpolated. cx.update_buffer(|buffer, cx| buffer.edit([(5..5, "o")], None, cx)); cx.update_editor(|editor, cx| { - assert!(editor.has_active_inline_completion(cx)); + assert!(editor.has_active_inline_completion()); assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n"); // AcceptInlineCompletion when there is an active suggestion inserts it. editor.accept_inline_completion(&Default::default(), cx); - assert!(!editor.has_active_inline_completion(cx)); + assert!(!editor.has_active_inline_completion()); assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.copilot2\ntwo\nthree\n"); // When undoing the previously active suggestion is shown again. editor.undo(&Default::default(), cx); - assert!(editor.has_active_inline_completion(cx)); + assert!(editor.has_active_inline_completion()); assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n"); // Hide suggestion. editor.cancel(&Default::default(), cx); - assert!(!editor.has_active_inline_completion(cx)); + assert!(!editor.has_active_inline_completion()); assert_eq!(editor.display_text(cx), "one.co\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n"); }); @@ -520,7 +485,7 @@ mod tests { // we won't make it visible. cx.update_buffer(|buffer, cx| buffer.edit([(6..6, "p")], None, cx)); cx.update_editor(|editor, cx| { - assert!(!editor.has_active_inline_completion(cx)); + assert!(!editor.has_active_inline_completion()); assert_eq!(editor.display_text(cx), "one.cop\ntwo\nthree\n"); assert_eq!(editor.text(cx), "one.cop\ntwo\nthree\n"); }); @@ -545,19 +510,19 @@ mod tests { cx.update_editor(|editor, cx| editor.next_inline_completion(&Default::default(), cx)); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, cx| { - assert!(editor.has_active_inline_completion(cx)); + assert!(editor.has_active_inline_completion()); assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}"); assert_eq!(editor.text(cx), "fn foo() {\n \n}"); // Tabbing inside of leading whitespace inserts indentation without accepting the suggestion. editor.tab(&Default::default(), cx); - assert!(editor.has_active_inline_completion(cx)); + assert!(editor.has_active_inline_completion()); assert_eq!(editor.text(cx), "fn foo() {\n \n}"); assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}"); // Using AcceptInlineCompletion again accepts the suggestion. editor.accept_inline_completion(&Default::default(), cx); - assert!(!editor.has_active_inline_completion(cx)); + assert!(!editor.has_active_inline_completion()); assert_eq!(editor.text(cx), "fn foo() {\n let x = 4;\n}"); assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}"); }); @@ -615,17 +580,17 @@ mod tests { ); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, cx| { - assert!(editor.has_active_inline_completion(cx)); + assert!(editor.has_active_inline_completion()); // Accepting the first word of the suggestion should only accept the first word and still show the rest. editor.accept_partial_inline_completion(&Default::default(), cx); - assert!(editor.has_active_inline_completion(cx)); + assert!(editor.has_active_inline_completion()); assert_eq!(editor.text(cx), "one.copilot\ntwo\nthree\n"); assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n"); // Accepting next word should accept the non-word and copilot suggestion should be gone editor.accept_partial_inline_completion(&Default::default(), cx); - assert!(!editor.has_active_inline_completion(cx)); + assert!(!editor.has_active_inline_completion()); assert_eq!(editor.text(cx), "one.copilot1\ntwo\nthree\n"); assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n"); }); @@ -657,11 +622,11 @@ mod tests { ); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, cx| { - assert!(editor.has_active_inline_completion(cx)); + assert!(editor.has_active_inline_completion()); // Accepting the first word (non-word) of the suggestion should only accept the first word and still show the rest. editor.accept_partial_inline_completion(&Default::default(), cx); - assert!(editor.has_active_inline_completion(cx)); + assert!(editor.has_active_inline_completion()); assert_eq!(editor.text(cx), "one.123. \ntwo\nthree\n"); assert_eq!( editor.display_text(cx), @@ -670,7 +635,7 @@ mod tests { // Accepting next word should accept the next word and copilot suggestion should still exist editor.accept_partial_inline_completion(&Default::default(), cx); - assert!(editor.has_active_inline_completion(cx)); + assert!(editor.has_active_inline_completion()); assert_eq!(editor.text(cx), "one.123. copilot\ntwo\nthree\n"); assert_eq!( editor.display_text(cx), @@ -679,7 +644,7 @@ mod tests { // Accepting the whitespace should accept the non-word/whitespaces with newline and copilot suggestion should be gone editor.accept_partial_inline_completion(&Default::default(), cx); - assert!(!editor.has_active_inline_completion(cx)); + assert!(!editor.has_active_inline_completion()); assert_eq!(editor.text(cx), "one.123. copilot\n 456\ntwo\nthree\n"); assert_eq!( editor.display_text(cx), @@ -730,29 +695,29 @@ mod tests { cx.update_editor(|editor, cx| editor.next_inline_completion(&Default::default(), cx)); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, cx| { - assert!(editor.has_active_inline_completion(cx)); + assert!(editor.has_active_inline_completion()); assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); assert_eq!(editor.text(cx), "one\ntw\nthree\n"); editor.backspace(&Default::default(), cx); - assert!(editor.has_active_inline_completion(cx)); + assert!(editor.has_active_inline_completion()); assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); assert_eq!(editor.text(cx), "one\nt\nthree\n"); editor.backspace(&Default::default(), cx); - assert!(editor.has_active_inline_completion(cx)); + assert!(editor.has_active_inline_completion()); assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); assert_eq!(editor.text(cx), "one\n\nthree\n"); // Deleting across the original suggestion range invalidates it. editor.backspace(&Default::default(), cx); - assert!(!editor.has_active_inline_completion(cx)); + assert!(!editor.has_active_inline_completion()); assert_eq!(editor.display_text(cx), "one\nthree\n"); assert_eq!(editor.text(cx), "one\nthree\n"); // Undoing the deletion restores the suggestion. editor.undo(&Default::default(), cx); - assert!(editor.has_active_inline_completion(cx)); + assert!(editor.has_active_inline_completion()); assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); assert_eq!(editor.text(cx), "one\n\nthree\n"); }); @@ -813,7 +778,7 @@ mod tests { }); executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); _ = editor.update(cx, |editor, cx| { - assert!(editor.has_active_inline_completion(cx)); + assert!(editor.has_active_inline_completion()); assert_eq!( editor.display_text(cx), "\n\n\na = 1\nb = 2 + a\n\n\n\n\n\nc = 3\nd = 4\n\n" @@ -835,7 +800,7 @@ mod tests { editor.change_selections(None, cx, |s| { s.select_ranges([Point::new(4, 5)..Point::new(4, 5)]) }); - assert!(!editor.has_active_inline_completion(cx)); + assert!(!editor.has_active_inline_completion()); assert_eq!( editor.display_text(cx), "\n\n\na = 1\nb = 2\n\n\n\n\n\nc = 3\nd = 4\n\n" @@ -844,7 +809,7 @@ mod tests { // Type a character, ensuring we don't even try to interpolate the previous suggestion. editor.handle_input(" ", cx); - assert!(!editor.has_active_inline_completion(cx)); + assert!(!editor.has_active_inline_completion()); assert_eq!( editor.display_text(cx), "\n\n\na = 1\nb = 2\n\n\n\n\n\nc = 3\nd = 4 \n\n" @@ -855,7 +820,7 @@ mod tests { // Ensure the new suggestion is displayed when the debounce timeout expires. executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); _ = editor.update(cx, |editor, cx| { - assert!(editor.has_active_inline_completion(cx)); + assert!(editor.has_active_inline_completion()); assert_eq!( editor.display_text(cx), "\n\n\na = 1\nb = 2\n\n\n\n\n\nc = 3\nd = 4 + c\n\n" @@ -916,7 +881,7 @@ mod tests { executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, cx| { assert!(!editor.context_menu_visible(), "Even there are some completions available, those are not triggered when active copilot suggestion is present"); - assert!(editor.has_active_inline_completion(cx)); + assert!(editor.has_active_inline_completion()); assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); assert_eq!(editor.text(cx), "one\ntw\nthree\n"); }); @@ -943,7 +908,7 @@ mod tests { executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); cx.update_editor(|editor, cx| { assert!(!editor.context_menu_visible()); - assert!(editor.has_active_inline_completion(cx)); + assert!(editor.has_active_inline_completion()); assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); assert_eq!(editor.text(cx), "one\ntwo\nthree\n"); }); @@ -974,7 +939,7 @@ mod tests { "On completion trigger input, the completions should be fetched and visible" ); assert!( - !editor.has_active_inline_completion(cx), + !editor.has_active_inline_completion(), "On completion trigger input, copilot suggestion should be dismissed" ); assert_eq!(editor.display_text(cx), "one\ntwo.\nthree\n"); @@ -998,7 +963,7 @@ mod tests { "/test", json!({ ".env": "SECRET=something\n", - "README.md": "hello\n" + "README.md": "hello\nworld\nhow\nare\nyou\ntoday" }), ) .await; @@ -1030,7 +995,7 @@ mod tests { multibuffer.push_excerpts( public_buffer.clone(), [ExcerptRange { - context: Point::new(0, 0)..Point::new(1, 0), + context: Point::new(0, 0)..Point::new(6, 0), primary: None, }], cx, @@ -1038,6 +1003,7 @@ mod tests { multibuffer }); let editor = cx.add_window(|cx| Editor::for_multibuffer(multibuffer, None, true, cx)); + editor.update(cx, |editor, cx| editor.focus(cx)).unwrap(); let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot)); editor .update(cx, |editor, cx| { @@ -1073,7 +1039,7 @@ mod tests { _ = editor.update(cx, |editor, cx| { editor.change_selections(None, cx, |s| { - s.select_ranges([Point::new(2, 0)..Point::new(2, 0)]) + s.select_ranges([Point::new(5, 0)..Point::new(5, 0)]) }); editor.refresh_inline_completion(true, false, cx); }); diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 2c62295a29..a02b925456 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -1125,6 +1125,12 @@ impl DisplaySnapshot { DisplayRow(self.block_snapshot.longest_row()) } + pub fn longest_row_in_range(&self, range: Range) -> DisplayRow { + let block_range = BlockRow(range.start.0)..BlockRow(range.end.0); + let longest_row = self.block_snapshot.longest_row_in_range(block_range); + DisplayRow(longest_row.0) + } + pub fn starts_indent(&self, buffer_row: MultiBufferRow) -> bool { let max_row = self.buffer_snapshot.max_row(); if buffer_row >= max_row { diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 1300537a2a..b495669ef8 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -1339,6 +1339,57 @@ impl BlockSnapshot { self.transforms.summary().longest_row } + pub fn longest_row_in_range(&self, range: Range) -> BlockRow { + let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); + cursor.seek(&range.start, Bias::Right, &()); + + let mut longest_row = range.start; + let mut longest_row_chars = 0; + if let Some(transform) = cursor.item() { + if transform.block.is_none() { + let (output_start, input_start) = cursor.start(); + let overshoot = range.start.0 - output_start.0; + let wrap_start_row = input_start.0 + overshoot; + let wrap_end_row = cmp::min( + input_start.0 + (range.end.0 - output_start.0), + cursor.end(&()).1 .0, + ); + let summary = self + .wrap_snapshot + .text_summary_for_range(wrap_start_row..wrap_end_row); + longest_row = BlockRow(range.start.0 + summary.longest_row); + longest_row_chars = summary.longest_row_chars; + } + cursor.next(&()); + } + + let cursor_start_row = cursor.start().0; + if range.end > cursor_start_row { + let summary = cursor.summary::<_, TransformSummary>(&range.end, Bias::Right, &()); + if summary.longest_row_chars > longest_row_chars { + longest_row = BlockRow(cursor_start_row.0 + summary.longest_row); + longest_row_chars = summary.longest_row_chars; + } + + if let Some(transform) = cursor.item() { + if transform.block.is_none() { + let (output_start, input_start) = cursor.start(); + let overshoot = range.end.0 - output_start.0; + let wrap_start_row = input_start.0; + let wrap_end_row = input_start.0 + overshoot; + let summary = self + .wrap_snapshot + .text_summary_for_range(wrap_start_row..wrap_end_row); + if summary.longest_row_chars > longest_row_chars { + longest_row = BlockRow(output_start.0 + summary.longest_row); + } + } + } + } + + longest_row + } + pub(super) fn line_len(&self, row: BlockRow) -> u32 { let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); cursor.seek(&BlockRow(row.0), Bias::Right, &()); @@ -2705,6 +2756,40 @@ mod tests { longest_line_len, ); + for _ in 0..10 { + let end_row = rng.gen_range(1..=expected_lines.len()); + let start_row = rng.gen_range(0..end_row); + + let mut expected_longest_rows_in_range = vec![]; + let mut longest_line_len_in_range = 0; + + let mut row = start_row as u32; + for line in &expected_lines[start_row..end_row] { + let line_char_count = line.chars().count() as isize; + match line_char_count.cmp(&longest_line_len_in_range) { + Ordering::Less => {} + Ordering::Equal => expected_longest_rows_in_range.push(row), + Ordering::Greater => { + longest_line_len_in_range = line_char_count; + expected_longest_rows_in_range.clear(); + expected_longest_rows_in_range.push(row); + } + } + row += 1; + } + + let longest_row_in_range = blocks_snapshot + .longest_row_in_range(BlockRow(start_row as u32)..BlockRow(end_row as u32)); + assert!( + expected_longest_rows_in_range.contains(&longest_row_in_range.0), + "incorrect longest row {} in range {:?}. expected {:?} with length {}", + longest_row, + start_row..end_row, + expected_longest_rows_in_range, + longest_line_len_in_range, + ); + } + // Ensure that conversion between block points and wrap points is stable. for row in 0..=blocks_snapshot.wrap_snapshot.max_point().row() { let wrap_point = WrapPoint::new(row, 0); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b2abe8db80..98e91f9751 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -42,6 +42,8 @@ pub mod tasks; #[cfg(test)] mod editor_tests; +#[cfg(test)] +mod inline_completion_tests; mod signature_help; #[cfg(any(test, feature = "test-support"))] pub mod test; @@ -87,7 +89,7 @@ use hunk_diff::{diff_hunk_to_display, DiffMap, DiffMapSnapshot}; use indent_guides::ActiveIndentGuidesState; use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy}; pub use inline_completion::Direction; -use inline_completion::{InlayProposal, InlineCompletionProvider, InlineCompletionProviderHandle}; +use inline_completion::{InlineCompletionProvider, InlineCompletionProviderHandle}; pub use items::MAX_TAB_TITLE_LEN; use itertools::Itertools; use language::{ @@ -438,22 +440,19 @@ pub fn make_inlay_hints_style(cx: &WindowContext) -> HighlightStyle { type CompletionId = usize; -#[derive(Clone, Debug)] -struct CompletionState { - // render_inlay_ids represents the inlay hints that are inserted - // for rendering the inline completions. They may be discontinuous - // in the event that the completion provider returns some intersection - // with the existing content. - render_inlay_ids: Vec, - // text is the resulting rope that is inserted when the user accepts a completion. - text: Rope, - // position is the position of the cursor when the completion was triggered. - position: multi_buffer::Anchor, - // delete_range is the range of text that this completion state covers. - // if the completion is accepted, this range should be deleted. - delete_range: Option>, +enum InlineCompletion { + Edit(Vec<(Range, String)>), + Move(Anchor), } +struct InlineCompletionState { + inlay_ids: Vec, + completion: InlineCompletion, + invalidation_range: Range, +} + +enum InlineCompletionHighlight {} + #[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug, Default)] struct EditorActionId(usize); @@ -619,7 +618,7 @@ pub struct Editor { hovered_link_state: Option, inline_completion_provider: Option, code_action_providers: Vec>, - active_inline_completion: Option, + active_inline_completion: Option, // enable_inline_completions is a switch that Vim can use to disable // inline completions based on its mode. enable_inline_completions: bool, @@ -2250,7 +2249,7 @@ impl Editor { key_context.set("extension", extension.to_string()); } - if self.has_active_inline_completion(cx) { + if self.has_active_inline_completion() { key_context.add("copilot_suggestion"); key_context.add("inline_completion"); } @@ -2760,7 +2759,7 @@ impl Editor { self.refresh_code_actions(cx); self.refresh_document_highlights(cx); refresh_matching_bracket_highlights(self, cx); - self.discard_inline_completion(false, cx); + self.update_visible_inline_completion(cx); linked_editing_ranges::refresh_linked_ranges(self, cx); if self.git_blame_inline_enabled { self.start_inline_blame_timer(cx); @@ -3651,7 +3650,7 @@ impl Editor { ); } - let had_active_inline_completion = this.has_active_inline_completion(cx); + let had_active_inline_completion = this.has_active_inline_completion(); this.change_selections_inner(Some(Autoscroll::fit()), false, cx, |s| { s.select(new_selections) }); @@ -4386,7 +4385,7 @@ impl Editor { cx: &mut ViewContext, ) { self.display_map.update(cx, |display_map, cx| { - display_map.splice_inlays(to_remove, to_insert, cx); + display_map.splice_inlays(to_remove, to_insert, cx) }); cx.notify(); } @@ -5243,7 +5242,8 @@ impl Editor { if !user_requested && (!self.enable_inline_completions - || !self.should_show_inline_completions(&buffer, cursor_buffer_position, cx)) + || !self.should_show_inline_completions(&buffer, cursor_buffer_position, cx) + || !self.is_focused(cx)) { self.discard_inline_completion(false, cx); return None; @@ -5276,7 +5276,7 @@ impl Editor { } pub fn show_inline_completion(&mut self, _: &ShowInlineCompletion, cx: &mut ViewContext) { - if !self.has_active_inline_completion(cx) { + if !self.has_active_inline_completion() { self.refresh_inline_completion(false, true, cx); return; } @@ -5303,7 +5303,7 @@ impl Editor { } pub fn next_inline_completion(&mut self, _: &NextInlineCompletion, cx: &mut ViewContext) { - if self.has_active_inline_completion(cx) { + if self.has_active_inline_completion() { self.cycle_inline_completion(Direction::Next, cx); } else { let is_copilot_disabled = self.refresh_inline_completion(false, true, cx).is_none(); @@ -5318,7 +5318,7 @@ impl Editor { _: &PreviousInlineCompletion, cx: &mut ViewContext, ) { - if self.has_active_inline_completion(cx) { + if self.has_active_inline_completion() { self.cycle_inline_completion(Direction::Prev, cx); } else { let is_copilot_disabled = self.refresh_inline_completion(false, true, cx).is_none(); @@ -5333,24 +5333,43 @@ impl Editor { _: &AcceptInlineCompletion, cx: &mut ViewContext, ) { - let Some(completion) = self.take_active_inline_completion(cx) else { + let Some(active_inline_completion) = self.active_inline_completion.as_ref() else { return; }; - if let Some(provider) = self.inline_completion_provider() { - provider.accept(cx); - } - cx.emit(EditorEvent::InputHandled { - utf16_range_to_replace: None, - text: completion.text.to_string().into(), - }); + self.report_inline_completion_event(true, cx); - if let Some(range) = completion.delete_range { - self.change_selections(None, cx, |s| s.select_ranges([range])) + match &active_inline_completion.completion { + InlineCompletion::Move(position) => { + let position = *position; + self.change_selections(Some(Autoscroll::newest()), cx, |selections| { + selections.select_anchor_ranges([position..position]); + }); + } + InlineCompletion::Edit(edits) => { + if let Some(provider) = self.inline_completion_provider() { + provider.accept(cx); + } + + let snapshot = self.buffer.read(cx).snapshot(cx); + let last_edit_end = edits.last().unwrap().0.end.bias_right(&snapshot); + + self.buffer.update(cx, |buffer, cx| { + buffer.edit(edits.iter().cloned(), None, cx) + }); + + self.change_selections(None, cx, |s| { + s.select_anchor_ranges([last_edit_end..last_edit_end]) + }); + + self.update_visible_inline_completion(cx); + if self.active_inline_completion.is_none() { + self.refresh_inline_completion(true, true, cx); + } + + cx.notify(); + } } - self.insert_with_autoindent_mode(&completion.text.to_string(), None, cx); - self.refresh_inline_completion(true, true, cx); - cx.notify(); } pub fn accept_partial_inline_completion( @@ -5358,35 +5377,48 @@ impl Editor { _: &AcceptPartialInlineCompletion, cx: &mut ViewContext, ) { - if self.selections.count() == 1 && self.has_active_inline_completion(cx) { - if let Some(completion) = self.take_active_inline_completion(cx) { - let mut partial_completion = completion - .text - .chars() - .by_ref() - .take_while(|c| c.is_alphabetic()) - .collect::(); - if partial_completion.is_empty() { - partial_completion = completion - .text + let Some(active_inline_completion) = self.active_inline_completion.as_ref() else { + return; + }; + if self.selections.count() != 1 { + return; + } + + self.report_inline_completion_event(true, cx); + + match &active_inline_completion.completion { + InlineCompletion::Move(position) => { + let position = *position; + self.change_selections(Some(Autoscroll::newest()), cx, |selections| { + selections.select_anchor_ranges([position..position]); + }); + } + InlineCompletion::Edit(edits) => { + if edits.len() == 1 && edits[0].0.start == edits[0].0.end { + let text = edits[0].1.as_str(); + let mut partial_completion = text .chars() .by_ref() - .take_while(|c| c.is_whitespace() || !c.is_alphabetic()) + .take_while(|c| c.is_alphabetic()) .collect::(); + if partial_completion.is_empty() { + partial_completion = text + .chars() + .by_ref() + .take_while(|c| c.is_whitespace() || !c.is_alphabetic()) + .collect::(); + } + + cx.emit(EditorEvent::InputHandled { + utf16_range_to_replace: None, + text: partial_completion.clone().into(), + }); + + self.insert_with_autoindent_mode(&partial_completion, None, cx); + + self.refresh_inline_completion(true, true, cx); + cx.notify(); } - - cx.emit(EditorEvent::InputHandled { - utf16_range_to_replace: None, - text: partial_completion.clone().into(), - }); - - if let Some(range) = completion.delete_range { - self.change_selections(None, cx, |s| s.select_ranges([range])) - } - self.insert_with_autoindent_mode(&partial_completion, None, cx); - - self.refresh_inline_completion(true, true, cx); - cx.notify(); } } } @@ -5396,106 +5428,178 @@ impl Editor { should_report_inline_completion_event: bool, cx: &mut ViewContext, ) -> bool { + if should_report_inline_completion_event { + self.report_inline_completion_event(false, cx); + } + if let Some(provider) = self.inline_completion_provider() { - provider.discard(should_report_inline_completion_event, cx); + provider.discard(cx); } self.take_active_inline_completion(cx).is_some() } - pub fn has_active_inline_completion(&self, cx: &AppContext) -> bool { - if let Some(completion) = self.active_inline_completion.as_ref() { - let buffer = self.buffer.read(cx).read(cx); - completion.position.is_valid(&buffer) - } else { - false - } + fn report_inline_completion_event(&self, accepted: bool, cx: &AppContext) { + let Some(provider) = self.inline_completion_provider() else { + return; + }; + let Some(project) = self.project.as_ref() else { + return; + }; + let Some((_, buffer, _)) = self + .buffer + .read(cx) + .excerpt_containing(self.selections.newest_anchor().head(), cx) + else { + return; + }; + + let project = project.read(cx); + let extension = buffer + .read(cx) + .file() + .and_then(|file| Some(file.path().extension()?.to_string_lossy().to_string())); + project.client().telemetry().report_inline_completion_event( + provider.name().into(), + accepted, + extension, + ); + } + + pub fn has_active_inline_completion(&self) -> bool { + self.active_inline_completion.is_some() } fn take_active_inline_completion( &mut self, cx: &mut ViewContext, - ) -> Option { - let completion = self.active_inline_completion.take()?; - let render_inlay_ids = completion.render_inlay_ids.clone(); - self.display_map.update(cx, |map, cx| { - map.splice_inlays(render_inlay_ids, Default::default(), cx); - }); - let buffer = self.buffer.read(cx).read(cx); - - if completion.position.is_valid(&buffer) { - Some(completion) - } else { - None - } + ) -> Option { + let active_inline_completion = self.active_inline_completion.take()?; + self.splice_inlays(active_inline_completion.inlay_ids, Default::default(), cx); + self.clear_highlights::(cx); + Some(active_inline_completion.completion) } - fn update_visible_inline_completion(&mut self, cx: &mut ViewContext) { + fn update_visible_inline_completion(&mut self, cx: &mut ViewContext) -> Option<()> { let selection = self.selections.newest_anchor(); let cursor = selection.head(); - + let multibuffer = self.buffer.read(cx).snapshot(cx); + let offset_selection = selection.map(|endpoint| endpoint.to_offset(&multibuffer)); let excerpt_id = cursor.excerpt_id; - if self.context_menu.read().is_none() - && self.completion_tasks.is_empty() - && selection.start == selection.end + if self.context_menu.read().is_some() + || (!self.completion_tasks.is_empty() && !self.has_active_inline_completion()) + || !offset_selection.is_empty() + || self + .active_inline_completion + .as_ref() + .map_or(false, |completion| { + let invalidation_range = completion.invalidation_range.to_offset(&multibuffer); + let invalidation_range = invalidation_range.start..=invalidation_range.end; + !invalidation_range.contains(&offset_selection.head()) + }) { - if let Some(provider) = self.inline_completion_provider() { - if let Some((buffer, cursor_buffer_position)) = - self.buffer.read(cx).text_anchor_for_position(cursor, cx) - { - if let Some(proposal) = - provider.active_completion_text(&buffer, cursor_buffer_position, cx) - { - let mut to_remove = Vec::new(); - if let Some(completion) = self.active_inline_completion.take() { - to_remove.extend(completion.render_inlay_ids.iter()); - } - - let to_add = proposal - .inlays - .iter() - .filter_map(|inlay| { - let snapshot = self.buffer.read(cx).snapshot(cx); - let id = post_inc(&mut self.next_inlay_id); - match inlay { - InlayProposal::Hint(position, hint) => { - let position = - snapshot.anchor_in_excerpt(excerpt_id, *position)?; - Some(Inlay::hint(id, position, hint)) - } - InlayProposal::Suggestion(position, text) => { - let position = - snapshot.anchor_in_excerpt(excerpt_id, *position)?; - Some(Inlay::suggestion(id, position, text.clone())) - } - } - }) - .collect_vec(); - - self.active_inline_completion = Some(CompletionState { - position: cursor, - text: proposal.text, - delete_range: proposal.delete_range.and_then(|range| { - let snapshot = self.buffer.read(cx).snapshot(cx); - let start = snapshot.anchor_in_excerpt(excerpt_id, range.start); - let end = snapshot.anchor_in_excerpt(excerpt_id, range.end); - Some(start?..end?) - }), - render_inlay_ids: to_add.iter().map(|i| i.id).collect(), - }); - - self.display_map - .update(cx, move |map, cx| map.splice_inlays(to_remove, to_add, cx)); - - cx.notify(); - return; - } - } - } + self.discard_inline_completion(false, cx); + return None; } - self.discard_inline_completion(false, cx); + self.take_active_inline_completion(cx); + let provider = self.inline_completion_provider()?; + + let (buffer, cursor_buffer_position) = + self.buffer.read(cx).text_anchor_for_position(cursor, cx)?; + + let completion = provider.suggest(&buffer, cursor_buffer_position, cx)?; + let edits = completion + .edits + .into_iter() + .map(|(range, new_text)| { + ( + multibuffer + .anchor_in_excerpt(excerpt_id, range.start) + .unwrap() + ..multibuffer + .anchor_in_excerpt(excerpt_id, range.end) + .unwrap(), + new_text, + ) + }) + .collect::>(); + if edits.is_empty() { + return None; + } + + let first_edit_start = edits.first().unwrap().0.start; + let edit_start_row = first_edit_start + .to_point(&multibuffer) + .row + .saturating_sub(2); + + let last_edit_end = edits.last().unwrap().0.end; + let edit_end_row = cmp::min( + multibuffer.max_point().row, + last_edit_end.to_point(&multibuffer).row + 2, + ); + + let cursor_row = cursor.to_point(&multibuffer).row; + + let mut inlay_ids = Vec::new(); + let invalidation_row_range; + let completion; + if cursor_row < edit_start_row { + invalidation_row_range = cursor_row..edit_end_row; + completion = InlineCompletion::Move(first_edit_start); + } else if cursor_row > edit_end_row { + invalidation_row_range = edit_start_row..cursor_row; + completion = InlineCompletion::Move(first_edit_start); + } else { + if edits + .iter() + .all(|(range, _)| range.to_offset(&multibuffer).is_empty()) + { + let mut inlays = Vec::new(); + for (range, new_text) in &edits { + let inlay = Inlay::suggestion( + post_inc(&mut self.next_inlay_id), + range.start, + new_text.as_str(), + ); + inlay_ids.push(inlay.id); + inlays.push(inlay); + } + + self.splice_inlays(vec![], inlays, cx); + } else { + let background_color = cx.theme().status().deleted_background; + self.highlight_text::( + edits.iter().map(|(range, _)| range.clone()).collect(), + HighlightStyle { + background_color: Some(background_color), + ..Default::default() + }, + cx, + ); + } + + invalidation_row_range = edit_start_row..edit_end_row; + completion = InlineCompletion::Edit(edits); + }; + + let invalidation_range = multibuffer + .anchor_before(Point::new(invalidation_row_range.start, 0)) + ..multibuffer.anchor_after(Point::new( + invalidation_row_range.end, + multibuffer.line_len(MultiBufferRow(invalidation_row_range.end)), + )); + + self.active_inline_completion = Some(InlineCompletionState { + inlay_ids, + completion, + invalidation_range, + }); + cx.notify(); + + Some(()) } fn inline_completion_provider(&self) -> Option> { @@ -12617,7 +12721,7 @@ impl Editor { self.active_indent_guides_state.dirty = true; self.refresh_active_diagnostics(cx); self.refresh_code_actions(cx); - if self.has_active_inline_completion(cx) { + if self.has_active_inline_completion() { self.update_visible_inline_completion(cx); } cx.emit(EditorEvent::BufferEdited); @@ -13310,10 +13414,10 @@ impl Editor { } pub fn display_to_pixel_point( - &mut self, + &self, source: DisplayPoint, editor_snapshot: &EditorSnapshot, - cx: &mut ViewContext, + cx: &WindowContext, ) -> Option> { let line_height = self.style()?.text.line_height_in_pixels(cx.rem_size()); let text_layout_details = self.text_layout_details(cx); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 2df6d66b6a..c3156da602 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -19,10 +19,10 @@ use crate::{ BlockId, ChunkReplacement, CodeActionsMenu, CursorShape, CustomBlockId, DisplayPoint, DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GutterDimensions, HalfPageDown, - HalfPageUp, HandleInput, HoveredCursor, HoveredHunk, JumpData, LineDown, LineUp, OpenExcerpts, - PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase, Selection, SoftWrap, ToPoint, - CURSORS_VISIBLE_FOR, FILE_HEADER_HEIGHT, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN, - MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, + HalfPageUp, HandleInput, HoveredCursor, HoveredHunk, InlineCompletion, JumpData, LineDown, + LineUp, OpenExcerpts, PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase, Selection, + SoftWrap, ToPoint, CURSORS_VISIBLE_FOR, FILE_HEADER_HEIGHT, + GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, }; use client::ParticipantIndex; use collections::{BTreeMap, HashMap, HashSet}; @@ -31,7 +31,7 @@ use gpui::{ anchored, deferred, div, fill, outline, point, px, quad, relative, size, svg, transparent_black, Action, AnchorCorner, AnyElement, AvailableSpace, Bounds, ClipboardItem, ContentMask, Corners, CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Entity, - FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, Length, + FontId, GlobalElementId, HighlightStyle, Hitbox, Hsla, InteractiveElement, IntoElement, Length, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled, TextRun, TextStyleRefinement, View, ViewContext, @@ -47,7 +47,10 @@ use language::{ ChunkRendererContext, }; use lsp::DiagnosticSeverity; -use multi_buffer::{Anchor, ExcerptId, ExpandExcerptDirection, MultiBufferPoint, MultiBufferRow}; +use multi_buffer::{ + Anchor, AnchorRangeExt, ExcerptId, ExpandExcerptDirection, MultiBufferPoint, MultiBufferRow, + MultiBufferSnapshot, +}; use project::{ project_settings::{GitGutterSetting, ProjectSettings}, ProjectPath, @@ -2720,6 +2723,157 @@ impl EditorElement { true } + #[allow(clippy::too_many_arguments)] + fn layout_inline_completion_popover( + &self, + text_bounds: &Bounds, + editor_snapshot: &EditorSnapshot, + visible_row_range: Range, + scroll_top: f32, + scroll_bottom: f32, + line_layouts: &[LineWithInvisibles], + line_height: Pixels, + scroll_pixel_position: gpui::Point, + editor_width: Pixels, + style: &EditorStyle, + cx: &mut WindowContext, + ) -> Option { + const PADDING_X: Pixels = Pixels(25.); + const PADDING_Y: Pixels = Pixels(2.); + + let active_inline_completion = self.editor.read(cx).active_inline_completion.as_ref()?; + + match &active_inline_completion.completion { + InlineCompletion::Move(target_position) => { + let container_element = div() + .bg(cx.theme().colors().editor_background) + .border_1() + .border_color(cx.theme().colors().border) + .rounded_md() + .px_1(); + + let target_display_point = target_position.to_display_point(editor_snapshot); + if target_display_point.row().as_f32() < scroll_top { + let mut element = container_element + .child( + h_flex() + .gap_1() + .child(Icon::new(IconName::Tab)) + .child(Label::new("Jump to Edit")) + .child(Icon::new(IconName::ArrowUp)), + ) + .into_any(); + let size = element.layout_as_root(AvailableSpace::min_size(), cx); + let offset = point((text_bounds.size.width - size.width) / 2., PADDING_Y); + element.prepaint_at(text_bounds.origin + offset, cx); + Some(element) + } else if (target_display_point.row().as_f32() + 1.) > scroll_bottom { + let mut element = container_element + .child( + h_flex() + .gap_1() + .child(Icon::new(IconName::Tab)) + .child(Label::new("Jump to Edit")) + .child(Icon::new(IconName::ArrowDown)), + ) + .into_any(); + let size = element.layout_as_root(AvailableSpace::min_size(), cx); + let offset = point( + (text_bounds.size.width - size.width) / 2., + text_bounds.size.height - size.height - PADDING_Y, + ); + element.prepaint_at(text_bounds.origin + offset, cx); + Some(element) + } else { + let mut element = container_element + .child( + h_flex() + .gap_1() + .child(Icon::new(IconName::Tab)) + .child(Label::new("Jump to Edit")), + ) + .into_any(); + + let target_line_end = DisplayPoint::new( + target_display_point.row(), + editor_snapshot.line_len(target_display_point.row()), + ); + let origin = self.editor.update(cx, |editor, cx| { + editor.display_to_pixel_point(target_line_end, editor_snapshot, cx) + })?; + element.prepaint_as_root( + text_bounds.origin + origin + point(PADDING_X, px(0.)), + AvailableSpace::min_size(), + cx, + ); + Some(element) + } + } + InlineCompletion::Edit(edits) => { + let edit_start = edits + .first() + .unwrap() + .0 + .start + .to_display_point(editor_snapshot); + let edit_end = edits + .last() + .unwrap() + .0 + .end + .to_display_point(editor_snapshot); + + let is_visible = visible_row_range.contains(&edit_start.row()) + || visible_row_range.contains(&edit_end.row()); + if !is_visible { + return None; + } + + if all_edits_insertions_or_deletions(edits, &editor_snapshot.buffer_snapshot) { + return None; + } + + let (text, highlights) = + inline_completion_popover_text(edit_start, editor_snapshot, edits, cx); + + let longest_row = + editor_snapshot.longest_row_in_range(edit_start.row()..edit_end.row() + 1); + let longest_line_width = if visible_row_range.contains(&longest_row) { + line_layouts[(longest_row.0 - visible_row_range.start.0) as usize].width + } else { + layout_line( + longest_row, + editor_snapshot, + style, + editor_width, + |_| false, + cx, + ) + .width + }; + + let text = gpui::StyledText::new(text).with_highlights(&style.text, highlights); + + let mut element = div() + .bg(cx.theme().colors().editor_background) + .border_1() + .border_color(cx.theme().colors().border) + .rounded_md() + .px_1() + .child(text) + .into_any(); + + let origin = text_bounds.origin + + point( + longest_line_width + PADDING_X - scroll_pixel_position.x, + edit_start.row().as_f32() * line_height - scroll_pixel_position.y, + ); + element.prepaint_as_root(origin, AvailableSpace::min_size(), cx); + Some(element) + } + } + } + fn layout_mouse_context_menu( &self, editor_snapshot: &EditorSnapshot, @@ -3942,6 +4096,16 @@ impl EditorElement { } } + fn paint_inline_completion_popover( + &mut self, + layout: &mut EditorLayout, + cx: &mut WindowContext, + ) { + if let Some(inline_completion_popover) = layout.inline_completion_popover.as_mut() { + inline_completion_popover.paint(cx); + } + } + fn paint_mouse_context_menu(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) { if let Some(mouse_context_menu) = layout.mouse_context_menu.as_mut() { mouse_context_menu.paint(cx); @@ -4134,6 +4298,67 @@ impl EditorElement { } } +fn inline_completion_popover_text( + edit_start: DisplayPoint, + editor_snapshot: &EditorSnapshot, + edits: &Vec<(Range, String)>, + cx: &WindowContext, +) -> (String, Vec<(Range, HighlightStyle)>) { + let mut text = String::new(); + let mut offset = DisplayPoint::new(edit_start.row(), 0).to_offset(editor_snapshot, Bias::Left); + let mut highlights = Vec::new(); + for (old_range, new_text) in edits { + let old_offset_range = old_range.to_offset(&editor_snapshot.buffer_snapshot); + text.extend( + editor_snapshot + .buffer_snapshot + .chunks(offset..old_offset_range.start, false) + .map(|chunk| chunk.text), + ); + offset = old_offset_range.end; + + let start = text.len(); + text.push_str(new_text); + let end = text.len(); + highlights.push(( + start..end, + HighlightStyle { + background_color: Some(cx.theme().status().created_background), + ..Default::default() + }, + )); + } + (text, highlights) +} + +fn all_edits_insertions_or_deletions( + edits: &Vec<(Range, String)>, + snapshot: &MultiBufferSnapshot, +) -> bool { + let mut all_insertions = true; + let mut all_deletions = true; + + for (range, new_text) in edits.iter() { + let range_is_empty = range.to_offset(&snapshot).is_empty(); + let text_is_empty = new_text.is_empty(); + + if range_is_empty != text_is_empty { + if range_is_empty { + all_deletions = false; + } else { + all_insertions = false; + } + } else { + return false; + } + + if !all_insertions && !all_deletions { + return false; + } + } + all_insertions || all_deletions +} + #[allow(clippy::too_many_arguments)] fn prepaint_gutter_button( button: IconButton, @@ -5566,6 +5791,20 @@ impl Element for EditorElement { ); } + let inline_completion_popover = self.layout_inline_completion_popover( + &text_hitbox.bounds, + &snapshot, + start_row..end_row, + scroll_position.y, + scroll_position.y + height_in_lines, + &line_layouts, + line_height, + scroll_pixel_position, + editor_width, + &style, + cx, + ); + let mouse_context_menu = self.layout_mouse_context_menu( &snapshot, start_row..end_row, @@ -5652,6 +5891,7 @@ impl Element for EditorElement { cursors, visible_cursors, selections, + inline_completion_popover, mouse_context_menu, test_indicators, code_actions_indicator, @@ -5741,6 +5981,7 @@ impl Element for EditorElement { } self.paint_scrollbar(layout, cx); + self.paint_inline_completion_popover(layout, cx); self.paint_mouse_context_menu(layout, cx); }); }) @@ -5796,6 +6037,7 @@ pub struct EditorLayout { test_indicators: Vec, crease_toggles: Vec>, crease_trailers: Vec>, + inline_completion_popover: Option, mouse_context_menu: Option, tab_invisible: ShapedLine, space_invisible: ShapedLine, @@ -6837,6 +7079,169 @@ mod tests { } } + #[gpui::test] + fn test_inline_completion_popover_text(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + // Test case 1: Simple insertion + { + let window = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("Hello, world!", cx); + Editor::new(EditorMode::Full, buffer, None, true, cx) + }); + let cx = &mut VisualTestContext::from_window(*window, cx); + + window + .update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + let edit_range = snapshot.buffer_snapshot.anchor_after(Point::new(0, 6)) + ..snapshot.buffer_snapshot.anchor_before(Point::new(0, 6)); + let edit_start = DisplayPoint::new(DisplayRow(0), 6); + let edits = vec![(edit_range, " beautiful".to_string())]; + + let (text, highlights) = + inline_completion_popover_text(edit_start, &snapshot, &edits, cx); + + assert_eq!(text, "Hello, beautiful"); + assert_eq!(highlights.len(), 1); + assert_eq!(highlights[0].0, 6..16); + assert_eq!( + highlights[0].1.background_color, + Some(cx.theme().status().created_background) + ); + }) + .unwrap(); + } + + // Test case 2: Replacement + { + let window = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("This is a test.", cx); + Editor::new(EditorMode::Full, buffer, None, true, cx) + }); + let cx = &mut VisualTestContext::from_window(*window, cx); + + window + .update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + let edit_start = DisplayPoint::new(DisplayRow(0), 0); + let edits = vec![( + snapshot.buffer_snapshot.anchor_after(Point::new(0, 0)) + ..snapshot.buffer_snapshot.anchor_before(Point::new(0, 4)), + "That".to_string(), + )]; + + let (text, highlights) = + inline_completion_popover_text(edit_start, &snapshot, &edits, cx); + + assert_eq!(text, "That"); + assert_eq!(highlights.len(), 1); + assert_eq!(highlights[0].0, 0..4); + assert_eq!( + highlights[0].1.background_color, + Some(cx.theme().status().created_background) + ); + }) + .unwrap(); + } + + // Test case 3: Multiple edits + { + let window = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("Hello, world!", cx); + Editor::new(EditorMode::Full, buffer, None, true, cx) + }); + let cx = &mut VisualTestContext::from_window(*window, cx); + + window + .update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + let edit_start = DisplayPoint::new(DisplayRow(0), 0); + let edits = vec![ + ( + snapshot.buffer_snapshot.anchor_after(Point::new(0, 0)) + ..snapshot.buffer_snapshot.anchor_before(Point::new(0, 5)), + "Greetings".into(), + ), + ( + snapshot.buffer_snapshot.anchor_after(Point::new(0, 12)) + ..snapshot.buffer_snapshot.anchor_before(Point::new(0, 13)), + " and universe".into(), + ), + ]; + + let (text, highlights) = + inline_completion_popover_text(edit_start, &snapshot, &edits, cx); + + assert_eq!(text, "Greetings, world and universe"); + assert_eq!(highlights.len(), 2); + assert_eq!(highlights[0].0, 0..9); + assert_eq!(highlights[1].0, 16..29); + assert_eq!( + highlights[0].1.background_color, + Some(cx.theme().status().created_background) + ); + assert_eq!( + highlights[1].1.background_color, + Some(cx.theme().status().created_background) + ); + }) + .unwrap(); + } + + // Test case 4: Multiple lines with edits + { + let window = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple( + "First line\nSecond line\nThird line\nFourth line", + cx, + ); + Editor::new(EditorMode::Full, buffer, None, true, cx) + }); + let cx = &mut VisualTestContext::from_window(*window, cx); + + window + .update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + let edit_start = DisplayPoint::new(DisplayRow(1), 0); + let edits = vec![ + ( + snapshot.buffer_snapshot.anchor_before(Point::new(1, 7)) + ..snapshot.buffer_snapshot.anchor_before(Point::new(1, 11)), + "modified".to_string(), + ), + ( + snapshot.buffer_snapshot.anchor_before(Point::new(2, 0)) + ..snapshot.buffer_snapshot.anchor_before(Point::new(2, 10)), + "New third line".to_string(), + ), + ( + snapshot.buffer_snapshot.anchor_before(Point::new(3, 6)) + ..snapshot.buffer_snapshot.anchor_before(Point::new(3, 6)), + " updated".to_string(), + ), + ]; + + let (text, highlights) = + inline_completion_popover_text(edit_start, &snapshot, &edits, cx); + + assert_eq!(text, "Second modified\nNew third line\nFourth updated"); + assert_eq!(highlights.len(), 3); + assert_eq!(highlights[0].0, 7..15); // "modified" + assert_eq!(highlights[1].0, 16..30); // "New third line" + assert_eq!(highlights[2].0, 37..45); // " updated" + + for highlight in &highlights { + assert_eq!( + highlight.1.background_color, + Some(cx.theme().status().created_background) + ); + } + }) + .unwrap(); + } + } + fn collect_invisibles_from_new_editor( cx: &mut TestAppContext, editor_mode: EditorMode, diff --git a/crates/editor/src/inline_completion_tests.rs b/crates/editor/src/inline_completion_tests.rs new file mode 100644 index 0000000000..b136f8ab1a --- /dev/null +++ b/crates/editor/src/inline_completion_tests.rs @@ -0,0 +1,360 @@ +use gpui::Model; +use indoc::indoc; +use inline_completion::InlineCompletionProvider; +use multi_buffer::{Anchor, MultiBufferSnapshot, ToPoint}; +use std::ops::Range; +use text::{Point, ToOffset}; +use ui::Context; + +use crate::{ + editor_tests::init_test, test::editor_test_context::EditorTestContext, InlineCompletion, +}; + +#[gpui::test] +async fn test_inline_completion_insert(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + let provider = cx.new_model(|_| FakeInlineCompletionProvider::default()); + assign_editor_completion_provider(provider.clone(), &mut cx); + cx.set_state("let absolute_zero_celsius = ˇ;"); + + propose_edits(&provider, vec![(28..28, "-273.15")], &mut cx); + cx.update_editor(|editor, cx| editor.update_visible_inline_completion(cx)); + + assert_editor_active_edit_completion(&mut cx, |_, edits| { + assert_eq!(edits.len(), 1); + assert_eq!(edits[0].1.as_str(), "-273.15"); + }); + + accept_completion(&mut cx); + + cx.assert_editor_state("let absolute_zero_celsius = -273.15ˇ;") +} + +#[gpui::test] +async fn test_inline_completion_modification(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + let provider = cx.new_model(|_| FakeInlineCompletionProvider::default()); + assign_editor_completion_provider(provider.clone(), &mut cx); + cx.set_state("let pi = ˇ\"foo\";"); + + propose_edits(&provider, vec![(9..14, "3.14159")], &mut cx); + cx.update_editor(|editor, cx| editor.update_visible_inline_completion(cx)); + + assert_editor_active_edit_completion(&mut cx, |_, edits| { + assert_eq!(edits.len(), 1); + assert_eq!(edits[0].1.as_str(), "3.14159"); + }); + + accept_completion(&mut cx); + + cx.assert_editor_state("let pi = 3.14159ˇ;") +} + +#[gpui::test] +async fn test_inline_completion_jump_button(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + let provider = cx.new_model(|_| FakeInlineCompletionProvider::default()); + assign_editor_completion_provider(provider.clone(), &mut cx); + + // Cursor is 2+ lines above the proposed edit + cx.set_state(indoc! {" + line 0 + line ˇ1 + line 2 + line 3 + line + "}); + + propose_edits( + &provider, + vec![(Point::new(4, 3)..Point::new(4, 3), " 4")], + &mut cx, + ); + + cx.update_editor(|editor, cx| editor.update_visible_inline_completion(cx)); + assert_editor_active_move_completion(&mut cx, |snapshot, move_target| { + assert_eq!(move_target.to_point(&snapshot), Point::new(4, 3)); + }); + + // When accepting, cursor is moved to the proposed location + accept_completion(&mut cx); + cx.assert_editor_state(indoc! {" + line 0 + line 1 + line 2 + line 3 + linˇe + "}); + + // Cursor is 2+ lines below the proposed edit + cx.set_state(indoc! {" + line 0 + line + line 2 + line 3 + line ˇ4 + "}); + + propose_edits( + &provider, + vec![(Point::new(1, 3)..Point::new(1, 3), " 1")], + &mut cx, + ); + + cx.update_editor(|editor, cx| editor.update_visible_inline_completion(cx)); + assert_editor_active_move_completion(&mut cx, |snapshot, move_target| { + assert_eq!(move_target.to_point(&snapshot), Point::new(1, 3)); + }); + + // When accepting, cursor is moved to the proposed location + accept_completion(&mut cx); + cx.assert_editor_state(indoc! {" + line 0 + linˇe + line 2 + line 3 + line 4 + "}); +} + +#[gpui::test] +async fn test_inline_completion_invalidation_range(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + let provider = cx.new_model(|_| FakeInlineCompletionProvider::default()); + assign_editor_completion_provider(provider.clone(), &mut cx); + + // Cursor is 3+ lines above the proposed edit + cx.set_state(indoc! {" + line 0 + line ˇ1 + line 2 + line 3 + line 4 + line + "}); + let edit_location = Point::new(5, 3); + + propose_edits( + &provider, + vec![(edit_location..edit_location, " 5")], + &mut cx, + ); + + cx.update_editor(|editor, cx| editor.update_visible_inline_completion(cx)); + assert_editor_active_move_completion(&mut cx, |snapshot, move_target| { + assert_eq!(move_target.to_point(&snapshot), edit_location); + }); + + // If we move *towards* the completion, it stays active + cx.set_selections_state(indoc! {" + line 0 + line 1 + line ˇ2 + line 3 + line 4 + line + "}); + assert_editor_active_move_completion(&mut cx, |snapshot, move_target| { + assert_eq!(move_target.to_point(&snapshot), edit_location); + }); + + // If we move *away* from the completion, it is discarded + cx.set_selections_state(indoc! {" + line ˇ0 + line 1 + line 2 + line 3 + line 4 + line + "}); + cx.editor(|editor, _| { + assert!(editor.active_inline_completion.is_none()); + }); + + // Cursor is 3+ lines below the proposed edit + cx.set_state(indoc! {" + line + line 1 + line 2 + line 3 + line ˇ4 + line 5 + "}); + let edit_location = Point::new(0, 3); + + propose_edits( + &provider, + vec![(edit_location..edit_location, " 0")], + &mut cx, + ); + + cx.update_editor(|editor, cx| editor.update_visible_inline_completion(cx)); + assert_editor_active_move_completion(&mut cx, |snapshot, move_target| { + assert_eq!(move_target.to_point(&snapshot), edit_location); + }); + + // If we move *towards* the completion, it stays active + cx.set_selections_state(indoc! {" + line + line 1 + line 2 + line ˇ3 + line 4 + line 5 + "}); + assert_editor_active_move_completion(&mut cx, |snapshot, move_target| { + assert_eq!(move_target.to_point(&snapshot), edit_location); + }); + + // If we move *away* from the completion, it is discarded + cx.set_selections_state(indoc! {" + line + line 1 + line 2 + line 3 + line 4 + line ˇ5 + "}); + cx.editor(|editor, _| { + assert!(editor.active_inline_completion.is_none()); + }); +} + +fn assert_editor_active_edit_completion( + cx: &mut EditorTestContext, + assert: impl FnOnce(MultiBufferSnapshot, &Vec<(Range, String)>), +) { + cx.editor(|editor, cx| { + let completion_state = editor + .active_inline_completion + .as_ref() + .expect("editor has no active completion"); + + if let InlineCompletion::Edit(edits) = &completion_state.completion { + assert(editor.buffer().read(cx).snapshot(cx), edits); + } else { + panic!("expected edit completion"); + } + }) +} + +fn assert_editor_active_move_completion( + cx: &mut EditorTestContext, + assert: impl FnOnce(MultiBufferSnapshot, Anchor), +) { + cx.editor(|editor, cx| { + let completion_state = editor + .active_inline_completion + .as_ref() + .expect("editor has no active completion"); + + if let InlineCompletion::Move(anchor) = &completion_state.completion { + assert(editor.buffer().read(cx).snapshot(cx), *anchor); + } else { + panic!("expected move completion"); + } + }) +} + +fn accept_completion(cx: &mut EditorTestContext) { + cx.update_editor(|editor, cx| { + editor.accept_inline_completion(&crate::AcceptInlineCompletion, cx) + }) +} + +fn propose_edits( + provider: &Model, + edits: Vec<(Range, &str)>, + cx: &mut EditorTestContext, +) { + let snapshot = cx.buffer_snapshot(); + let edits = edits.into_iter().map(|(range, text)| { + let range = snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end); + (range, text.into()) + }); + + cx.update(|cx| { + provider.update(cx, |provider, _| { + provider.set_inline_completion(Some(inline_completion::InlineCompletion { + edits: edits.collect(), + })) + }) + }); +} + +fn assign_editor_completion_provider( + provider: Model, + cx: &mut EditorTestContext, +) { + cx.update_editor(|editor, cx| { + editor.set_inline_completion_provider(Some(provider), cx); + }) +} + +#[derive(Default, Clone)] +struct FakeInlineCompletionProvider { + completion: Option, +} + +impl FakeInlineCompletionProvider { + pub fn set_inline_completion( + &mut self, + completion: Option, + ) { + self.completion = completion; + } +} + +impl InlineCompletionProvider for FakeInlineCompletionProvider { + fn name() -> &'static str { + "fake-completion-provider" + } + + fn is_enabled( + &self, + _buffer: &gpui::Model, + _cursor_position: language::Anchor, + _cx: &gpui::AppContext, + ) -> bool { + true + } + + fn refresh( + &mut self, + _buffer: gpui::Model, + _cursor_position: language::Anchor, + _debounce: bool, + _cx: &mut gpui::ModelContext, + ) { + } + + fn cycle( + &mut self, + _buffer: gpui::Model, + _cursor_position: language::Anchor, + _direction: inline_completion::Direction, + _cx: &mut gpui::ModelContext, + ) { + } + + fn accept(&mut self, _cx: &mut gpui::ModelContext) {} + + fn discard(&mut self, _cx: &mut gpui::ModelContext) {} + + fn suggest<'a>( + &mut self, + _buffer: &gpui::Model, + _cursor_position: language::Anchor, + _cx: &mut gpui::ModelContext, + ) -> Option { + self.completion.clone() + } +} diff --git a/crates/inline_completion/Cargo.toml b/crates/inline_completion/Cargo.toml index 237b0ff43f..cdcf71c230 100644 --- a/crates/inline_completion/Cargo.toml +++ b/crates/inline_completion/Cargo.toml @@ -14,5 +14,3 @@ path = "src/inline_completion.rs" [dependencies] gpui.workspace = true language.workspace = true -project.workspace = true -text.workspace = true diff --git a/crates/inline_completion/src/inline_completion.rs b/crates/inline_completion/src/inline_completion.rs index 689bc03174..fba19ca216 100644 --- a/crates/inline_completion/src/inline_completion.rs +++ b/crates/inline_completion/src/inline_completion.rs @@ -1,7 +1,6 @@ use gpui::{AppContext, Model, ModelContext}; use language::Buffer; use std::ops::Range; -use text::{Anchor, Rope}; // TODO: Find a better home for `Direction`. // @@ -13,15 +12,9 @@ pub enum Direction { Next, } -pub enum InlayProposal { - Hint(Anchor, project::InlayHint), - Suggestion(Anchor, Rope), -} - -pub struct CompletionProposal { - pub inlays: Vec, - pub text: Rope, - pub delete_range: Option>, +#[derive(Clone)] +pub struct InlineCompletion { + pub edits: Vec<(Range, String)>, } pub trait InlineCompletionProvider: 'static + Sized { @@ -47,16 +40,17 @@ pub trait InlineCompletionProvider: 'static + Sized { cx: &mut ModelContext, ); fn accept(&mut self, cx: &mut ModelContext); - fn discard(&mut self, should_report_inline_completion_event: bool, cx: &mut ModelContext); - fn active_completion_text<'a>( - &'a self, + fn discard(&mut self, cx: &mut ModelContext); + fn suggest( + &mut self, buffer: &Model, cursor_position: language::Anchor, - cx: &'a AppContext, - ) -> Option; + cx: &mut ModelContext, + ) -> Option; } pub trait InlineCompletionProviderHandle { + fn name(&self) -> &'static str; fn is_enabled( &self, buffer: &Model, @@ -78,19 +72,23 @@ pub trait InlineCompletionProviderHandle { cx: &mut AppContext, ); fn accept(&self, cx: &mut AppContext); - fn discard(&self, should_report_inline_completion_event: bool, cx: &mut AppContext); - fn active_completion_text<'a>( - &'a self, + fn discard(&self, cx: &mut AppContext); + fn suggest( + &self, buffer: &Model, cursor_position: language::Anchor, - cx: &'a AppContext, - ) -> Option; + cx: &mut AppContext, + ) -> Option; } impl InlineCompletionProviderHandle for Model where T: InlineCompletionProvider, { + fn name(&self) -> &'static str { + T::name() + } + fn is_enabled( &self, buffer: &Model, @@ -128,19 +126,16 @@ where self.update(cx, |this, cx| this.accept(cx)) } - fn discard(&self, should_report_inline_completion_event: bool, cx: &mut AppContext) { - self.update(cx, |this, cx| { - this.discard(should_report_inline_completion_event, cx) - }) + fn discard(&self, cx: &mut AppContext) { + self.update(cx, |this, cx| this.discard(cx)) } - fn active_completion_text<'a>( - &'a self, + fn suggest( + &self, buffer: &Model, cursor_position: language::Anchor, - cx: &'a AppContext, - ) -> Option { - self.read(cx) - .active_completion_text(buffer, cursor_position, cx) + cx: &mut AppContext, + ) -> Option { + self.update(cx, |this, cx| this.suggest(buffer, cursor_position, cx)) } } diff --git a/crates/inline_completion_button/Cargo.toml b/crates/inline_completion_button/Cargo.toml index 427d0dafd8..2029ab4da2 100644 --- a/crates/inline_completion_button/Cargo.toml +++ b/crates/inline_completion_button/Cargo.toml @@ -16,6 +16,7 @@ doctest = false anyhow.workspace = true copilot.workspace = true editor.workspace = true +feature_flags.workspace = true fs.workspace = true gpui.workspace = true language.workspace = true @@ -25,6 +26,7 @@ supermaven.workspace = true ui.workspace = true workspace.workspace = true zed_actions.workspace = true +zeta.workspace = true [dev-dependencies] copilot = { workspace = true, features = ["test-support"] } diff --git a/crates/inline_completion_button/src/inline_completion_button.rs b/crates/inline_completion_button/src/inline_completion_button.rs index 5470678d38..a18c250875 100644 --- a/crates/inline_completion_button/src/inline_completion_button.rs +++ b/crates/inline_completion_button/src/inline_completion_button.rs @@ -1,6 +1,7 @@ use anyhow::Result; use copilot::{Copilot, Status}; use editor::{scroll::Autoscroll, Editor}; +use feature_flags::FeatureFlagAppExt; use fs::Fs; use gpui::{ div, Action, AnchorCorner, AppContext, AsyncWindowContext, Entity, IntoElement, ParentElement, @@ -15,6 +16,7 @@ use language::{ use settings::{update_settings_file, Settings, SettingsStore}; use std::{path::Path, sync::Arc}; use supermaven::{AccountStatus, Supermaven}; +use ui::{Button, LabelSize}; use workspace::{ create_and_open_local_file, item::ItemHandle, @@ -25,6 +27,7 @@ use workspace::{ StatusItemView, Toast, Workspace, }; use zed_actions::OpenBrowser; +use zeta::RateCompletionModal; const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot"; @@ -36,6 +39,7 @@ pub struct InlineCompletionButton { language: Option>, file: Option>, fs: Arc, + workspace: WeakView, } enum SupermavenButtonStatus { @@ -193,12 +197,35 @@ impl Render for InlineCompletionButton { ), ); } + + InlineCompletionProvider::Zeta => { + if !cx.is_staff() { + return div(); + } + + div().child( + Button::new("zeta", "Zeta") + .label_size(LabelSize::Small) + .on_click(cx.listener(|this, _, cx| { + if let Some(workspace) = this.workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + RateCompletionModal::toggle(workspace, cx) + }); + } + })) + .tooltip(|cx| Tooltip::text("Rate Completions", cx)), + ) + } } } } impl InlineCompletionButton { - pub fn new(fs: Arc, cx: &mut ViewContext) -> Self { + pub fn new( + workspace: WeakView, + fs: Arc, + cx: &mut ViewContext, + ) -> Self { if let Some(copilot) = Copilot::global(cx) { cx.observe(&copilot, |_, _, cx| cx.notify()).detach() } @@ -211,6 +238,7 @@ impl InlineCompletionButton { editor_enabled: None, language: None, file: None, + workspace, fs, } } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 833a71c899..b3a9530847 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -563,7 +563,7 @@ impl<'a, 'b> DerefMut for ChunkRendererContext<'a, 'b> { pub struct Diff { pub(crate) base_version: clock::Global, line_ending: LineEnding, - edits: Vec<(Range, Arc)>, + pub edits: Vec<(Range, Arc)>, } #[derive(Clone, Copy)] diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index a3ac40b714..5f3227cea8 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -197,6 +197,7 @@ pub enum InlineCompletionProvider { #[default] Copilot, Supermaven, + Zeta, } /// The settings for inline completions, such as [GitHub Copilot](https://github.com/features/copilot) diff --git a/crates/language_models/src/language_models.rs b/crates/language_models/src/language_models.rs index 028ea0cfa4..6d618d1ec5 100644 --- a/crates/language_models/src/language_models.rs +++ b/crates/language_models/src/language_models.rs @@ -10,7 +10,9 @@ pub mod provider; mod settings; use crate::provider::anthropic::AnthropicLanguageModelProvider; -use crate::provider::cloud::{CloudLanguageModelProvider, RefreshLlmTokenListener}; +use crate::provider::cloud::CloudLanguageModelProvider; +pub use crate::provider::cloud::LlmApiToken; +pub use crate::provider::cloud::RefreshLlmTokenListener; use crate::provider::copilot_chat::CopilotChatLanguageModelProvider; use crate::provider::google::GoogleLanguageModelProvider; use crate::provider::ollama::OllamaLanguageModelProvider; diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index f54e8c8d19..6d76b733b7 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -444,7 +444,7 @@ pub struct CloudLanguageModel { } #[derive(Clone, Default)] -struct LlmApiToken(Arc>>); +pub struct LlmApiToken(Arc>>); #[derive(Error, Debug)] pub struct PaymentRequiredError; @@ -814,7 +814,7 @@ fn response_lines( } impl LlmApiToken { - async fn acquire(&self, client: &Arc) -> Result { + pub async fn acquire(&self, client: &Arc) -> Result { let lock = self.0.upgradable_read().await; if let Some(token) = lock.as_ref() { Ok(token.to_string()) @@ -823,7 +823,7 @@ impl LlmApiToken { } } - async fn refresh(&self, client: &Arc) -> Result { + pub async fn refresh(&self, client: &Arc) -> Result { Self::fetch(self.0.write().await, client).await } diff --git a/crates/open_ai/src/open_ai.rs b/crates/open_ai/src/open_ai.rs index dfafff2089..1e92841249 100644 --- a/crates/open_ai/src/open_ai.rs +++ b/crates/open_ai/src/open_ai.rs @@ -169,6 +169,24 @@ pub struct Request { pub tools: Vec, } +#[derive(Debug, Serialize, Deserialize)] +pub struct CompletionRequest { + pub model: String, + pub prompt: String, + pub max_tokens: u32, + pub temperature: f32, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub prediction: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub rewrite_speculation: Option, +} + +#[derive(Clone, Deserialize, Serialize, Debug)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum Prediction { + Content { content: String }, +} + #[derive(Debug, Serialize, Deserialize)] #[serde(untagged)] pub enum ToolChoice { @@ -285,6 +303,21 @@ pub struct ResponseStreamEvent { pub usage: Option, } +#[derive(Serialize, Deserialize, Debug)] +pub struct CompletionResponse { + pub id: String, + pub object: String, + pub created: u64, + pub model: String, + pub choices: Vec, + pub usage: Usage, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct CompletionChoice { + pub text: String, +} + #[derive(Serialize, Deserialize, Debug)] pub struct Response { pub id: String, @@ -355,6 +388,56 @@ pub async fn complete( } } +pub async fn complete_text( + client: &dyn HttpClient, + api_url: &str, + api_key: &str, + request: CompletionRequest, +) -> Result { + let uri = format!("{api_url}/completions"); + let request_builder = HttpRequest::builder() + .method(Method::POST) + .uri(uri) + .header("Content-Type", "application/json") + .header("Authorization", format!("Bearer {}", api_key)); + + let request = request_builder.body(AsyncBody::from(serde_json::to_string(&request)?))?; + let mut response = client.send(request).await?; + + if response.status().is_success() { + let mut body = String::new(); + response.body_mut().read_to_string(&mut body).await?; + let response = serde_json::from_str(&body)?; + Ok(response) + } else { + let mut body = String::new(); + response.body_mut().read_to_string(&mut body).await?; + + #[derive(Deserialize)] + struct OpenAiResponse { + error: OpenAiError, + } + + #[derive(Deserialize)] + struct OpenAiError { + message: String, + } + + match serde_json::from_str::(&body) { + Ok(response) if !response.error.message.is_empty() => Err(anyhow!( + "Failed to connect to OpenAI API: {}", + response.error.message, + )), + + _ => Err(anyhow!( + "Failed to connect to OpenAI API: {} {}", + response.status(), + body, + )), + } + } +} + fn adapt_response_to_stream(response: Response) -> ResponseStreamEvent { ResponseStreamEvent { created: response.created as u32, diff --git a/crates/rpc/src/llm.rs b/crates/rpc/src/llm.rs index 0a7510d891..975114350a 100644 --- a/crates/rpc/src/llm.rs +++ b/crates/rpc/src/llm.rs @@ -33,3 +33,14 @@ pub struct PerformCompletionParams { pub model: String, pub provider_request: Box, } + +#[derive(Debug, Serialize, Deserialize)] +pub struct PredictEditsParams { + pub input_events: String, + pub input_excerpt: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PredictEditsResponse { + pub output_excerpt: String, +} diff --git a/crates/supermaven/src/supermaven_completion_provider.rs b/crates/supermaven/src/supermaven_completion_provider.rs index 5e77cc21ef..a943054d83 100644 --- a/crates/supermaven/src/supermaven_completion_provider.rs +++ b/crates/supermaven/src/supermaven_completion_provider.rs @@ -1,14 +1,12 @@ use crate::{Supermaven, SupermavenCompletionStateId}; use anyhow::Result; -use client::telemetry::Telemetry; use futures::StreamExt as _; use gpui::{AppContext, EntityId, Model, ModelContext, Task}; -use inline_completion::{CompletionProposal, Direction, InlayProposal, InlineCompletionProvider}; +use inline_completion::{Direction, InlineCompletion, InlineCompletionProvider}; use language::{language_settings::all_language_settings, Anchor, Buffer, BufferSnapshot}; use std::{ ops::{AddAssign, Range}, path::Path, - sync::Arc, time::Duration, }; use text::{ToOffset, ToPoint}; @@ -22,7 +20,6 @@ pub struct SupermavenCompletionProvider { completion_id: Option, file_extension: Option, pending_refresh: Task>, - telemetry: Option>, } impl SupermavenCompletionProvider { @@ -33,31 +30,25 @@ impl SupermavenCompletionProvider { completion_id: None, file_extension: None, pending_refresh: Task::ready(Ok(())), - telemetry: None, } } - - pub fn with_telemetry(mut self, telemetry: Arc) -> Self { - self.telemetry = Some(telemetry); - self - } } -// Computes the completion state from the difference between the completion text. +// Computes the inline completion from the difference between the completion text. // this is defined by greedily matching the buffer text against the completion text, with any leftover buffer placed at the end. // for example, given the completion text "moo cows are cool" and the buffer text "cowsre pool", the completion state would be // the inlays "moo ", " a", and "cool" which will render as "[moo ]cows[ a]re [cool]pool" in the editor. -fn completion_state_from_diff( +fn completion_from_diff( snapshot: BufferSnapshot, completion_text: &str, position: Anchor, delete_range: Range, -) -> CompletionProposal { +) -> InlineCompletion { let buffer_text = snapshot .text_for_range(delete_range.clone()) .collect::(); - let mut inlays: Vec = Vec::new(); + let mut edits: Vec<(Range, String)> = Vec::new(); let completion_graphemes: Vec<&str> = completion_text.graphemes(true).collect(); let buffer_graphemes: Vec<&str> = buffer_text.graphemes(true).collect(); @@ -74,11 +65,10 @@ fn completion_state_from_diff( match k { Some(k) => { if k != 0 { + let offset = snapshot.anchor_after(offset); // the range from the current position to item is an inlay. - inlays.push(InlayProposal::Suggestion( - snapshot.anchor_after(offset), - completion_graphemes[i..i + k].join("").into(), - )); + let edit = (offset..offset, completion_graphemes[i..i + k].join("")); + edits.push(edit); } i += k + 1; j += 1; @@ -93,18 +83,14 @@ fn completion_state_from_diff( } if j == buffer_graphemes.len() && i < completion_graphemes.len() { + let offset = snapshot.anchor_after(offset); // there is leftover completion text, so drop it as an inlay. - inlays.push(InlayProposal::Suggestion( - snapshot.anchor_after(offset), - completion_graphemes[i..].join("").into(), - )); + let edit_range = offset..offset; + let edit_text = completion_graphemes[i..].join(""); + edits.push((edit_range, edit_text)); } - CompletionProposal { - inlays, - text: completion_text.into(), - delete_range: Some(delete_range), - } + InlineCompletion { edits } } impl InlineCompletionProvider for SupermavenCompletionProvider { @@ -171,44 +157,21 @@ impl InlineCompletionProvider for SupermavenCompletionProvider { } fn accept(&mut self, _cx: &mut ModelContext) { - if self.completion_id.is_some() { - if let Some(telemetry) = self.telemetry.as_ref() { - telemetry.report_inline_completion_event( - Self::name().to_string(), - true, - self.file_extension.clone(), - ); - } - } self.pending_refresh = Task::ready(Ok(())); self.completion_id = None; } - fn discard( + fn discard(&mut self, _cx: &mut ModelContext) { + self.pending_refresh = Task::ready(Ok(())); + self.completion_id = None; + } + + fn suggest( &mut self, - should_report_inline_completion_event: bool, - _cx: &mut ModelContext, - ) { - if should_report_inline_completion_event && self.completion_id.is_some() { - if let Some(telemetry) = self.telemetry.as_ref() { - telemetry.report_inline_completion_event( - Self::name().to_string(), - false, - self.file_extension.clone(), - ); - } - } - - self.pending_refresh = Task::ready(Ok(())); - self.completion_id = None; - } - - fn active_completion_text<'a>( - &'a self, buffer: &Model, cursor_position: Anchor, - cx: &'a AppContext, - ) -> Option { + cx: &mut ModelContext, + ) -> Option { let completion_text = self .supermaven .read(cx) @@ -223,7 +186,7 @@ impl InlineCompletionProvider for SupermavenCompletionProvider { let mut point = cursor_position.to_point(&snapshot); point.column = snapshot.line_len(point.row); let range = cursor_position..snapshot.anchor_after(point); - Some(completion_state_from_diff( + Some(completion_from_diff( snapshot, completion_text, cursor_position, diff --git a/crates/telemetry_events/src/telemetry_events.rs b/crates/telemetry_events/src/telemetry_events.rs index 0c4ee8cb9e..0002a169d4 100644 --- a/crates/telemetry_events/src/telemetry_events.rs +++ b/crates/telemetry_events/src/telemetry_events.rs @@ -93,6 +93,7 @@ impl Display for AssistantPhase { pub enum Event { Editor(EditorEvent), InlineCompletion(InlineCompletionEvent), + InlineCompletionRating(InlineCompletionRatingEvent), Call(CallEvent), Assistant(AssistantEvent), Cpu(CpuEvent), @@ -130,6 +131,21 @@ pub struct InlineCompletionEvent { pub file_extension: Option, } +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub enum InlineCompletionRating { + Positive, + Negative, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct InlineCompletionRatingEvent { + pub rating: InlineCompletionRating, + pub input_events: Arc, + pub input_excerpt: Arc, + pub output_excerpt: Arc, + pub feedback: String, +} + #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct CallEvent { /// Operation performed: invite/join call; begin/end screenshare; share/unshare project; etc diff --git a/crates/vim/src/normal/change.rs b/crates/vim/src/normal/change.rs index 59b5d3cb3d..ffa0ec8b96 100644 --- a/crates/vim/src/normal/change.rs +++ b/crates/vim/src/normal/change.rs @@ -77,6 +77,7 @@ impl Vim { }); vim.copy_selections_content(editor, motion.linewise(), cx); editor.insert("", cx); + editor.refresh_inline_completion(true, false, cx); }); }); @@ -101,6 +102,7 @@ impl Vim { if objects_found { vim.copy_selections_content(editor, false, cx); editor.insert("", cx); + editor.refresh_inline_completion(true, false, cx); } }); }); diff --git a/crates/vim/src/normal/delete.rs b/crates/vim/src/normal/delete.rs index fee2ef56e1..e633db1df0 100644 --- a/crates/vim/src/normal/delete.rs +++ b/crates/vim/src/normal/delete.rs @@ -72,6 +72,7 @@ impl Vim { selection.collapse_to(cursor, selection.goal) }); }); + editor.refresh_inline_completion(true, false, cx); }); }); } @@ -151,6 +152,7 @@ impl Vim { selection.collapse_to(cursor, selection.goal) }); }); + editor.refresh_inline_completion(true, false, cx); }); }); } diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 843b094700..43dbdc6316 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -1162,6 +1162,15 @@ impl Vim { if self.mode == Mode::Replace { self.multi_replace(text, cx) } + + if self.mode == Mode::Normal { + self.update_editor(cx, |_, editor, cx| { + editor.accept_inline_completion( + &editor::actions::AcceptInlineCompletion {}, + cx, + ); + }); + } } } } @@ -1174,7 +1183,10 @@ impl Vim { editor.set_input_enabled(vim.editor_input_enabled()); editor.set_autoindent(vim.should_autoindent()); editor.selections.line_mode = matches!(vim.mode, Mode::VisualLine); - editor.set_inline_completions_enabled(matches!(vim.mode, Mode::Insert | Mode::Replace)); + editor.set_inline_completions_enabled(matches!( + vim.mode, + Mode::Insert | Mode::Normal | Mode::Replace + )); }); cx.notify() } diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 2220cc7be0..1fbdcbab6f 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -126,6 +126,7 @@ vim_mode_setting.workspace = true welcome.workspace = true workspace.workspace = true zed_actions.workspace = true +zeta.workspace = true [target.'cfg(target_os = "windows")'.dependencies] windows.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index c598054356..3e2ec18f1f 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -399,7 +399,7 @@ fn main() { cx, ); snippet_provider::init(cx); - inline_completion_registry::init(app_state.client.telemetry().clone(), cx); + inline_completion_registry::init(app_state.client.clone(), cx); let prompt_builder = assistant::init( app_state.fs.clone(), app_state.client.clone(), diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index a52c8ec405..5829726ada 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -197,7 +197,7 @@ pub fn initialize_workspace( } let inline_completion_button = cx.new_view(|cx| { - inline_completion_button::InlineCompletionButton::new(app_state.fs.clone(), cx) + inline_completion_button::InlineCompletionButton::new(workspace.weak_handle(), app_state.fs.clone(), cx) }); let diagnostic_summary = diff --git a/crates/zed/src/zed/inline_completion_registry.rs b/crates/zed/src/zed/inline_completion_registry.rs index aa0707d851..2b9e300273 100644 --- a/crates/zed/src/zed/inline_completion_registry.rs +++ b/crates/zed/src/zed/inline_completion_registry.rs @@ -1,19 +1,20 @@ use std::{cell::RefCell, rc::Rc, sync::Arc}; -use client::telemetry::Telemetry; +use client::Client; use collections::HashMap; use copilot::{Copilot, CopilotCompletionProvider}; use editor::{Editor, EditorMode}; +use feature_flags::FeatureFlagAppExt; use gpui::{AnyWindowHandle, AppContext, Context, ViewContext, WeakView}; use language::language_settings::all_language_settings; use settings::SettingsStore; use supermaven::{Supermaven, SupermavenCompletionProvider}; -pub fn init(telemetry: Arc, cx: &mut AppContext) { +pub fn init(client: Arc, cx: &mut AppContext) { let editors: Rc, AnyWindowHandle>>> = Rc::default(); cx.observe_new_views({ let editors = editors.clone(); - let telemetry = telemetry.clone(); + let client = client.clone(); move |editor: &mut Editor, cx: &mut ViewContext| { if editor.mode() != EditorMode::Full { return; @@ -34,7 +35,7 @@ pub fn init(telemetry: Arc, cx: &mut AppContext) { .borrow_mut() .insert(editor_handle, cx.window_handle()); let provider = all_language_settings(None, cx).inline_completions.provider; - assign_inline_completion_provider(editor, provider, &telemetry, cx); + assign_inline_completion_provider(editor, provider, &client, cx); } }) .detach(); @@ -43,7 +44,7 @@ pub fn init(telemetry: Arc, cx: &mut AppContext) { for (editor, window) in editors.borrow().iter() { _ = window.update(cx, |_window, cx| { _ = editor.update(cx, |editor, cx| { - assign_inline_completion_provider(editor, provider, &telemetry, cx); + assign_inline_completion_provider(editor, provider, &client, cx); }) }); } @@ -55,7 +56,7 @@ pub fn init(telemetry: Arc, cx: &mut AppContext) { for (editor, window) in editors.borrow().iter() { _ = window.update(cx, |_window, cx| { _ = editor.update(cx, |editor, cx| { - assign_inline_completion_provider(editor, provider, &telemetry, cx); + assign_inline_completion_provider(editor, provider, &client, cx); }) }); } @@ -103,7 +104,7 @@ fn register_backward_compatible_actions(editor: &mut Editor, cx: &ViewContext, + client: &Arc, cx: &mut ViewContext, ) { match provider { @@ -117,17 +118,27 @@ fn assign_inline_completion_provider( }); } } - let provider = cx.new_model(|_| { - CopilotCompletionProvider::new(copilot).with_telemetry(telemetry.clone()) - }); + let provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot)); editor.set_inline_completion_provider(Some(provider), cx); } } language::language_settings::InlineCompletionProvider::Supermaven => { if let Some(supermaven) = Supermaven::global(cx) { - let provider = cx.new_model(|_| { - SupermavenCompletionProvider::new(supermaven).with_telemetry(telemetry.clone()) - }); + let provider = cx.new_model(|_| SupermavenCompletionProvider::new(supermaven)); + editor.set_inline_completion_provider(Some(provider), cx); + } + } + language::language_settings::InlineCompletionProvider::Zeta => { + if cx.is_staff() { + let zeta = zeta::Zeta::register(client.clone(), cx); + if let Some(buffer) = editor.buffer().read(cx).as_singleton() { + if buffer.read(cx).file().is_some() { + zeta.update(cx, |zeta, cx| { + zeta.register_buffer(&buffer, cx); + }); + } + } + let provider = cx.new_model(|_| zeta::ZetaInlineCompletionProvider::new(zeta)); editor.set_inline_completion_provider(Some(provider), cx); } } diff --git a/crates/zeta/Cargo.toml b/crates/zeta/Cargo.toml new file mode 100644 index 0000000000..0b07703eff --- /dev/null +++ b/crates/zeta/Cargo.toml @@ -0,0 +1,60 @@ +[package] +name = "zeta" +version = "0.1.0" +edition = "2021" +publish = false +license = "GPL-3.0-or-later" +exclude = ["fixtures"] + +[lints] +workspace = true + +[lib] +path = "src/zeta.rs" +doctest = false + +[dependencies] +anyhow.workspace = true +client.workspace = true +collections.workspace = true +editor.workspace = true +futures.workspace = true +gpui.workspace = true +http_client.workspace = true +inline_completion.workspace = true +language.workspace = true +language_models.workspace = true +log.workspace = true +menu.workspace = true +rpc.workspace = true +serde_json.workspace = true +settings.workspace = true +similar.workspace = true +telemetry_events.workspace = true +theme.workspace = true +util.workspace = true +ui.workspace = true +uuid.workspace = true +workspace.workspace = true + +[dev-dependencies] +collections = { workspace = true, features = ["test-support"] } +client = { workspace = true, features = ["test-support"] } +clock = { workspace = true, features = ["test-support"] } +ctor.workspace = true +editor = { workspace = true, features = ["test-support"] } +env_logger.workspace = true +gpui = { workspace = true, features = ["test-support"] } +http_client = { workspace = true, features = ["test-support"] } +indoc.workspace = true +language = { workspace = true, features = ["test-support"] } +reqwest_client = { workspace = true, features = ["test-support"] } +rpc = { workspace = true, features = ["test-support"] } +settings = { workspace = true, features = ["test-support"] } +theme = { workspace = true, features = ["test-support"] } +tree-sitter-go.workspace = true +tree-sitter-rust.workspace = true +util = { workspace = true, features = ["test-support"] } +workspace = { workspace = true, features = ["test-support"] } +worktree = { workspace = true, features = ["test-support"] } +call = { workspace = true, features = ["test-support"] } diff --git a/crates/zeta/LICENSE-GPL b/crates/zeta/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/zeta/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/zeta/src/rate_completion_modal.rs b/crates/zeta/src/rate_completion_modal.rs new file mode 100644 index 0000000000..2d5650ba16 --- /dev/null +++ b/crates/zeta/src/rate_completion_modal.rs @@ -0,0 +1,301 @@ +use crate::{InlineCompletion, InlineCompletionRating, Zeta}; +use editor::Editor; +use gpui::{ + prelude::*, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, HighlightStyle, + Model, StyledText, TextStyle, View, ViewContext, +}; +use language::{language_settings, OffsetRangeExt}; +use settings::Settings; +use theme::ThemeSettings; +use ui::{prelude::*, ListItem, ListItemSpacing}; +use workspace::{ModalView, Workspace}; + +pub struct RateCompletionModal { + zeta: Model, + active_completion: Option, + focus_handle: FocusHandle, + _subscription: gpui::Subscription, +} + +struct ActiveCompletion { + completion: InlineCompletion, + feedback_editor: View, +} + +impl RateCompletionModal { + pub fn toggle(workspace: &mut Workspace, cx: &mut ViewContext) { + if let Some(zeta) = Zeta::global(cx) { + workspace.toggle_modal(cx, |cx| RateCompletionModal::new(zeta, cx)); + } + } + + pub fn new(zeta: Model, cx: &mut ViewContext) -> Self { + let subscription = cx.observe(&zeta, |_, _, cx| cx.notify()); + Self { + zeta, + focus_handle: cx.focus_handle(), + active_completion: None, + _subscription: subscription, + } + } + + fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { + cx.emit(DismissEvent); + } + + pub fn select_completion( + &mut self, + completion: Option, + cx: &mut ViewContext, + ) { + // Avoid resetting completion rating if it's already selected. + if let Some(completion) = completion.as_ref() { + if let Some(prev_completion) = self.active_completion.as_ref() { + if completion.id == prev_completion.completion.id { + return; + } + } + } + + self.active_completion = completion.map(|completion| ActiveCompletion { + completion, + feedback_editor: cx.new_view(|cx| { + let mut editor = Editor::multi_line(cx); + editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx); + editor.set_show_line_numbers(false, cx); + editor.set_show_git_diff_gutter(false, cx); + editor.set_show_code_actions(false, cx); + editor.set_show_runnables(false, cx); + editor.set_show_wrap_guides(false, cx); + editor.set_show_indent_guides(false, cx); + editor.set_show_inline_completions(Some(false), cx); + editor.set_placeholder_text("Your feedback about this completion...", cx); + editor + }), + }); + } + + fn render_active_completion(&mut self, cx: &mut ViewContext) -> Option { + let active_completion = self.active_completion.as_ref()?; + let completion_id = active_completion.completion.id; + + let mut diff = active_completion + .completion + .snapshot + .text_for_range(active_completion.completion.excerpt_range.clone()) + .collect::(); + + let mut delta = 0; + let mut diff_highlights = Vec::new(); + for (old_range, new_text) in active_completion.completion.edits.iter() { + let old_range = old_range.to_offset(&active_completion.completion.snapshot); + let old_start_in_text = + old_range.start - active_completion.completion.excerpt_range.start + delta; + let old_end_in_text = + old_range.end - active_completion.completion.excerpt_range.start + delta; + if old_start_in_text < old_end_in_text { + diff_highlights.push(( + old_start_in_text..old_end_in_text, + HighlightStyle { + background_color: Some(cx.theme().status().deleted_background), + strikethrough: Some(gpui::StrikethroughStyle { + thickness: px(1.), + color: Some(cx.theme().colors().text_muted), + }), + ..Default::default() + }, + )); + } + + if !new_text.is_empty() { + diff.insert_str(old_end_in_text, new_text); + diff_highlights.push(( + old_end_in_text..old_end_in_text + new_text.len(), + HighlightStyle { + background_color: Some(cx.theme().status().created_background), + ..Default::default() + }, + )); + delta += new_text.len(); + } + } + + let settings = ThemeSettings::get_global(cx).clone(); + let text_style = TextStyle { + color: cx.theme().colors().editor_foreground, + font_size: settings.buffer_font_size(cx).into(), + font_family: settings.buffer_font.family, + font_features: settings.buffer_font.features, + font_fallbacks: settings.buffer_font.fallbacks, + line_height: relative(settings.buffer_line_height.value()), + font_weight: settings.buffer_font.weight, + font_style: settings.buffer_font.style, + ..Default::default() + }; + + let rated = self.zeta.read(cx).is_completion_rated(completion_id); + Some( + v_flex() + .flex_1() + .size_full() + .gap_2() + .child(h_flex().justify_center().children(if rated { + Some( + Label::new("This completion was already rated") + .color(Color::Muted) + .size(LabelSize::Large), + ) + } else if active_completion.completion.edits.is_empty() { + Some( + Label::new("This completion didn't produce any edits") + .color(Color::Warning) + .size(LabelSize::Large), + ) + } else { + None + })) + .child( + v_flex() + .id("diff") + .flex_1() + .flex_basis(relative(0.75)) + .bg(cx.theme().colors().editor_background) + .overflow_y_scroll() + .p_2() + .border_color(cx.theme().colors().border) + .border_1() + .rounded_lg() + .child(StyledText::new(diff).with_highlights(&text_style, diff_highlights)), + ) + .child( + div() + .flex_1() + .flex_basis(relative(0.25)) + .bg(cx.theme().colors().editor_background) + .border_color(cx.theme().colors().border) + .border_1() + .rounded_lg() + .child(active_completion.feedback_editor.clone()), + ) + .child( + h_flex() + .gap_2() + .justify_end() + .child( + Button::new("bad", "👎 Bad Completion") + .size(ButtonSize::Large) + .disabled(rated) + .label_size(LabelSize::Large) + .color(Color::Error) + .on_click({ + let completion = active_completion.completion.clone(); + let feedback_editor = active_completion.feedback_editor.clone(); + cx.listener(move |this, _, cx| { + this.zeta.update(cx, |zeta, cx| { + zeta.rate_completion( + &completion, + InlineCompletionRating::Negative, + feedback_editor.read(cx).text(cx), + cx, + ) + }) + }) + }), + ) + .child( + Button::new("good", "👍 Good Completion") + .size(ButtonSize::Large) + .disabled(rated) + .label_size(LabelSize::Large) + .color(Color::Success) + .on_click({ + let completion = active_completion.completion.clone(); + let feedback_editor = active_completion.feedback_editor.clone(); + cx.listener(move |this, _, cx| { + this.zeta.update(cx, |zeta, cx| { + zeta.rate_completion( + &completion, + InlineCompletionRating::Positive, + feedback_editor.read(cx).text(cx), + cx, + ) + }) + }) + }), + ), + ), + ) + } +} + +impl Render for RateCompletionModal { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + h_flex() + .gap_2() + .bg(cx.theme().colors().elevated_surface_background) + .w(cx.viewport_size().width - px(256.)) + .h(cx.viewport_size().height - px(256.)) + .rounded_lg() + .shadow_lg() + .p_2() + .key_context("RateCompletionModal") + .track_focus(&self.focus_handle) + .on_action(cx.listener(Self::dismiss)) + .child( + div() + .id("completion_list") + .w_96() + .h_full() + .overflow_y_scroll() + .child( + ui::List::new() + .empty_message( + "No completions, use the editor to generate some and rate them!", + ) + .children(self.zeta.read(cx).recent_completions().cloned().map( + |completion| { + let selected = + self.active_completion.as_ref().map_or(false, |selected| { + selected.completion.id == completion.id + }); + let rated = + self.zeta.read(cx).is_completion_rated(completion.id); + ListItem::new(completion.id) + .spacing(ListItemSpacing::Sparse) + .selected(selected) + .end_slot(if rated { + Icon::new(IconName::Check).color(Color::Success) + } else if completion.edits.is_empty() { + Icon::new(IconName::Ellipsis).color(Color::Muted) + } else { + Icon::new(IconName::Diff).color(Color::Muted) + }) + .child(Label::new( + completion.path.to_string_lossy().to_string(), + )) + .child( + Label::new(format!("({})", completion.id)) + .color(Color::Muted) + .size(LabelSize::XSmall), + ) + .on_click(cx.listener(move |this, _, cx| { + this.select_completion(Some(completion.clone()), cx); + })) + }, + )), + ), + ) + .children(self.render_active_completion(cx)) + .on_mouse_down_out(cx.listener(|_, _, cx| cx.emit(DismissEvent))) + } +} + +impl EventEmitter for RateCompletionModal {} + +impl FocusableView for RateCompletionModal { + fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl ModalView for RateCompletionModal {} diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs new file mode 100644 index 0000000000..dea15b0b08 --- /dev/null +++ b/crates/zeta/src/zeta.rs @@ -0,0 +1,960 @@ +mod rate_completion_modal; + +pub use rate_completion_modal::*; + +use anyhow::{anyhow, Context as _, Result}; +use client::Client; +use collections::{HashMap, HashSet, VecDeque}; +use futures::AsyncReadExt; +use gpui::{AppContext, Context, Global, Model, ModelContext, Subscription, Task}; +use http_client::{HttpClient, Method}; +use language::{ + language_settings::all_language_settings, Anchor, Buffer, BufferSnapshot, OffsetRangeExt, + Point, ToOffset, ToPoint, +}; +use language_models::LlmApiToken; +use rpc::{PredictEditsParams, PredictEditsResponse}; +use std::{ + borrow::Cow, + cmp, + fmt::Write, + mem, + ops::Range, + path::Path, + sync::Arc, + time::{Duration, Instant}, +}; +use telemetry_events::InlineCompletionRating; +use util::ResultExt; +use uuid::Uuid; + +const CURSOR_MARKER: &'static str = "<|user_cursor_is_here|>"; +const START_OF_FILE_MARKER: &'static str = "<|start_of_file|>"; +const EDITABLE_REGION_START_MARKER: &'static str = "<|editable_region_start|>"; +const EDITABLE_REGION_END_MARKER: &'static str = "<|editable_region_end|>"; +const BUFFER_CHANGE_GROUPING_INTERVAL: Duration = Duration::from_secs(1); + +#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)] +pub struct InlineCompletionId(Uuid); + +impl From for gpui::ElementId { + fn from(value: InlineCompletionId) -> Self { + gpui::ElementId::Uuid(value.0) + } +} + +impl std::fmt::Display for InlineCompletionId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl InlineCompletionId { + fn new() -> Self { + Self(Uuid::new_v4()) + } +} + +#[derive(Clone)] +struct ZetaGlobal(Model); + +impl Global for ZetaGlobal {} + +#[derive(Clone)] +pub struct InlineCompletion { + id: InlineCompletionId, + path: Arc, + excerpt_range: Range, + edits: Arc<[(Range, String)]>, + snapshot: BufferSnapshot, + input_events: Arc, + input_excerpt: Arc, + output_excerpt: Arc, +} + +impl InlineCompletion { + fn interpolate(&self, new_snapshot: BufferSnapshot) -> Option, String)>> { + let mut edits = Vec::new(); + + let mut user_edits = new_snapshot + .edits_since::(&self.snapshot.version) + .peekable(); + for (model_old_range, model_new_text) in self.edits.iter() { + let model_offset_range = model_old_range.to_offset(&self.snapshot); + while let Some(next_user_edit) = user_edits.peek() { + if next_user_edit.old.end < model_offset_range.start { + user_edits.next(); + } else { + break; + } + } + + if let Some(user_edit) = user_edits.peek() { + if user_edit.old.start > model_offset_range.end { + edits.push((model_old_range.clone(), model_new_text.clone())); + } else if user_edit.old == model_offset_range { + let user_new_text = new_snapshot + .text_for_range(user_edit.new.clone()) + .collect::(); + + if let Some(model_suffix) = model_new_text.strip_prefix(&user_new_text) { + if !model_suffix.is_empty() { + edits.push(( + new_snapshot.anchor_after(user_edit.new.end) + ..new_snapshot.anchor_before(user_edit.new.end), + model_suffix.into(), + )); + } + + user_edits.next(); + } else { + return None; + } + } else { + return None; + } + } else { + edits.push((model_old_range.clone(), model_new_text.clone())); + } + } + + Some(edits) + } +} + +impl std::fmt::Debug for InlineCompletion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("InlineCompletion") + .field("id", &self.id) + .field("path", &self.path) + .field("edits", &self.edits) + .finish_non_exhaustive() + } +} + +pub struct Zeta { + client: Arc, + events: VecDeque, + registered_buffers: HashMap, + recent_completions: VecDeque, + rated_completions: HashSet, + llm_token: LlmApiToken, + _llm_token_subscription: Subscription, +} + +impl Zeta { + pub fn global(cx: &mut AppContext) -> Option> { + cx.try_global::().map(|global| global.0.clone()) + } + + pub fn register(client: Arc, cx: &mut AppContext) -> Model { + Self::global(cx).unwrap_or_else(|| { + let model = cx.new_model(|cx| Self::new(client, cx)); + cx.set_global(ZetaGlobal(model.clone())); + model + }) + } + + fn new(client: Arc, cx: &mut ModelContext) -> Self { + let refresh_llm_token_listener = language_models::RefreshLlmTokenListener::global(cx); + + Self { + client, + events: VecDeque::new(), + recent_completions: VecDeque::new(), + rated_completions: HashSet::default(), + registered_buffers: HashMap::default(), + llm_token: LlmApiToken::default(), + _llm_token_subscription: cx.subscribe( + &refresh_llm_token_listener, + |this, _listener, _event, cx| { + let client = this.client.clone(); + let llm_token = this.llm_token.clone(); + cx.spawn(|_this, _cx| async move { + llm_token.refresh(&client).await?; + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + }, + ), + } + } + + fn push_event(&mut self, event: Event) { + if let Some(Event::BufferChange { + new_snapshot: last_new_snapshot, + timestamp: last_timestamp, + .. + }) = self.events.back_mut() + { + // Coalesce edits for the same buffer when they happen one after the other. + let Event::BufferChange { + old_snapshot, + new_snapshot, + timestamp, + } = &event; + + if timestamp.duration_since(*last_timestamp) <= BUFFER_CHANGE_GROUPING_INTERVAL + && old_snapshot.remote_id() == last_new_snapshot.remote_id() + && old_snapshot.version == last_new_snapshot.version + { + *last_new_snapshot = new_snapshot.clone(); + *last_timestamp = *timestamp; + return; + } + } + + self.events.push_back(event); + if self.events.len() > 10 { + self.events.pop_front(); + } + } + + pub fn register_buffer(&mut self, buffer: &Model, cx: &mut ModelContext) { + let buffer_id = buffer.entity_id(); + let weak_buffer = buffer.downgrade(); + + if let std::collections::hash_map::Entry::Vacant(entry) = + self.registered_buffers.entry(buffer_id) + { + let snapshot = buffer.read(cx).snapshot(); + + entry.insert(RegisteredBuffer { + snapshot, + _subscriptions: [ + cx.subscribe(buffer, move |this, buffer, event, cx| { + this.handle_buffer_event(buffer, event, cx); + }), + cx.observe_release(buffer, move |this, _buffer, _cx| { + this.registered_buffers.remove(&weak_buffer.entity_id()); + }), + ], + }); + }; + } + + fn handle_buffer_event( + &mut self, + buffer: Model, + event: &language::BufferEvent, + cx: &mut ModelContext, + ) { + match event { + language::BufferEvent::Edited => { + self.report_changes_for_buffer(&buffer, cx); + } + _ => {} + } + } + + pub fn request_completion( + &mut self, + buffer: &Model, + position: language::Anchor, + cx: &mut ModelContext, + ) -> Task> { + let snapshot = self.report_changes_for_buffer(buffer, cx); + let point = position.to_point(&snapshot); + let offset = point.to_offset(&snapshot); + let excerpt_range = excerpt_range_for_position(point, &snapshot); + let events = self.events.clone(); + let path = snapshot + .file() + .map(|f| f.path().clone()) + .unwrap_or_else(|| Arc::from(Path::new("untitled"))); + + let client = self.client.clone(); + let llm_token = self.llm_token.clone(); + + cx.spawn(|this, mut cx| async move { + let start = std::time::Instant::now(); + + let token = llm_token.acquire(&client).await?; + + let mut input_events = String::new(); + for event in events { + if !input_events.is_empty() { + input_events.push('\n'); + input_events.push('\n'); + } + input_events.push_str(&event.to_prompt()); + } + let input_excerpt = prompt_for_excerpt(&snapshot, &excerpt_range, offset); + + log::debug!("Events:\n{}\nExcerpt:\n{}", input_events, input_excerpt); + + let http_client = client.http_client(); + let body = PredictEditsParams { + input_events: input_events.clone(), + input_excerpt: input_excerpt.clone(), + }; + let request_builder = http_client::Request::builder(); + let request = request_builder + .method(Method::POST) + .uri( + client + .http_client() + .build_zed_llm_url("/predict_edits", &[])? + .as_ref(), + ) + .header("Content-Type", "application/json") + .header("Authorization", format!("Bearer {}", token)) + .body(serde_json::to_string(&body)?.into())?; + let mut response = http_client.send(request).await?; + let mut body = String::new(); + response.body_mut().read_to_string(&mut body).await?; + if !response.status().is_success() { + return Err(anyhow!( + "error predicting edits.\nStatus: {:?}\nBody: {}", + response.status(), + body + )); + } + + let response = serde_json::from_str::(&body)?; + let output_excerpt = response.output_excerpt; + log::debug!("prediction took: {:?}", start.elapsed()); + log::debug!("completion response: {}", output_excerpt); + + let content = output_excerpt.replace(CURSOR_MARKER, ""); + let mut new_text = content.as_str(); + + let codefence_start = new_text + .find(EDITABLE_REGION_START_MARKER) + .context("could not find start marker")?; + new_text = &new_text[codefence_start..]; + + let newline_ix = new_text.find('\n').context("could not find newline")?; + new_text = &new_text[newline_ix + 1..]; + + let codefence_end = new_text + .rfind(&format!("\n{EDITABLE_REGION_END_MARKER}")) + .context("could not find end marker")?; + new_text = &new_text[..codefence_end]; + log::debug!("sanitized completion response: {}", new_text); + + let old_text = snapshot + .text_for_range(excerpt_range.clone()) + .collect::(); + + let diff = similar::TextDiff::from_chars(old_text.as_str(), new_text); + + let mut edits: Vec<(Range, String)> = Vec::new(); + let mut old_start = excerpt_range.start; + for change in diff.iter_all_changes() { + let value = change.value(); + match change.tag() { + similar::ChangeTag::Equal => { + old_start += value.len(); + } + similar::ChangeTag::Delete => { + let old_end = old_start + value.len(); + if let Some((last_old_range, _)) = edits.last_mut() { + if last_old_range.end == old_start { + last_old_range.end = old_end; + } else { + edits.push((old_start..old_end, String::new())); + } + } else { + edits.push((old_start..old_end, String::new())); + } + + old_start = old_end; + } + similar::ChangeTag::Insert => { + if let Some((last_old_range, last_new_text)) = edits.last_mut() { + if last_old_range.end == old_start { + last_new_text.push_str(value); + } else { + edits.push((old_start..old_start, value.into())); + } + } else { + edits.push((old_start..old_start, value.into())); + } + } + } + } + + let edits = edits + .into_iter() + .map(|(mut old_range, new_text)| { + let prefix_len = common_prefix( + snapshot.chars_for_range(old_range.clone()), + new_text.chars(), + ); + old_range.start += prefix_len; + let suffix_len = common_prefix( + snapshot.reversed_chars_for_range(old_range.clone()), + new_text[prefix_len..].chars().rev(), + ); + old_range.end = old_range.end.saturating_sub(suffix_len); + + let new_text = new_text[prefix_len..new_text.len() - suffix_len].to_string(); + ( + snapshot.anchor_after(old_range.start) + ..snapshot.anchor_before(old_range.end), + new_text, + ) + }) + .collect(); + let inline_completion = InlineCompletion { + id: InlineCompletionId::new(), + path, + excerpt_range, + edits, + snapshot, + input_events: input_events.into(), + input_excerpt: input_excerpt.into(), + output_excerpt: output_excerpt.into(), + }; + this.update(&mut cx, |this, cx| { + this.recent_completions + .push_front(inline_completion.clone()); + if this.recent_completions.len() > 50 { + this.recent_completions.pop_back(); + } + cx.notify(); + })?; + + Ok(inline_completion) + }) + } + + pub fn is_completion_rated(&self, completion_id: InlineCompletionId) -> bool { + self.rated_completions.contains(&completion_id) + } + + pub fn rate_completion( + &mut self, + completion: &InlineCompletion, + rating: InlineCompletionRating, + feedback: String, + cx: &mut ModelContext, + ) { + self.rated_completions.insert(completion.id); + self.client + .telemetry() + .report_inline_completion_rating_event( + rating, + completion.input_events.clone(), + completion.input_excerpt.clone(), + completion.output_excerpt.clone(), + feedback, + ); + self.client.telemetry().flush_events(); + cx.notify(); + } + + pub fn recent_completions(&self) -> impl Iterator { + self.recent_completions.iter() + } + + fn report_changes_for_buffer( + &mut self, + buffer: &Model, + cx: &mut ModelContext, + ) -> BufferSnapshot { + self.register_buffer(buffer, cx); + + let registered_buffer = self + .registered_buffers + .get_mut(&buffer.entity_id()) + .unwrap(); + let new_snapshot = buffer.read(cx).snapshot(); + + if new_snapshot.version != registered_buffer.snapshot.version { + let old_snapshot = mem::replace(&mut registered_buffer.snapshot, new_snapshot.clone()); + self.push_event(Event::BufferChange { + old_snapshot, + new_snapshot: new_snapshot.clone(), + timestamp: Instant::now(), + }); + } + + new_snapshot + } +} + +fn common_prefix, T2: Iterator>(a: T1, b: T2) -> usize { + a.zip(b) + .take_while(|(a, b)| a == b) + .map(|(a, _)| a.len_utf8()) + .sum() +} + +fn prompt_for_excerpt( + snapshot: &BufferSnapshot, + excerpt_range: &Range, + offset: usize, +) -> String { + let mut prompt_excerpt = String::new(); + writeln!( + prompt_excerpt, + "```{}", + snapshot + .file() + .map_or(Cow::Borrowed("untitled"), |file| file + .path() + .to_string_lossy()) + ) + .unwrap(); + + if excerpt_range.start == 0 { + writeln!(prompt_excerpt, "{START_OF_FILE_MARKER}").unwrap(); + } + + let point_range = excerpt_range.to_point(snapshot); + if point_range.start.row > 0 && !snapshot.is_line_blank(point_range.start.row - 1) { + let extra_context_line_range = Point::new(point_range.start.row - 1, 0)..point_range.start; + for chunk in snapshot.text_for_range(extra_context_line_range) { + prompt_excerpt.push_str(chunk); + } + } + writeln!(prompt_excerpt, "{EDITABLE_REGION_START_MARKER}").unwrap(); + for chunk in snapshot.text_for_range(excerpt_range.start..offset) { + prompt_excerpt.push_str(chunk); + } + prompt_excerpt.push_str(CURSOR_MARKER); + for chunk in snapshot.text_for_range(offset..excerpt_range.end) { + prompt_excerpt.push_str(chunk); + } + write!(prompt_excerpt, "\n{EDITABLE_REGION_END_MARKER}").unwrap(); + + if point_range.end.row < snapshot.max_point().row + && !snapshot.is_line_blank(point_range.end.row + 1) + { + let extra_context_line_range = point_range.end + ..Point::new( + point_range.end.row + 1, + snapshot.line_len(point_range.end.row + 1), + ); + for chunk in snapshot.text_for_range(extra_context_line_range) { + prompt_excerpt.push_str(chunk); + } + } + + write!(prompt_excerpt, "\n```").unwrap(); + prompt_excerpt +} + +fn excerpt_range_for_position(point: Point, snapshot: &BufferSnapshot) -> Range { + const CONTEXT_LINES: u32 = 16; + + let mut context_lines_before = CONTEXT_LINES; + let mut context_lines_after = CONTEXT_LINES; + if point.row < CONTEXT_LINES { + context_lines_after += CONTEXT_LINES - point.row; + } else if point.row + CONTEXT_LINES > snapshot.max_point().row { + context_lines_before += (point.row + CONTEXT_LINES) - snapshot.max_point().row; + } + + let excerpt_start_row = point.row.saturating_sub(context_lines_before); + let excerpt_start = Point::new(excerpt_start_row, 0); + let excerpt_end_row = cmp::min(point.row + context_lines_after, snapshot.max_point().row); + let excerpt_end = Point::new(excerpt_end_row, snapshot.line_len(excerpt_end_row)); + excerpt_start.to_offset(snapshot)..excerpt_end.to_offset(snapshot) +} + +struct RegisteredBuffer { + snapshot: BufferSnapshot, + _subscriptions: [gpui::Subscription; 2], +} + +#[derive(Clone)] +enum Event { + BufferChange { + old_snapshot: BufferSnapshot, + new_snapshot: BufferSnapshot, + timestamp: Instant, + }, +} + +impl Event { + fn to_prompt(&self) -> String { + match self { + Event::BufferChange { + old_snapshot, + new_snapshot, + .. + } => { + let mut prompt = String::new(); + + let old_path = old_snapshot + .file() + .map(|f| f.path().as_ref()) + .unwrap_or(Path::new("untitled")); + let new_path = new_snapshot + .file() + .map(|f| f.path().as_ref()) + .unwrap_or(Path::new("untitled")); + if old_path != new_path { + writeln!(prompt, "User renamed {:?} to {:?}\n", old_path, new_path).unwrap(); + } + + let diff = + similar::TextDiff::from_lines(&old_snapshot.text(), &new_snapshot.text()) + .unified_diff() + .to_string(); + if !diff.is_empty() { + write!( + prompt, + "User edited {:?}:\n```diff\n{}\n```", + new_path, diff + ) + .unwrap(); + } + + prompt + } + } + } +} + +pub struct ZetaInlineCompletionProvider { + zeta: Model, + current_completion: Option, + pending_refresh: Task<()>, +} + +impl ZetaInlineCompletionProvider { + pub const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75); + + pub fn new(zeta: Model) -> Self { + Self { + zeta, + current_completion: None, + pending_refresh: Task::ready(()), + } + } +} + +impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvider { + fn name() -> &'static str { + "Zeta" + } + + fn is_enabled( + &self, + buffer: &Model, + cursor_position: language::Anchor, + cx: &AppContext, + ) -> bool { + let buffer = buffer.read(cx); + let file = buffer.file(); + let language = buffer.language_at(cursor_position); + let settings = all_language_settings(file, cx); + settings.inline_completions_enabled(language.as_ref(), file.map(|f| f.path().as_ref()), cx) + } + + fn refresh( + &mut self, + buffer: Model, + position: language::Anchor, + debounce: bool, + cx: &mut ModelContext, + ) { + self.pending_refresh = cx.spawn(|this, mut cx| async move { + if debounce { + cx.background_executor().timer(Self::DEBOUNCE_TIMEOUT).await; + } + + let completion_request = this.update(&mut cx, |this, cx| { + this.zeta.update(cx, |zeta, cx| { + zeta.request_completion(&buffer, position, cx) + }) + }); + + let mut completion = None; + if let Ok(completion_request) = completion_request { + completion = completion_request.await.log_err(); + } + + this.update(&mut cx, |this, cx| { + this.current_completion = completion; + cx.notify(); + }) + .ok(); + }); + } + + fn cycle( + &mut self, + _buffer: Model, + _cursor_position: language::Anchor, + _direction: inline_completion::Direction, + _cx: &mut ModelContext, + ) { + // Right now we don't support cycling. + } + + fn accept(&mut self, _cx: &mut ModelContext) {} + + fn discard(&mut self, _cx: &mut ModelContext) { + self.current_completion.take(); + } + + fn suggest( + &mut self, + buffer: &Model, + cursor_position: language::Anchor, + cx: &mut ModelContext, + ) -> Option { + let completion = self.current_completion.as_mut()?; + + let buffer = buffer.read(cx); + let Some(edits) = completion.interpolate(buffer.snapshot()) else { + self.current_completion.take(); + return None; + }; + + let cursor_row = cursor_position.to_point(buffer).row; + let (closest_edit_ix, (closest_edit_range, _)) = + edits.iter().enumerate().min_by_key(|(_, (range, _))| { + let distance_from_start = cursor_row.abs_diff(range.start.to_point(buffer).row); + let distance_from_end = cursor_row.abs_diff(range.end.to_point(buffer).row); + cmp::min(distance_from_start, distance_from_end) + })?; + + let mut edit_start_ix = closest_edit_ix; + for (range, _) in edits[..edit_start_ix].iter().rev() { + let distance_from_closest_edit = + closest_edit_range.start.to_point(buffer).row - range.end.to_point(buffer).row; + if distance_from_closest_edit <= 1 { + edit_start_ix -= 1; + } else { + break; + } + } + + let mut edit_end_ix = closest_edit_ix + 1; + for (range, _) in &edits[edit_end_ix..] { + let distance_from_closest_edit = + range.start.to_point(buffer).row - closest_edit_range.end.to_point(buffer).row; + if distance_from_closest_edit <= 1 { + edit_end_ix += 1; + } else { + break; + } + } + + Some(inline_completion::InlineCompletion { + edits: edits[edit_start_ix..edit_end_ix].to_vec(), + }) + } +} + +#[cfg(test)] +mod tests { + use client::test::FakeServer; + use clock::FakeSystemClock; + use gpui::TestAppContext; + use http_client::FakeHttpClient; + use indoc::indoc; + use language_models::RefreshLlmTokenListener; + use rpc::proto; + use settings::SettingsStore; + + use super::*; + + #[gpui::test] + fn test_inline_completion_basic_interpolation(cx: &mut AppContext) { + let buffer = cx.new_model(|cx| Buffer::local("Lorem ipsum dolor", cx)); + let completion = InlineCompletion { + edits: to_completion_edits( + [(2..5, "REM".to_string()), (9..11, "".to_string())], + &buffer, + cx, + ) + .into(), + path: Path::new("").into(), + snapshot: buffer.read(cx).snapshot(), + id: InlineCompletionId::new(), + excerpt_range: 0..0, + input_events: "".into(), + input_excerpt: "".into(), + output_excerpt: "".into(), + }; + + assert_eq!( + from_completion_edits( + &completion.interpolate(buffer.read(cx).snapshot()).unwrap(), + &buffer, + cx + ), + vec![(2..5, "REM".to_string()), (9..11, "".to_string())] + ); + + buffer.update(cx, |buffer, cx| buffer.edit([(2..5, "")], None, cx)); + assert_eq!( + from_completion_edits( + &completion.interpolate(buffer.read(cx).snapshot()).unwrap(), + &buffer, + cx + ), + vec![(2..2, "REM".to_string()), (6..8, "".to_string())] + ); + + buffer.update(cx, |buffer, cx| buffer.undo(cx)); + assert_eq!( + from_completion_edits( + &completion.interpolate(buffer.read(cx).snapshot()).unwrap(), + &buffer, + cx + ), + vec![(2..5, "REM".to_string()), (9..11, "".to_string())] + ); + + buffer.update(cx, |buffer, cx| buffer.edit([(2..5, "R")], None, cx)); + assert_eq!( + from_completion_edits( + &completion.interpolate(buffer.read(cx).snapshot()).unwrap(), + &buffer, + cx + ), + vec![(3..3, "EM".to_string()), (7..9, "".to_string())] + ); + + buffer.update(cx, |buffer, cx| buffer.edit([(3..3, "E")], None, cx)); + assert_eq!( + from_completion_edits( + &completion.interpolate(buffer.read(cx).snapshot()).unwrap(), + &buffer, + cx + ), + vec![(4..4, "M".to_string()), (8..10, "".to_string())] + ); + + buffer.update(cx, |buffer, cx| buffer.edit([(4..4, "M")], None, cx)); + assert_eq!( + from_completion_edits( + &completion.interpolate(buffer.read(cx).snapshot()).unwrap(), + &buffer, + cx + ), + vec![(9..11, "".to_string())] + ); + + buffer.update(cx, |buffer, cx| buffer.edit([(4..5, "")], None, cx)); + assert_eq!( + from_completion_edits( + &completion.interpolate(buffer.read(cx).snapshot()).unwrap(), + &buffer, + cx + ), + vec![(4..4, "M".to_string()), (8..10, "".to_string())] + ); + + buffer.update(cx, |buffer, cx| buffer.edit([(8..10, "")], None, cx)); + assert_eq!( + from_completion_edits( + &completion.interpolate(buffer.read(cx).snapshot()).unwrap(), + &buffer, + cx + ), + vec![(4..4, "M".to_string())] + ); + + buffer.update(cx, |buffer, cx| buffer.edit([(4..6, "")], None, cx)); + assert_eq!(completion.interpolate(buffer.read(cx).snapshot()), None); + } + + #[gpui::test] + async fn test_inline_completion_end_of_buffer(cx: &mut TestAppContext) { + cx.update(|cx| { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + client::init_settings(cx); + }); + + let buffer_content = "lorem\n"; + let completion_response = indoc! {" + ```animals.js + <|start_of_file|> + <|editable_region_start|> + lorem + ipsum + <|editable_region_end|> + ```"}; + + let http_client = FakeHttpClient::create(move |_| async move { + Ok(http_client::Response::builder() + .status(200) + .body( + serde_json::to_string(&PredictEditsResponse { + output_excerpt: completion_response.to_string(), + }) + .unwrap() + .into(), + ) + .unwrap()) + }); + + let client = cx.update(|cx| Client::new(Arc::new(FakeSystemClock::new()), http_client, cx)); + cx.update(|cx| { + RefreshLlmTokenListener::register(client.clone(), cx); + }); + let server = FakeServer::for_client(42, &client, cx).await; + + let zeta = cx.new_model(|cx| Zeta::new(client, cx)); + let buffer = cx.new_model(|cx| Buffer::local(buffer_content, cx)); + let cursor = buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(1, 0))); + let completion_task = + zeta.update(cx, |zeta, cx| zeta.request_completion(&buffer, cursor, cx)); + + let token_request = server.receive::().await.unwrap(); + server.respond( + token_request.receipt(), + proto::GetLlmTokenResponse { token: "".into() }, + ); + + let completion = completion_task.await.unwrap(); + buffer.update(cx, |buffer, cx| { + buffer.edit(completion.edits.iter().cloned(), None, cx) + }); + assert_eq!( + buffer.read_with(cx, |buffer, _| buffer.text()), + "lorem\nipsum" + ); + } + + fn to_completion_edits( + iterator: impl IntoIterator, String)>, + buffer: &Model, + cx: &AppContext, + ) -> Vec<(Range, String)> { + let buffer = buffer.read(cx); + iterator + .into_iter() + .map(|(range, text)| { + ( + buffer.anchor_after(range.start)..buffer.anchor_before(range.end), + text, + ) + }) + .collect() + } + + fn from_completion_edits( + editor_edits: &[(Range, String)], + buffer: &Model, + cx: &AppContext, + ) -> Vec<(Range, String)> { + let buffer = buffer.read(cx); + editor_edits + .iter() + .map(|(range, text)| { + ( + range.start.to_offset(buffer)..range.end.to_offset(buffer), + text.clone(), + ) + }) + .collect() + } + + #[ctor::ctor] + fn init_logger() { + if std::env::var("RUST_LOG").is_ok() { + env_logger::init(); + } + } +} From 8fcaf8b8706b6656226f35003eb3a244736b647d Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Mon, 9 Dec 2024 15:14:46 +0100 Subject: [PATCH 356/886] collab: Fix compilation error by removing dependency on livekit_client (#21744) This fixes collab not being able to compile anymore for Linux: https://github.com/zed-industries/zed/actions/runs/12236650046/job/34130962682 Release Notes: - N/A Co-authored-by: Antonio --- crates/collab/Cargo.toml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 9c7f09bcf5..d08bcfa18d 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -77,12 +77,6 @@ tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json", "re util.workspace = true uuid.workspace = true -[target.'cfg(target_os = "macos")'.dependencies] -livekit_client_macos = { workspace = true, features = ["test-support"] } - -[target.'cfg(not(target_os = "macos"))'.dependencies] -livekit_client = { workspace = true, features = ["test-support"] } - [dev-dependencies] assistant = { workspace = true, features = ["test-support"] } assistant_tool.workspace = true From 8c91eecb6720ce5dba5c10df556bd67ba6b477c3 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 9 Dec 2024 11:21:02 -0500 Subject: [PATCH 357/886] call: Add `test-support` feature for `livekit_client_macos` (#21748) This PR updates the `call` crate to include the `test-support` feature for `livekit_client_macos` when `call` is used with `test-support`. This fixes running `cargo test -p copilot` and `cargo test -p editor` (and perhaps some other crates). Release Notes: - N/A --- crates/call/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/call/Cargo.toml b/crates/call/Cargo.toml index 9ba10e56ba..42f007dbce 100644 --- a/crates/call/Cargo.toml +++ b/crates/call/Cargo.toml @@ -18,6 +18,7 @@ test-support = [ "collections/test-support", "gpui/test-support", "livekit_client/test-support", + "livekit_client_macos/test-support", "project/test-support", "util/test-support" ] From e5f3a683f0e476ca0a90e0e0688cab135e70de92 Mon Sep 17 00:00:00 2001 From: Travis Stevens Date: Mon, 9 Dec 2024 11:49:40 -0500 Subject: [PATCH 358/886] Fixing Missing comma (#21749) Fix a missing comma in the docs Release Notes: - N/A --- docs/src/configuring-zed.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index b51b01a1e7..433b705e4d 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -994,7 +994,7 @@ The result is still `)))` and not `))))))`, which is what it would be by default "**/.git", "**/.svn", "**/.hg", - "**/.jj" + "**/.jj", "**/CVS", "**/.DS_Store", "**/Thumbs.db", From 16ecbafa7a28d4d788de4c1f432ac0d2baf4ed3f Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Mon, 9 Dec 2024 10:18:18 -0700 Subject: [PATCH 359/886] Skip spawning task for `background_executor.timer(Duration::ZERO)` (#21729) Release Notes: - N/A --- crates/gpui/src/executor.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/gpui/src/executor.rs b/crates/gpui/src/executor.rs index 3035892d7a..34132d72f8 100644 --- a/crates/gpui/src/executor.rs +++ b/crates/gpui/src/executor.rs @@ -328,6 +328,9 @@ impl BackgroundExecutor { /// Depending on other concurrent tasks the elapsed duration may be longer /// than requested. pub fn timer(&self, duration: Duration) -> Task<()> { + if duration.is_zero() { + return Task::ready(()); + } let (runnable, task) = async_task::spawn(async move {}, { let dispatcher = self.dispatcher.clone(); move |runnable| dispatcher.dispatch_after(duration, runnable) From 2af9fa7785a0db2aefd24f816bb20d05e9ea9dda Mon Sep 17 00:00:00 2001 From: Alexandre Hamez Date: Mon, 9 Dec 2024 18:22:19 +0100 Subject: [PATCH 360/886] docs: Add missing ':' (#21751) Release Notes: - N/A --- docs/src/languages/cpp.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/languages/cpp.md b/docs/src/languages/cpp.md index b14f16473d..cb2c2c7da3 100644 --- a/docs/src/languages/cpp.md +++ b/docs/src/languages/cpp.md @@ -77,7 +77,7 @@ You can trigger formatting via {#kb editor::Format} or the `editor: format` acti ```json "languages": { - "C++" { + "C++": { "format_on_save": "on", "tab_size": 2 } From 7bd69130f872393db0bb9d5d5887d50522738cf9 Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Mon, 9 Dec 2024 10:47:14 -0700 Subject: [PATCH 361/886] Make space for documentation aside during followup completion select (#21716) The goal of #7115 appears to be to limit the disruptiveness of completion documentation load causing the completion selector to move around. The approach was to debounce load of documentation via a setting `completion_documentation_secondary_query_debounce`. This particularly had a nonideal interaction with #21286, where now this debounce interval was used between the documentation fetches of every individual completion item. I think a better solution is to continue making space for documentation to be shown as soon as any documentation is shown. #21704 implemented part of this, but it did not persist across followup completions. Release Notes: - Fixed completion list moving around on load of documentation. The previous approach to mitigating this was to rate-limit the fetch of docs, configured by a `completion_documentation_secondary_query_debounce` setting, which is now deprecated. --- assets/settings/default.json | 3 -- crates/editor/src/debounced_delay.rs | 46 ----------------------- crates/editor/src/editor.rs | 56 +++++++++++----------------- crates/editor/src/editor_settings.rs | 6 --- docs/src/configuring-zed.md | 10 ----- 5 files changed, 21 insertions(+), 100 deletions(-) delete mode 100644 crates/editor/src/debounced_delay.rs diff --git a/assets/settings/default.json b/assets/settings/default.json index 3b78580610..628b51df2c 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -150,9 +150,6 @@ // Whether to display inline and alongside documentation for items in the // completions menu "show_completion_documentation": true, - // The debounce delay before re-querying the language server for completion - // documentation when not included in original completion list. - "completion_documentation_secondary_query_debounce": 300, // Show method signatures in the editor, when inside parentheses. "auto_signature_help": false, /// Whether to show the signature help after completion or a bracket pair inserted. diff --git a/crates/editor/src/debounced_delay.rs b/crates/editor/src/debounced_delay.rs deleted file mode 100644 index ad4b55b209..0000000000 --- a/crates/editor/src/debounced_delay.rs +++ /dev/null @@ -1,46 +0,0 @@ -use std::time::Duration; - -use futures::{channel::oneshot, FutureExt}; -use gpui::{Task, ViewContext}; - -use crate::Editor; - -#[derive(Debug)] -pub struct DebouncedDelay { - task: Option>, - cancel_channel: Option>, -} - -impl DebouncedDelay { - pub fn new() -> DebouncedDelay { - DebouncedDelay { - task: None, - cancel_channel: None, - } - } - - pub fn fire_new(&mut self, delay: Duration, cx: &mut ViewContext, func: F) - where - F: 'static + Send + FnOnce(&mut Editor, &mut ViewContext) -> Task<()>, - { - if let Some(channel) = self.cancel_channel.take() { - _ = channel.send(()); - } - - let (sender, mut receiver) = oneshot::channel::<()>(); - self.cancel_channel = Some(sender); - - drop(self.task.take()); - self.task = Some(cx.spawn(move |model, mut cx| async move { - let mut timer = cx.background_executor().timer(delay).fuse(); - futures::select_biased! { - _ = receiver => return, - _ = timer => {} - } - - if let Ok(task) = model.update(&mut cx, |project, cx| (func)(project, cx)) { - task.await; - } - })); - } -} diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 98e91f9751..ed0bd3f798 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -16,7 +16,6 @@ pub mod actions; mod blame_entry_tooltip; mod blink_manager; mod clangd_ext; -mod debounced_delay; pub mod display_map; mod editor_settings; mod editor_settings_controls; @@ -58,7 +57,6 @@ use client::{Collaborator, ParticipantIndex}; use clock::ReplicaId; use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque}; use convert_case::{Case, Casing}; -use debounced_delay::DebouncedDelay; use display_map::*; pub use display_map::{DisplayPoint, FoldPlaceholder}; pub use editor_settings::{ @@ -123,7 +121,7 @@ use multi_buffer::{ ExpandExcerptDirection, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow, ToOffsetUtf16, }; use ordered_float::OrderedFloat; -use parking_lot::{Mutex, RwLock}; +use parking_lot::RwLock; use project::{ lsp_store::{FormatTarget, FormatTrigger}, project_settings::{GitGutterSetting, ProjectSettings}, @@ -1006,7 +1004,7 @@ struct CompletionsMenu { matches: Arc<[StringMatch]>, selected_item: usize, scroll_handle: UniformListScrollHandle, - selected_completion_resolve_debounce: Option>>, + resolve_completions: bool, aside_was_displayed: Cell, } @@ -1017,6 +1015,7 @@ impl CompletionsMenu { initial_position: Anchor, buffer: Model, completions: Box<[Completion]>, + aside_was_displayed: bool, ) -> Self { let match_candidates = completions .iter() @@ -1039,8 +1038,8 @@ impl CompletionsMenu { matches: Vec::new().into(), selected_item: 0, scroll_handle: UniformListScrollHandle::new(), - selected_completion_resolve_debounce: Some(Arc::new(Mutex::new(DebouncedDelay::new()))), - aside_was_displayed: Cell::new(false), + resolve_completions: true, + aside_was_displayed: Cell::new(aside_was_displayed), } } @@ -1093,16 +1092,11 @@ impl CompletionsMenu { matches, selected_item: 0, scroll_handle: UniformListScrollHandle::new(), - selected_completion_resolve_debounce: Some(Arc::new(Mutex::new(DebouncedDelay::new()))), + resolve_completions: false, aside_was_displayed: Cell::new(false), } } - fn suppress_documentation_resolution(mut self) -> Self { - self.selected_completion_resolve_debounce.take(); - self - } - fn select_first( &mut self, provider: Option<&dyn CompletionProvider>, @@ -1164,14 +1158,14 @@ impl CompletionsMenu { provider: Option<&dyn CompletionProvider>, cx: &mut ViewContext, ) { - let completion_index = self.matches[self.selected_item].candidate_id; + if !self.resolve_completions { + return; + } let Some(provider) = provider else { return; }; - let Some(completion_resolve) = self.selected_completion_resolve_debounce.as_ref() else { - return; - }; + let completion_index = self.matches[self.selected_item].candidate_id; let resolve_task = provider.resolve_completions( self.buffer.clone(), vec![completion_index], @@ -1179,17 +1173,12 @@ impl CompletionsMenu { cx, ); - let delay_ms = - EditorSettings::get_global(cx).completion_documentation_secondary_query_debounce; - let delay = Duration::from_millis(delay_ms); - - completion_resolve.lock().fire_new(delay, cx, |_, cx| { - cx.spawn(move |editor, mut cx| async move { - if let Some(true) = resolve_task.await.log_err() { - editor.update(&mut cx, |_, cx| cx.notify()).ok(); - } - }) - }); + cx.spawn(move |editor, mut cx| async move { + if let Some(true) = resolve_task.await.log_err() { + editor.update(&mut cx, |_, cx| cx.notify()).ok(); + } + }) + .detach(); } fn visible(&self) -> bool { @@ -4472,12 +4461,9 @@ impl Editor { }; let query = Self::completion_query(&self.buffer.read(cx).read(cx), position); - let is_followup_invoke = { - let context_menu_state = self.context_menu.read(); - matches!( - context_menu_state.deref(), - Some(ContextMenu::Completions(_)) - ) + let (is_followup_invoke, aside_was_displayed) = match self.context_menu.read().deref() { + Some(ContextMenu::Completions(menu)) => (true, menu.aside_was_displayed.get()), + _ => (false, false), }; let trigger_kind = match (&options.trigger, is_followup_invoke) { (_, true) => CompletionTriggerKind::TRIGGER_FOR_INCOMPLETE_COMPLETIONS, @@ -4514,6 +4500,7 @@ impl Editor { position, buffer.clone(), completions.into(), + aside_was_displayed, ); menu.filter(query.as_deref(), cx.background_executor().clone()) .await; @@ -5858,8 +5845,7 @@ impl Editor { if let Some(buffer) = buffer { *self.context_menu.write() = Some(ContextMenu::Completions( - CompletionsMenu::new_snippet_choices(id, true, choices, selection, buffer) - .suppress_documentation_resolution(), + CompletionsMenu::new_snippet_choices(id, true, choices, selection, buffer), )); } } diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index e669c21554..436ab970e2 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -12,7 +12,6 @@ pub struct EditorSettings { pub hover_popover_enabled: bool, pub show_completions_on_input: bool, pub show_completion_documentation: bool, - pub completion_documentation_secondary_query_debounce: u64, pub toolbar: Toolbar, pub scrollbar: Scrollbar, pub gutter: Gutter, @@ -204,11 +203,6 @@ pub struct EditorSettingsContent { /// /// Default: true pub show_completion_documentation: Option, - /// The debounce delay before re-querying the language server for completion - /// documentation when not included in original completion list. - /// - /// Default: 300 ms - pub completion_documentation_secondary_query_debounce: Option, /// Toolbar related settings pub toolbar: Option, /// Scrollbar related settings diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index 433b705e4d..57df291338 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -1517,16 +1517,6 @@ Or to set a `socks5` proxy: `boolean` values -## Completion Documentation Debounce Delay - -- Description: The debounce delay before re-querying the language server for completion documentation when not included in original completion list. -- Setting: `completion_documentation_secondary_query_debounce` -- Default: `300` ms - -**Options** - -`integer` values - ## Show Inline Completions - Description: Whether to show inline completions as you type or manually by triggering `editor::ShowInlineCompletion`. From b7edf3117089769566c9a7ecbf55b8897c9f017d Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 9 Dec 2024 19:10:34 +0100 Subject: [PATCH 362/886] lsp: Disable usage of follow-up completion invokes (#21755) Some of our users ran into a peculiar bug: autoimports with vtsls were leaving behind an extra curly brace. I think we were slightly incorrect in always requesting a follow-up completion without regard for last result of completion request (whether it was incomplete or not). Specifically, we're falling into this branch in current form: https://github.com/yioneko/vtsls/blob/037c2b615bf4cfe9dd65d9affc7a155fbb2ca255/packages/service/src/service/completion.ts#L121 which then leads to incorrect edits being returned from vtsls. Release Notes: - Fixed an edge case with appliance of autocompletions in VTSLS that could result in incorrect edits being applied. --- crates/editor/src/editor.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ed0bd3f798..51f4cf7c0e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -4461,16 +4461,15 @@ impl Editor { }; let query = Self::completion_query(&self.buffer.read(cx).read(cx), position); - let (is_followup_invoke, aside_was_displayed) = match self.context_menu.read().deref() { - Some(ContextMenu::Completions(menu)) => (true, menu.aside_was_displayed.get()), - _ => (false, false), + + let aside_was_displayed = match self.context_menu.read().deref() { + Some(ContextMenu::Completions(menu)) => menu.aside_was_displayed.get(), + _ => false, }; - let trigger_kind = match (&options.trigger, is_followup_invoke) { - (_, true) => CompletionTriggerKind::TRIGGER_FOR_INCOMPLETE_COMPLETIONS, - (Some(trigger), _) if buffer.read(cx).completion_triggers().contains(trigger) => { + let trigger_kind = match &options.trigger { + Some(trigger) if buffer.read(cx).completion_triggers().contains(trigger) => { CompletionTriggerKind::TRIGGER_CHARACTER } - _ => CompletionTriggerKind::INVOKED, }; let completion_context = CompletionContext { From a5355e92e30750bdd7afc56bf338765f55bd6ab8 Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Mon, 9 Dec 2024 11:53:50 -0700 Subject: [PATCH 363/886] Add per-language settings `show_completions_on_input` and `show_completion_documentation` (#21722) Release Notes: - Added `show_completions_on_input` and `show_completion_documentation` per-language settings. These settings were available before, but were not configurable per-language. --- crates/editor/src/editor.rs | 27 ++++++++++++++---------- crates/editor/src/editor_settings.rs | 12 ----------- crates/editor/src/editor_tests.rs | 8 ++----- crates/language/src/language_settings.rs | 24 +++++++++++++++++++++ docs/src/configuring-languages.md | 2 ++ 5 files changed, 44 insertions(+), 29 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 51f4cf7c0e..830d561398 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1006,12 +1006,14 @@ struct CompletionsMenu { scroll_handle: UniformListScrollHandle, resolve_completions: bool, aside_was_displayed: Cell, + show_completion_documentation: bool, } impl CompletionsMenu { fn new( id: CompletionId, sort_completions: bool, + show_completion_documentation: bool, initial_position: Anchor, buffer: Model, completions: Box<[Completion]>, @@ -1040,6 +1042,7 @@ impl CompletionsMenu { scroll_handle: UniformListScrollHandle::new(), resolve_completions: true, aside_was_displayed: Cell::new(aside_was_displayed), + show_completion_documentation: show_completion_documentation, } } @@ -1094,6 +1097,7 @@ impl CompletionsMenu { scroll_handle: UniformListScrollHandle::new(), resolve_completions: false, aside_was_displayed: Cell::new(false), + show_completion_documentation: false, } } @@ -1192,9 +1196,7 @@ impl CompletionsMenu { workspace: Option>, cx: &mut ViewContext, ) -> AnyElement { - let settings = EditorSettings::get_global(cx); - let show_completion_documentation = settings.show_completion_documentation; - + let show_completion_documentation = self.show_completion_documentation; let widest_completion_ix = self .matches .iter() @@ -4459,6 +4461,11 @@ impl Editor { } else { return; }; + let show_completion_documentation = buffer + .read(cx) + .snapshot() + .settings_at(buffer_position, cx) + .show_completion_documentation; let query = Self::completion_query(&self.buffer.read(cx).read(cx), position); @@ -4496,6 +4503,7 @@ impl Editor { let mut menu = CompletionsMenu::new( id, sort_completions, + show_completion_documentation, position, buffer.clone(), completions.into(), @@ -14174,10 +14182,6 @@ impl CompletionProvider for Model { trigger_in_words: bool, cx: &mut ViewContext, ) -> bool { - if !EditorSettings::get_global(cx).show_completions_on_input { - return false; - } - let mut chars = text.chars(); let char = if let Some(char) = chars.next() { char @@ -14189,10 +14193,11 @@ impl CompletionProvider for Model { } let buffer = buffer.read(cx); - let classifier = buffer - .snapshot() - .char_classifier_at(position) - .for_completion(true); + let snapshot = buffer.snapshot(); + if !snapshot.settings_at(position, cx).show_completions_on_input { + return false; + } + let classifier = snapshot.char_classifier_at(position).for_completion(true); if trigger_in_words && classifier.is_word(char) { return true; } diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index 436ab970e2..b131fa5c21 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -10,8 +10,6 @@ pub struct EditorSettings { pub cursor_shape: Option, pub current_line_highlight: CurrentLineHighlight, pub hover_popover_enabled: bool, - pub show_completions_on_input: bool, - pub show_completion_documentation: bool, pub toolbar: Toolbar, pub scrollbar: Scrollbar, pub gutter: Gutter, @@ -193,16 +191,6 @@ pub struct EditorSettingsContent { /// Default: true pub hover_popover_enabled: Option, - /// Whether to pop the completions menu while typing in an editor without - /// explicitly requesting it. - /// - /// Default: true - pub show_completions_on_input: Option, - /// Whether to display inline and alongside documentation for items in the - /// completions menu. - /// - /// Default: true - pub show_completion_documentation: Option, /// Toolbar related settings pub toolbar: Option, /// Scrollbar related settings diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 7561c31f13..c63840c839 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -8376,12 +8376,8 @@ async fn test_completion(cx: &mut gpui::TestAppContext) { handle_resolve_completion_request(&mut cx, None).await; apply_additional_edits.await.unwrap(); - cx.update(|cx| { - cx.update_global::(|settings, cx| { - settings.update_user_settings::(cx, |settings| { - settings.show_completions_on_input = Some(false); - }); - }) + update_test_language_settings(&mut cx, |settings| { + settings.defaults.show_completions_on_input = Some(false); }); cx.set_state("editorˇ"); cx.simulate_keystroke("."); diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 5f3227cea8..cee765f9f9 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -138,6 +138,12 @@ pub struct LanguageSettings { pub linked_edits: bool, /// Task configuration for this language. pub tasks: LanguageTaskConfig, + /// Whether to pop the completions menu while typing in an editor without + /// explicitly requesting it. + pub show_completions_on_input: bool, + /// Whether to display inline and alongside documentation for items in the + /// completions menu. + pub show_completion_documentation: bool, } impl LanguageSettings { @@ -382,6 +388,16 @@ pub struct LanguageSettingsContent { /// /// Default: {} pub tasks: Option, + /// Whether to pop the completions menu while typing in an editor without + /// explicitly requesting it. + /// + /// Default: true + pub show_completions_on_input: Option, + /// Whether to display inline and alongside documentation for items in the + /// completions menu. + /// + /// Default: true + pub show_completion_documentation: Option, } /// The contents of the inline completion settings. @@ -1186,6 +1202,14 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent src.extend_comment_on_newline, ); merge(&mut settings.inlay_hints, src.inlay_hints); + merge( + &mut settings.show_completions_on_input, + src.show_completions_on_input, + ); + merge( + &mut settings.show_completion_documentation, + src.show_completion_documentation, + ); } /// Allows to enable/disable formatting with Prettier diff --git a/docs/src/configuring-languages.md b/docs/src/configuring-languages.md index 3b9e72a08b..dce2fc5552 100644 --- a/docs/src/configuring-languages.md +++ b/docs/src/configuring-languages.md @@ -56,6 +56,8 @@ You can customize a wide range of settings for each language, including: - [`hard_tabs`](./configuring-zed.md#hard-tabs): Use tabs instead of spaces for indentation - [`preferred_line_length`](./configuring-zed.md#preferred-line-length): The recommended maximum line length - [`soft_wrap`](./configuring-zed.md#soft-wrap): How to wrap long lines of code +- [`show_completions_on_input`](./configuring-zed.md#show-completions-on-input): Whether or not to show completions as you type +- [`show_completion_documentation`](./configuring-zed.md#show-completion-documentation): Whether to display inline and alongside documentation for items in the completions menu These settings allow you to maintain specific coding styles across different languages and projects. From 25a5ad54ae536341e42c0e5065079f57451b5925 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 9 Dec 2024 21:43:25 +0200 Subject: [PATCH 364/886] Sync newly added diff hunks (#21759) Fixed project diff multi buffer not expanding its diff until edited Release Notes: - N/A --- crates/editor/src/git/project_diff.rs | 10 +++++----- crates/editor/src/hunk_diff.rs | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/editor/src/git/project_diff.rs b/crates/editor/src/git/project_diff.rs index 8fb600c52c..c1fbbee8b2 100644 --- a/crates/editor/src/git/project_diff.rs +++ b/crates/editor/src/git/project_diff.rs @@ -243,7 +243,7 @@ impl ProjectDiffEditor { .map_err(|_| anyhow!("Unexpected non-buffer")) }) .with_context(|| { - format!("loading {} for git diff", entry_path.path.display()) + format!("loading {:?} for git diff", entry_path.path) }) .log_err() else { @@ -313,11 +313,11 @@ impl ProjectDiffEditor { project_diff_editor .update(&mut cx, |project_diff_editor, cx| { project_diff_editor.update_excerpts(id, new_changes, new_entry_order, cx); - for change_set in change_sets { - project_diff_editor.editor.update(cx, |editor, cx| { + project_diff_editor.editor.update(cx, |editor, cx| { + for change_set in change_sets { editor.diff_map.add_change_set(change_set, cx) - }); - } + } + }); }) .ok(); }), diff --git a/crates/editor/src/hunk_diff.rs b/crates/editor/src/hunk_diff.rs index 3f798eaa58..2102c111f6 100644 --- a/crates/editor/src/hunk_diff.rs +++ b/crates/editor/src/hunk_diff.rs @@ -89,7 +89,6 @@ impl DiffMap { self.snapshot .0 .insert(buffer_id, change_set.read(cx).diff_to_buffer.clone()); - Editor::sync_expanded_diff_hunks(self, buffer_id, cx); self.diff_bases.insert( buffer_id, DiffBaseState { @@ -105,6 +104,7 @@ impl DiffMap { change_set, }, ); + Editor::sync_expanded_diff_hunks(self, buffer_id, cx); } pub fn hunks(&self, include_folded: bool) -> impl Iterator { From 803855e7b12fac553f64e3daeac59ac3ca8da584 Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Mon, 9 Dec 2024 12:45:37 -0700 Subject: [PATCH 365/886] Add `async_task::spawn_local` variant that includes caller in panics (#21758) For debugging #21020. Copy-modified [from async_task here](https://github.com/smol-rs/async-task/blob/ca9dbe1db9c422fd765847fa91306e30a6bb58a9/src/runnable.rs#L432) Release Notes: - N/A --- crates/gpui/src/executor.rs | 74 ++++++++++++++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/crates/gpui/src/executor.rs b/crates/gpui/src/executor.rs index 34132d72f8..86d4d0058f 100644 --- a/crates/gpui/src/executor.rs +++ b/crates/gpui/src/executor.rs @@ -1,6 +1,10 @@ use crate::{AppContext, PlatformDispatcher}; +use async_task::Runnable; use futures::channel::mpsc; use smol::prelude::*; +use std::mem::ManuallyDrop; +use std::panic::Location; +use std::thread::{self, ThreadId}; use std::{ fmt::Debug, marker::PhantomData, @@ -440,16 +444,19 @@ impl ForegroundExecutor { } /// Enqueues the given Task to run on the main thread at some point in the future. + #[track_caller] pub fn spawn(&self, future: impl Future + 'static) -> Task where R: 'static, { let dispatcher = self.dispatcher.clone(); + + #[track_caller] fn inner( dispatcher: Arc, future: AnyLocalFuture, ) -> Task { - let (runnable, task) = async_task::spawn_local(future, move |runnable| { + let (runnable, task) = spawn_local_with_source_location(future, move |runnable| { dispatcher.dispatch_on_main_thread(runnable) }); runnable.schedule(); @@ -459,6 +466,71 @@ impl ForegroundExecutor { } } +/// Variant of `async_task::spawn_local` that includes the source location of the spawn in panics. +/// +/// Copy-modified from: +/// https://github.com/smol-rs/async-task/blob/ca9dbe1db9c422fd765847fa91306e30a6bb58a9/src/runnable.rs#L405 +#[track_caller] +fn spawn_local_with_source_location( + future: Fut, + schedule: S, +) -> (Runnable<()>, async_task::Task) +where + Fut: Future + 'static, + Fut::Output: 'static, + S: async_task::Schedule<()> + Send + Sync + 'static, +{ + #[inline] + fn thread_id() -> ThreadId { + std::thread_local! { + static ID: ThreadId = thread::current().id(); + } + ID.try_with(|id| *id) + .unwrap_or_else(|_| thread::current().id()) + } + + struct Checked { + id: ThreadId, + inner: ManuallyDrop, + location: &'static Location<'static>, + } + + impl Drop for Checked { + fn drop(&mut self) { + assert!( + self.id == thread_id(), + "local task dropped by a thread that didn't spawn it. Task spawned at {}", + self.location + ); + unsafe { + ManuallyDrop::drop(&mut self.inner); + } + } + } + + impl Future for Checked { + type Output = F::Output; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + assert!( + self.id == thread_id(), + "local task polled by a thread that didn't spawn it. Task spawned at {}", + self.location + ); + unsafe { self.map_unchecked_mut(|c| &mut *c.inner).poll(cx) } + } + } + + // Wrap the future into one that checks which thread it's on. + let future = Checked { + id: thread_id(), + inner: ManuallyDrop::new(future), + location: Location::caller(), + }; + + unsafe { async_task::spawn_unchecked(future, schedule) } +} + /// Scope manages a set of tasks that are enqueued and waited on together. See [`BackgroundExecutor::scoped`]. pub struct Scope<'a> { executor: BackgroundExecutor, From ef45eca88e0dff078fca579f56d4c5f6f9beaed3 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 9 Dec 2024 15:23:28 -0500 Subject: [PATCH 366/886] extension_host: Fix uploading dev extensions to the remote server (#21761) This PR fixes an issue where dev extensions were not working when uploaded to the remote server. The `extension.toml` for dev extensions may not contain all of the information (such as the list of languages), as this is something that we derive from the filesystem at packaging time. This meant that uploading a dev extension that contained languages could have them absent from the uploaded `extension.toml`. For dev extensions we now upload a serialized version of the in-memory extension manifest, which should have all of the information present. Release Notes: - SSH Remoting: Fixed an issue where some dev extensions would not work after being uploaded to the remote server. --------- Co-authored-by: Conrad --- crates/extension_host/src/extension_host.rs | 52 +++++++++++++++------ 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index 7ceb1fa714..6c965d3d56 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -32,7 +32,7 @@ use gpui::{ }; use http_client::{AsyncBody, HttpClient, HttpClientWithUrl}; use language::{ - LanguageConfig, LanguageMatcher, LanguageName, LanguageQueries, LoadedLanguage, + LanguageConfig, LanguageMatcher, LanguageName, LanguageQueries, LoadedLanguage, Rope, QUERY_FILENAME_PREFIXES, }; use node_runtime::NodeRuntime; @@ -1387,6 +1387,7 @@ impl ExtensionStore { fn prepare_remote_extension( &mut self, extension_id: Arc, + is_dev: bool, tmp_dir: PathBuf, cx: &mut ModelContext, ) -> Task> { @@ -1397,26 +1398,45 @@ impl ExtensionStore { }; 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? - } + const EXTENSION_TOML: &str = "extension.toml"; + const EXTENSION_WASM: &str = "extension.wasm"; + const CONFIG_TOML: &str = "config.toml"; + + if is_dev { + let manifest_toml = toml::to_string(&loaded_extension.manifest)?; + fs.save( + &tmp_dir.join(EXTENSION_TOML), + &Rope::from(manifest_toml), + language::LineEnding::Unix, + ) + .await?; + } else { + fs.copy_file( + &src_dir.join(EXTENSION_TOML), + &tmp_dir.join(EXTENSION_TOML), + fs::CopyOptions::default(), + ) + .await? + } + + if fs.is_file(&src_dir.join(EXTENSION_WASM)).await { + fs.copy_file( + &src_dir.join(EXTENSION_WASM), + &tmp_dir.join(EXTENSION_WASM), + 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")) + .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"), + &src_dir.join(language_path).join(CONFIG_TOML), + &tmp_dir.join(language_path).join(CONFIG_TOML), fs::CopyOptions::default(), ) .await? @@ -1462,6 +1482,7 @@ impl ExtensionStore { this.update(cx, |this, cx| { this.prepare_remote_extension( missing_extension.id.clone().into(), + missing_extension.dev, tmp_dir.path().to_owned(), cx, ) @@ -1476,6 +1497,11 @@ impl ExtensionStore { })? .await?; + log::info!( + "Finished uploading extension {}", + missing_extension.clone().id + ); + client .update(cx, |client, _cx| { client.proto_client().request(proto::InstallExtension { From 6538227f0798677e5925b333e111551801ea1eb7 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 9 Dec 2024 14:15:23 -0700 Subject: [PATCH 367/886] Revert "Avoid endless loop of the diagnostic updates (#21209)" (#21764) This reverts commit 9999c31859210654dd572d54dfa42b67c00b33b0. Release Notes: - Fixes diagnostics not updating in some circumstances --- crates/diagnostics/src/diagnostics.rs | 41 +++++++++------------------ crates/project/src/lsp_store.rs | 15 ---------- 2 files changed, 13 insertions(+), 43 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 9f02033237..d86676729e 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -138,27 +138,16 @@ impl ProjectDiagnosticsEditor { language_server_id, path, } => { - let max_severity = this.max_severity(); - let has_diagnostics_to_display = project.read(cx).lsp_store().read(cx).diagnostics_for_buffer(path) - .into_iter().flatten() - .filter(|(server_id, _)| language_server_id == server_id) - .flat_map(|(_, diagnostics)| diagnostics) - .any(|diagnostic| diagnostic.diagnostic.severity <= max_severity); + this.paths_to_update + .insert((path.clone(), Some(*language_server_id))); + this.summary = project.read(cx).diagnostic_summary(false, cx); + cx.emit(EditorEvent::TitleChanged); - if has_diagnostics_to_display { - this.paths_to_update - .insert((path.clone(), Some(*language_server_id))); - this.summary = project.read(cx).diagnostic_summary(false, cx); - cx.emit(EditorEvent::TitleChanged); - - if this.editor.focus_handle(cx).contains_focused(cx) || this.focus_handle.contains_focused(cx) { - log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. recording change"); - } else { - log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. updating excerpts"); - this.update_stale_excerpts(cx); - } + if this.editor.focus_handle(cx).contains_focused(cx) || this.focus_handle.contains_focused(cx) { + log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. recording change"); } else { - log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. no diagnostics to display"); + log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. updating excerpts"); + this.update_stale_excerpts(cx); } } _ => {} @@ -363,12 +352,16 @@ impl ProjectDiagnosticsEditor { ExcerptId::min() }; - let max_severity = self.max_severity(); let path_state = &mut self.path_states[path_ix]; let mut new_group_ixs = Vec::new(); let mut blocks_to_add = Vec::new(); let mut blocks_to_remove = HashSet::default(); let mut first_excerpt_id = None; + let max_severity = if self.include_warnings { + DiagnosticSeverity::WARNING + } else { + DiagnosticSeverity::ERROR + }; let excerpts_snapshot = self.excerpts.update(cx, |excerpts, cx| { let mut old_groups = mem::take(&mut path_state.diagnostic_groups) .into_iter() @@ -657,14 +650,6 @@ impl ProjectDiagnosticsEditor { prev_path = Some(path); } } - - fn max_severity(&self) -> DiagnosticSeverity { - if self.include_warnings { - DiagnosticSeverity::WARNING - } else { - DiagnosticSeverity::ERROR - } - } } impl FocusableView for ProjectDiagnosticsEditor { diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 6a9acd3048..f719b91942 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -2950,21 +2950,6 @@ impl LspStore { }) } - pub fn diagnostics_for_buffer( - &self, - path: &ProjectPath, - ) -> Option< - &[( - LanguageServerId, - Vec>>, - )], - > { - self.diagnostics - .get(&path.worktree_id)? - .get(&path.path) - .map(|diagnostics| diagnostics.as_slice()) - } - pub fn started_language_servers(&self) -> Vec<(WorktreeId, LanguageServerName)> { self.language_server_ids.keys().cloned().collect() } From 73e0d816c4711fa74e79c0204e9175441b362492 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 9 Dec 2024 13:31:20 -0800 Subject: [PATCH 368/886] Move `ContextMenu` out of editor.rs and rename `ContextMenu` to `CodeContextMenu` (#21766) This is a no-functionality refactor of where the `ContextMenu` type is defined. Just the type definition and implementation is up to almost 1,000 lines; so I've moved it to it's own file and renamed the type to `CodeContextMenu` Release Notes: - N/A --- crates/editor/src/code_context_menus.rs | 895 +++++++++++++++++++++++ crates/editor/src/editor.rs | 934 ++---------------------- crates/editor/src/editor_tests.rs | 30 +- crates/editor/src/element.rs | 9 +- 4 files changed, 956 insertions(+), 912 deletions(-) create mode 100644 crates/editor/src/code_context_menus.rs diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs new file mode 100644 index 0000000000..1817190e42 --- /dev/null +++ b/crates/editor/src/code_context_menus.rs @@ -0,0 +1,895 @@ +use std::{cell::Cell, cmp::Reverse, ops::Range, sync::Arc}; + +use fuzzy::{StringMatch, StringMatchCandidate}; +use gpui::{ + div, px, uniform_list, AnyElement, BackgroundExecutor, Div, FontWeight, ListSizingBehavior, + Model, MouseButton, Pixels, ScrollStrategy, SharedString, StrikethroughStyle, StyledText, + UniformListScrollHandle, ViewContext, WeakView, +}; +use language::Buffer; +use language::{CodeLabel, Documentation}; +use lsp::LanguageServerId; +use multi_buffer::{Anchor, ExcerptId}; +use ordered_float::OrderedFloat; +use parking_lot::RwLock; +use project::{CodeAction, Completion, TaskSourceKind}; +use task::ResolvedTask; +use ui::{ + h_flex, ActiveTheme as _, Color, FluentBuilder as _, InteractiveElement as _, IntoElement, + Label, LabelCommon as _, LabelSize, ListItem, ParentElement as _, Popover, Selectable as _, + StatefulInteractiveElement as _, Styled, StyledExt as _, +}; +use util::ResultExt as _; +use workspace::Workspace; + +use crate::{ + actions::{ConfirmCodeAction, ConfirmCompletion}, + display_map::DisplayPoint, + render_parsed_markdown, split_words, styled_runs_for_code_label, CodeActionProvider, + CompletionId, CompletionProvider, DisplayRow, Editor, EditorStyle, ResolvedTasks, +}; + +pub enum CodeContextMenu { + Completions(CompletionsMenu), + CodeActions(CodeActionsMenu), +} + +impl CodeContextMenu { + pub fn select_first( + &mut self, + provider: Option<&dyn CompletionProvider>, + cx: &mut ViewContext, + ) -> bool { + if self.visible() { + match self { + CodeContextMenu::Completions(menu) => menu.select_first(provider, cx), + CodeContextMenu::CodeActions(menu) => menu.select_first(cx), + } + true + } else { + false + } + } + + pub fn select_prev( + &mut self, + provider: Option<&dyn CompletionProvider>, + cx: &mut ViewContext, + ) -> bool { + if self.visible() { + match self { + CodeContextMenu::Completions(menu) => menu.select_prev(provider, cx), + CodeContextMenu::CodeActions(menu) => menu.select_prev(cx), + } + true + } else { + false + } + } + + pub fn select_next( + &mut self, + provider: Option<&dyn CompletionProvider>, + cx: &mut ViewContext, + ) -> bool { + if self.visible() { + match self { + CodeContextMenu::Completions(menu) => menu.select_next(provider, cx), + CodeContextMenu::CodeActions(menu) => menu.select_next(cx), + } + true + } else { + false + } + } + + pub fn select_last( + &mut self, + provider: Option<&dyn CompletionProvider>, + cx: &mut ViewContext, + ) -> bool { + if self.visible() { + match self { + CodeContextMenu::Completions(menu) => menu.select_last(provider, cx), + CodeContextMenu::CodeActions(menu) => menu.select_last(cx), + } + true + } else { + false + } + } + + pub fn visible(&self) -> bool { + match self { + CodeContextMenu::Completions(menu) => menu.visible(), + CodeContextMenu::CodeActions(menu) => menu.visible(), + } + } + + pub fn render( + &self, + cursor_position: DisplayPoint, + style: &EditorStyle, + max_height: Pixels, + workspace: Option>, + cx: &mut ViewContext, + ) -> (ContextMenuOrigin, AnyElement) { + match self { + CodeContextMenu::Completions(menu) => ( + ContextMenuOrigin::EditorPoint(cursor_position), + menu.render(style, max_height, workspace, cx), + ), + CodeContextMenu::CodeActions(menu) => { + menu.render(cursor_position, style, max_height, cx) + } + } + } +} + +pub enum ContextMenuOrigin { + EditorPoint(DisplayPoint), + GutterIndicator(DisplayRow), +} + +#[derive(Clone, Debug)] +pub struct CompletionsMenu { + pub id: CompletionId, + sort_completions: bool, + pub initial_position: Anchor, + pub buffer: Model, + pub completions: Arc>>, + match_candidates: Arc<[StringMatchCandidate]>, + pub matches: Arc<[StringMatch]>, + pub selected_item: usize, + scroll_handle: UniformListScrollHandle, + resolve_completions: bool, + pub aside_was_displayed: Cell, + show_completion_documentation: bool, +} + +impl CompletionsMenu { + pub fn new( + id: CompletionId, + sort_completions: bool, + show_completion_documentation: bool, + initial_position: Anchor, + buffer: Model, + completions: Box<[Completion]>, + aside_was_displayed: bool, + ) -> Self { + let match_candidates = completions + .iter() + .enumerate() + .map(|(id, completion)| { + StringMatchCandidate::new( + id, + completion.label.text[completion.label.filter_range.clone()].into(), + ) + }) + .collect(); + + Self { + id, + sort_completions, + initial_position, + buffer, + show_completion_documentation, + completions: Arc::new(RwLock::new(completions)), + match_candidates, + matches: Vec::new().into(), + selected_item: 0, + scroll_handle: UniformListScrollHandle::new(), + resolve_completions: true, + aside_was_displayed: Cell::new(aside_was_displayed), + } + } + + pub fn new_snippet_choices( + id: CompletionId, + sort_completions: bool, + choices: &Vec, + selection: Range, + buffer: Model, + ) -> Self { + let completions = choices + .iter() + .map(|choice| Completion { + old_range: selection.start.text_anchor..selection.end.text_anchor, + new_text: choice.to_string(), + label: CodeLabel { + text: choice.to_string(), + runs: Default::default(), + filter_range: Default::default(), + }, + server_id: LanguageServerId(usize::MAX), + documentation: None, + lsp_completion: Default::default(), + confirm: None, + }) + .collect(); + + let match_candidates = choices + .iter() + .enumerate() + .map(|(id, completion)| StringMatchCandidate::new(id, completion.to_string())) + .collect(); + let matches = choices + .iter() + .enumerate() + .map(|(id, completion)| StringMatch { + candidate_id: id, + score: 1., + positions: vec![], + string: completion.clone(), + }) + .collect(); + Self { + id, + sort_completions, + initial_position: selection.start, + buffer, + completions: Arc::new(RwLock::new(completions)), + match_candidates, + matches, + selected_item: 0, + scroll_handle: UniformListScrollHandle::new(), + resolve_completions: false, + aside_was_displayed: Cell::new(false), + show_completion_documentation: false, + } + } + + fn select_first( + &mut self, + provider: Option<&dyn CompletionProvider>, + cx: &mut ViewContext, + ) { + self.selected_item = 0; + self.scroll_handle + .scroll_to_item(self.selected_item, ScrollStrategy::Top); + self.resolve_selected_completion(provider, cx); + cx.notify(); + } + + fn select_prev( + &mut self, + provider: Option<&dyn CompletionProvider>, + cx: &mut ViewContext, + ) { + if self.selected_item > 0 { + self.selected_item -= 1; + } else { + self.selected_item = self.matches.len() - 1; + } + self.scroll_handle + .scroll_to_item(self.selected_item, ScrollStrategy::Top); + self.resolve_selected_completion(provider, cx); + cx.notify(); + } + + fn select_next( + &mut self, + provider: Option<&dyn CompletionProvider>, + cx: &mut ViewContext, + ) { + if self.selected_item + 1 < self.matches.len() { + self.selected_item += 1; + } else { + self.selected_item = 0; + } + self.scroll_handle + .scroll_to_item(self.selected_item, ScrollStrategy::Top); + self.resolve_selected_completion(provider, cx); + cx.notify(); + } + + fn select_last( + &mut self, + provider: Option<&dyn CompletionProvider>, + cx: &mut ViewContext, + ) { + self.selected_item = self.matches.len() - 1; + self.scroll_handle + .scroll_to_item(self.selected_item, ScrollStrategy::Top); + self.resolve_selected_completion(provider, cx); + cx.notify(); + } + + pub fn resolve_selected_completion( + &mut self, + provider: Option<&dyn CompletionProvider>, + cx: &mut ViewContext, + ) { + if !self.resolve_completions { + return; + } + let Some(provider) = provider else { + return; + }; + + let completion_index = self.matches[self.selected_item].candidate_id; + let resolve_task = provider.resolve_completions( + self.buffer.clone(), + vec![completion_index], + self.completions.clone(), + cx, + ); + + cx.spawn(move |editor, mut cx| async move { + if let Some(true) = resolve_task.await.log_err() { + editor.update(&mut cx, |_, cx| cx.notify()).ok(); + } + }) + .detach(); + } + + fn visible(&self) -> bool { + !self.matches.is_empty() + } + + fn render( + &self, + style: &EditorStyle, + max_height: Pixels, + workspace: Option>, + cx: &mut ViewContext, + ) -> AnyElement { + let show_completion_documentation = self.show_completion_documentation; + let widest_completion_ix = self + .matches + .iter() + .enumerate() + .max_by_key(|(_, mat)| { + let completions = self.completions.read(); + let completion = &completions[mat.candidate_id]; + let documentation = &completion.documentation; + + let mut len = completion.label.text.chars().count(); + if let Some(Documentation::SingleLine(text)) = documentation { + if show_completion_documentation { + len += text.chars().count(); + } + } + + len + }) + .map(|(ix, _)| ix); + + let completions = self.completions.clone(); + let matches = self.matches.clone(); + let selected_item = self.selected_item; + let style = style.clone(); + + let multiline_docs = if show_completion_documentation { + let mat = &self.matches[selected_item]; + match &self.completions.read()[mat.candidate_id].documentation { + Some(Documentation::MultiLinePlainText(text)) => { + Some(div().child(SharedString::from(text.clone()))) + } + Some(Documentation::MultiLineMarkdown(parsed)) if !parsed.text.is_empty() => { + Some(div().child(render_parsed_markdown( + "completions_markdown", + parsed, + &style, + workspace, + cx, + ))) + } + Some(Documentation::Undocumented) if self.aside_was_displayed.get() => { + Some(div().child("No documentation")) + } + _ => None, + } + } else { + None + }; + + let aside_contents = if let Some(multiline_docs) = multiline_docs { + Some(multiline_docs) + } else if self.aside_was_displayed.get() { + Some(div().child("Fetching documentation...")) + } else { + None + }; + self.aside_was_displayed.set(aside_contents.is_some()); + + let aside_contents = aside_contents.map(|div| { + div.id("multiline_docs") + .max_h(max_height) + .flex_1() + .px_1p5() + .py_1() + .min_w(px(260.)) + .max_w(px(640.)) + .w(px(500.)) + .overflow_y_scroll() + .occlude() + }); + + let list = uniform_list( + cx.view().clone(), + "completions", + matches.len(), + move |_editor, range, cx| { + let start_ix = range.start; + let completions_guard = completions.read(); + + matches[range] + .iter() + .enumerate() + .map(|(ix, mat)| { + let item_ix = start_ix + ix; + let candidate_id = mat.candidate_id; + let completion = &completions_guard[candidate_id]; + + let documentation = if show_completion_documentation { + &completion.documentation + } else { + &None + }; + + let highlights = gpui::combine_highlights( + mat.ranges().map(|range| (range, FontWeight::BOLD.into())), + styled_runs_for_code_label(&completion.label, &style.syntax).map( + |(range, mut highlight)| { + // Ignore font weight for syntax highlighting, as we'll use it + // for fuzzy matches. + highlight.font_weight = None; + + if completion.lsp_completion.deprecated.unwrap_or(false) { + highlight.strikethrough = Some(StrikethroughStyle { + thickness: 1.0.into(), + ..Default::default() + }); + highlight.color = Some(cx.theme().colors().text_muted); + } + + (range, highlight) + }, + ), + ); + let completion_label = StyledText::new(completion.label.text.clone()) + .with_highlights(&style.text, highlights); + let documentation_label = + if let Some(Documentation::SingleLine(text)) = documentation { + if text.trim().is_empty() { + None + } else { + Some( + Label::new(text.clone()) + .ml_4() + .size(LabelSize::Small) + .color(Color::Muted), + ) + } + } else { + None + }; + + let color_swatch = completion + .color() + .map(|color| div().size_4().bg(color).rounded_sm()); + + div().min_w(px(220.)).max_w(px(540.)).child( + ListItem::new(mat.candidate_id) + .inset(true) + .selected(item_ix == selected_item) + .on_click(cx.listener(move |editor, _event, cx| { + cx.stop_propagation(); + if let Some(task) = editor.confirm_completion( + &ConfirmCompletion { + item_ix: Some(item_ix), + }, + cx, + ) { + task.detach_and_log_err(cx) + } + })) + .start_slot::
(color_swatch) + .child(h_flex().overflow_hidden().child(completion_label)) + .end_slot::